Skip to content

feat: working CLI, vitest 4, modernized tooling, Changesets-driven releases#13

Open
adiled wants to merge 31 commits into
mainfrom
feat/cli-improvements
Open

feat: working CLI, vitest 4, modernized tooling, Changesets-driven releases#13
adiled wants to merge 31 commits into
mainfrom
feat/cli-improvements

Conversation

@adiled
Copy link
Copy Markdown
Owner

@adiled adiled commented Jun 15, 2025

Closes #8.

Ships as 2.0.0 (1.2.1 → major). The major bump is driven by engines.node ≥22.12.0 for npm consumers — the runtime API itself is backward-compatible (smoke-verified). For non-Node users, a new install.sh distribution path means most people can install + use the CLI without ever touching npm or having Node pre-installed.

Distribution

  • curl -fsSL .../install.sh | sh — the installer uses the host's Node ≥22.12 if present; otherwise it downloads the official Node binary from nodejs.org, installs the package into ~/.ohlc/lib, and writes a ~/.local/bin/ohlc launcher. No per-platform binaries published — the npm tarball is the single release artifact, the installer does the platform work client-side.
  • npm install ohlc-resample — for JS/TS projects that want the library API. Requires Node ≥22.12.

Breaking change

  • engines.node ≥22.12.0 (was ≥20.12.2). Required by vite 7 / vitest 4 (require-of-ESM only stable from Node 22.12+). Runtime library API on supported Node versions is unchanged.

CLI (was a non-functional stub in 1.x; now ships)

  • --input-format <csv|json|auto> honors the requested format (was a no-op heuristic).
  • New -s, --shape <object|array|auto> for OHLCV tuple vs object JSON output. Auto-detects shape on JSON input and preserves it through the pipeline by default.
  • -i always wins over stdin, even in non-TTY contexts (CI, scripts) — caught while ad-hoc kicking the tires.
  • Malformed CSV rows reported to stderr; zero valid rows now exits non-zero instead of producing empty output.
  • bin.ohlcdist/cli.js (was src/cli.ts, non-functional post-install).
  • SIGINT/SIGTERM preserve a previously-set non-zero exit code.

Library (BC preserved)

  • resampleOhlcv has overloaded signatures: return type follows input shape (OHLCV[] in → OHLCV[] out, same for IOHLCV[]). Original union signature kept as a third overload, so 1.x TS callers with union-typed data still compile.
  • Types re-exported from the package entrypoint.
  • Fixed inverted JSDoc on resampleOhlcvArray.
  • Legacy default export with resample_ohlcv / array / json / trade_to_candle / tick_chart aliases preserved unchanged.
  • Verified empirically: declarations diff vs origin/main is strictly additive; runtime smoke test on every public function and every default-export alias shows zero behavioural drift.

Build & tooling

  • TypeScript 5.x → 6.x. tsconfig modernized (target: es2022, lib: es2022, types: ["node"]).
  • Test runner jest → vitest 4. Cold-start ~9s → ~2.5s.
  • Releases managed by Changesets with a direct-commit workflow: a push to main with a changeset triggers bump + commit + npm publish (with provenance) + tag + GitHub Release in a single CI run.
  • Dropped unused chalk, coveralls, fast-csv deps; moved ts-node to devDependencies.
  • All other deps bumped to latest.
  • CI: triggers on main, runs Node 22.x and 24.x.
  • Removed obsolete .npmignore, dropped stale deprecated.md, cleaned coveralls/yarn/master references from README.

Release flow after this PR

  1. Squash-merge this PR (or push branch into main).
  2. CI sees the seed changeset, runs pnpm changeset version, commits the bump (1.2.1 → 2.0.0) back to main with [skip ci], then publishes 2.0.0 to npm with provenance, tags v2.0.0, creates the GitHub Release.

One-time setup before merging

  • Add NPM_TOKEN (npm "Granular Access Token", read+write on this package) as a repo secret on GitHub.

Test status

  • 48 tests passing in ~2.5s under vitest 4 + Node 22.
  • BC-verified empirically against origin/main's build (zero drift).
  • install.sh verified end-to-end on both host-Node and BYO-Node paths.

@adiled adiled changed the title feat(cli): add pipe support and improve data handling feat(cli): add pipe support and improve data handling [min-vibable] Jun 15, 2025
adiled added 3 commits May 9, 2026 13:31
BREAKING CHANGE: removes the default export from the package entrypoint.
The legacy aliases `resample_ohlcv`, `array`, `json`, `trade_to_candle`,
and `tick_chart` are gone. Use the named exports `resampleOhlcv`,
`resampleTicksByTime`, and `resampleTicksByCount` directly.

- `resampleOhlcv` now has overloaded signatures so the return type
  follows the input shape (`OHLCV[]` in → `OHLCV[]` out, same for
  `IOHLCV[]`).
- Fixed inverted JSDoc on `resampleOhlcvArray`.
- Re-export types from the package entrypoint.
- Update existing library tests to use named imports.
BREAKING CHANGE: `parseCSV` (CLI helper, not part of the library API)
now returns `{ rows, skipped }` instead of `IOHLCV[]`.

Issue #8's "standardize to OHLCV (array-of-arrays)" bullet is now
addressed.

- `--input-format <csv|json|auto>` now actually honors the requested
  format (was previously a no-op heuristic that ignored the flag).
- New `-s, --shape <object|array|auto>` flag for OHLCV tuple vs object
  JSON output. Auto-detects shape on JSON input and preserves it
  through the pipeline by default.
- `parseCSV` reports skipped rows; the CLI now warns to stderr and
  errors out when a CSV produces zero valid rows (was silently exiting
  0 with empty output).
- Collapsed triple-nested error wrapping in `readFileData` to a single
  `prefixError` helper.
- Extracted `formatCSV`/`formatJSON` writer helpers.
- SIGINT/SIGTERM now use `process.exit()` so they preserve a
  previously-set non-zero `exitCode`.
- Dropped redundant `stdoutStream` default in `writeOutput`.
- Tests now check the real `process.exitCode` (the previous
  `getExitCode()` helper was a tautology). Added array-of-arrays
  shape coverage.
- README CLI section updated for the new flags.
BREAKING CHANGE: `bin.ohlc` now points at `dist/cli.js` (was
`src/cli.ts`). The previous binary was non-functional post-install
because npm has no way to execute raw TypeScript; this fix changes
the resolved path. Combined with the library and CLI breaking
changes in the same release, this warrants a major bump.

- `bin.ohlc` → `dist/cli.js`; `src/cli.ts` retains its shebang so the
  emitted JS is directly executable.
- Drop unused `chalk` runtime dep; drop `coveralls` devDep.
- Move `ts-node` to devDependencies (no longer needed at runtime now
  that the bin points at compiled JS).
- `prebuild` now cleans `dist/` (was incorrectly cleaning `build/`).
- `prepublishOnly` simplified: a single test+build pass without the
  coveralls step that always failed without a token.
- Drop obsolete `.npmignore` — superseded by `package.json#files`.
- Add `types: dist/index.d.ts` to advertise type declarations.
- CI workflow now triggers on `main` (was `master`) and runs Node
  20.x and 22.x (was EOL 10.x and 12.x). Switched to pnpm with
  frozen lockfile, bumped action versions.
- Added CHANGELOG.md with the full 2.0.0 breaking-change list.
@adiled adiled force-pushed the feat/cli-improvements branch from ed78cd7 to 88317d8 Compare May 9, 2026 08:32
adiled added 4 commits May 9, 2026 13:39
- Replace jest + ts-jest + @types/jest with vitest + @vitest/coverage-v8.
- New `vitest.config.ts`, drops `jest.config.js` (incl. its ts-jest
  `ignoreCodes: [2345]` workaround).
- Add `test:watch` and `test:coverage` scripts. The default `test`
  script no longer collects coverage on every run; use `test:coverage`
  on demand.
- Add explicit `import { ... } from 'vitest'` at the top of each test
  file. `jest.spyOn` → `vi.spyOn` in the one place it appears.
- Fix a pre-existing race in `withTimeout`: a slow `testFn` could log
  after the test had torn down ("Cannot log after tests are done").
  Tracked via a `settled` flag so post-rejection resolutions are
  ignored.

Cold-start test time: ~9s → ~2.5s.
Re-target this PR as a backward-compatible 1.4.0 minor instead of 2.0.
- Restore the legacy `default` export with the `resample_ohlcv`,
  `array`, `json`, `trade_to_candle`, `tick_chart` aliases. 1.x
  consumers using `import lib from 'ohlc-resample'; lib.tick_chart(...)`
  continue to work unchanged.
- Source the CLI's `--version` string from `package.json` at runtime
  via `require('../package.json')`, so version bumps stay in one
  place (matches the project's manual-bump convention).
- Bump to 1.4.0 in `package.json`.
- Rewrite CHANGELOG entry as 1.4.0; drop the breaking-changes section.

Note: prior commits on this branch carry stale `!` markers and
`BREAKING CHANGE:` trailers from when this was scoped as 2.0. The
final state of the branch is non-breaking; squash-merging the PR is
the cleanest path.
Pushing a `v*` tag now runs build + tests, then `pnpm publish` with
npm provenance. Replaces the implicit "publish from a maintainer's
laptop" model.

One-time setup before the first release:
1. Generate an npm "Automation" granular access token scoped to this
   package (npmjs.com → Access Tokens).
2. Add it to the repo as the `NPM_TOKEN` secret.

Release flow:
- Bump `package.json#version`, commit, merge to `main`.
- `git tag v1.4.0 && git push --tags`.
- The workflow tests, builds, and publishes with provenance attesting
  the artifact came from this exact commit + workflow run.
Replaces the tag-triggered publish workflow with a Changesets-driven
release pipeline. Every PR that affects users carries a markdown file
in `.changeset/` describing the change and the semver bump. On merge
to `main`, the action either opens a "Version Packages" PR (consuming
pending changesets, bumping `package.json#version`, regenerating
`CHANGELOG.md`) or, when no changesets are pending, publishes to npm
with provenance and creates a matching GitHub Release.

This delivers the trio (git tag + GitHub Release + npm publish) as a
single consequence of merging the Version Packages PR, and tracks
release notes in the file system instead of relying on commit message
discipline.

Changes
- New `release.yml` workflow using `changesets/action@v1` with
  `contents: write`, `pull-requests: write`, and `id-token: write`
  (npm provenance).
- Drop the prior `publish.yml`.
- Add `@changesets/cli` and `@changesets/changelog-github` as
  devDependencies. Changelog entries are auto-linked to PRs/contributors.
- New `release` script: `pnpm publish --no-git-checks --access public --provenance`.
- New `changeset` script for ergonomics (`pnpm changeset`).
- Reset `CHANGELOG.md` to a stub header so Changesets owns it going
  forward.
- Roll `package.json#version` back to 1.3.0. The Version Packages PR
  produced after merging this one will propose the bump to 1.4.0
  driven by the seed changeset.
- Seed `.changeset/working-cli-and-modern-tooling.md` capturing the
  full body of work in this PR; this becomes the 1.4.0 release note.
- README: new "Releasing" section documenting the flow.

One-time setup before the first release
- Add `NPM_TOKEN` (npm "Granular Access Token", read+write on this
  package) as a repo secret.
- Settings → Actions → General → enable "Allow GitHub Actions to
  create and approve pull requests".
@adiled adiled changed the title feat(cli): add pipe support and improve data handling [min-vibable] feat(cli): working CLI, vitest, Changesets-managed releases May 9, 2026
adiled added 7 commits May 9, 2026 15:38
- README: drop coveralls badge + link (coveralls was removed earlier
  in this branch); fix master → main in remaining links; replace
  yarn → pnpm in the test instructions; drop reference to a
  CONTRIBUTING.md that doesn't exist; point license link to COPYING.
- Remove deprecated.md — coveralls is gone and TypeScript is on 5.x,
  the file's contents no longer reflect reality.
- prepublishOnly: switch to pnpm for consistency with the rest of
  the toolchain.
Make the layered architecture explicit: GitHub is the conveyer belt
for our CI, not a dependency of the release model. The Changesets
machinery (`.changeset/*.md`, CHANGELOG, version-bump logic, npm
publish) is portable to any forge or to no forge at all. Only
`.github/workflows/release.yml` is GitHub-specific.

Adds two subsections under Releasing:
- "Hosting independence" — table of layers and what to swap when
  moving off GitHub.
- "Manual release" — the local laptop flow for when CI is
  unavailable, the project hasn't been pushed to a forge yet, or
  you simply prefer hand-driven publishes.
Replace the changesets/action PR-opening behaviour with raw CLI calls
that bump, commit-to-main with [skip ci], publish, tag, and create the
GitHub Release in a single workflow run from one push to main.

Rationale: the release decision is driven entirely by what's in
`.changeset/`. The "Version Packages" PR was just plumbing for repos
with branch protection on `main`. This branch isn't protected, so
collapse the two-PR dance into a single push-and-ship.

Behaviour:
- Push to main with a `.changeset/*.md` → CI bumps, commits the bump
  back to main with [skip ci], publishes, tags, creates GH Release.
- Push to main without a changeset → CI exits cleanly, nothing
  released. Doc-only edits and refactors are silent.

Order of operations is publish-then-tag so a phantom version on main
without a published artifact is avoided. (Re-running after a publish
failure is safe — npm rejects duplicate versions.)

Permissions trimmed: dropped `pull-requests: write` since we no
longer open PRs.

Side effects:
- Removed the "Allow GitHub Actions to create and approve pull
  requests" prerequisite from the README — direct-commit flow
  doesn't need it.
- Local `main` will be one commit behind after each release; pull to
  catch up.
- gitignore: AGENTS.md and CLAUDE.md added (local-only agent context).
The two-overload form added in this branch (`OHLCV[]` in → `OHLCV[]`
out, `IOHLCV[]` in → `IOHLCV[]` out) is strictly narrower than the
1.x signature. Callers with code like

    function process(data: OHLCV[] | IOHLCV[]) {
      return resampleOhlcv(data, opts); // 1.x: OK; 1.4.0-pre: type error
    }

would fail to type-check, because TypeScript overload resolution
doesn't match a union argument against the individual overloads.

Add the original union signature as a third overload. The narrower
overloads remain preferred for new code; the union form keeps 1.x
TypeScript callers compiling.

Verified empirically against `origin/main`'s build:
- declarations diff is now strictly additive
- runtime smoke test (each public function + every default export
  alias on identical inputs) shows no behavioural drift
Earlier commits on this branch bumped `package.json#version` from
1.2.1 (matches `main`) to 1.3.0 / 1.4.0 by hand. With Changesets
adopted, the version is supposed to be a derived value — the workflow
runs `pnpm changeset version` post-merge and computes the bump from
the pending `.changeset/*.md` files.

Reset the working tree's version to match `main` (1.2.1). The seed
changeset declares "minor", so the next release will ship as 1.3.0
(previous published was 1.2.1 → minor → 1.3.0).

Verified locally: `pnpm changeset version` against this state
produces `"version": "1.3.0"` and a clean `## 1.3.0` CHANGELOG entry.
Major-bump worthy because of `engines.node` ≥22.12.0 (was ≥20.12.2).
Runtime API surface is unchanged; smoke-tested against `origin/main`'s
build with zero behavioural drift on every public function and every
default-export alias.

Deps
- typescript: 5.8 → 6.x. tsconfig modernized: `target: es2022`,
  `lib: es2022`, explicit `types: ["node"]`,
  `ignoreDeprecations: "6.0"` for `moduleResolution: node` (will need
  a node16/nodenext migration before TS 7).
- vitest: 2.x → 4.x. Requires Node ≥22.12 because vite 7 + std-env
  use require-of-ESM, which is only stable from Node 22.12+.
- @vitest/coverage-v8 to match.
- @types/node: 24 → 25.
- rimraf, commander, lodash, @types/lodash, @changesets/changelog-github
  — all to latest within range.

Engines / CI
- engines.node: ≥20.12.2 → ≥22.12.0.
- nodejs.yml matrix: [20.x, 22.x] → [22.x, 24.x].
- release.yml setup-node: 22.x → 24.x.

CLI fix (caught by ad-hoc kicking-the-tires)
- `-i` now always wins over stdin, regardless of `isTTY`. The previous
  `isTTY && options.input` gate broke any non-interactive caller (CI,
  scripted invocations) where stdin is not a TTY but `-i` is the
  intended source. Removed the unused `isTTY` parameter from `runCli`
  and added a regression test.

Seed changeset relabelled `minor → major` so the next release ships as
2.0.0, matching the engine bump's semver impact.
The Releasing / Hosting independence / Manual release subsections
were maintainer-facing and don't belong in a public-facing README.
That content lives in the gitignored AGENTS.md.
@adiled adiled changed the title feat(cli): working CLI, vitest, Changesets-managed releases feat: working CLI, vitest 4, modernized tooling, Changesets-driven releases May 9, 2026
adiled added 3 commits May 9, 2026 18:45
Audience expansion: users no longer need Node.js to install or use the
CLI. Two parallel distribution channels, both fed by the same release
event:

1. npm — for JS/TS projects (existing)
2. install.sh + standalone binary — for everyone else (new)

How it works
- New `install.sh` at the repo root detects platform (Darwin/Linux,
  arm64/x64), resolves the latest release tag via GitHub's API, and
  downloads the matching binary to `$HOME/.local/bin/ohlc`. PATH hint
  emitted if the install dir isn't on PATH. Flags: `--version` to pin,
  `--bin-dir` to customize destination.
- `release.yml` extended with a parallel `binaries` matrix job that
  runs only when an actual release fired. For each of darwin-arm64,
  darwin-x64, linux-x64, linux-arm64, and windows-x64, the matching
  runner installs Bun, runs `bun build --compile --target=...` against
  `src/cli.ts`, and uploads the resulting self-contained binary as a
  release asset. Each binary is ~60MB (Bun runtime is bundled).
- README's Install section now leads with the curl-pipe-sh path; the
  npm path is listed below for library consumers.

Locally verified
- `bun build --compile --target=bun-darwin-arm64 src/cli.ts` produces
  a working 60MB binary that responds to `--version`, `--help`, pipe
  input, file input, all output shapes, and reads its version from
  the bundled package.json (single source of truth preserved).
- `install.sh` syntax-checks clean (`bash -n`) and `--help` renders
  the documented usage.
Constraint clarified: Node is the runtime everywhere. No third-party JS
runtimes (Bun, Deno, …) at dev, in CI, in shipped artifacts, or in
installer-provided runtimes.

Distribution model
- Single source of truth: the npm package.
- `install.sh` runs client-side. If the host has Node ≥22.12, it uses
  it. Otherwise it downloads the official Node binary distribution
  from nodejs.org into `${INSTALL_DIR}/runtime/`, then `npm install`s
  the package into `${INSTALL_DIR}/lib/` and writes a `${BIN_DIR}/ohlc`
  launcher that exec's `<chosen-node> <installed-cli.js> "$@"`.
- No per-platform binaries are built or published.

Reverts
- The `binaries` matrix job in release.yml (was building per-platform
  binaries via `bun build --compile`).
- The README copy that advertised "no Node.js required."

Bug fixed in install.sh during this rewrite
- The bundled `npm` script's `#!/usr/bin/env node` shebang failed when
  `node` wasn't on `PATH`. Fix: prepend the chosen Node's bin dir to
  `PATH` for the install call.

Verified end-to-end
- Host-Node path: install.sh detected my host Node 22.17.1, npm-installed
  the package, launcher resolves to host Node.
- BYO-Node path: with `PATH=/usr/bin:/bin` (no Node on PATH), install.sh
  downloaded Node 22.12.0 from nodejs.org, npm-installed the package
  using the bundled Node, launcher exec's the bundled Node correctly.

AGENTS.md (gitignored) now documents this constraint up top so future
agents don't reach for Bun (or Deno, or anything else) to solve a
distribution problem.
Same script, two modes. Uninstall removes the launcher (`$BIN_DIR/ohlc`)
and the install dir (`$INSTALL_DIR`, runtime + lib). Both removals are
gated by sanity checks: the launcher must reference our package's
`cli.js`; the install dir must contain `lib/node_modules/$PKG` or a
`runtime/` we wrote. Pass `--force` to override the gate.

Verified locally:
- Real install + --uninstall round-trip cleanly removes everything.
- A planted foreign launcher and unrelated dir are refused without
  --force; --force removes them.
- Re-running --uninstall on an empty target is a clean no-op.

README updated with the uninstall one-liner under the Install section.
AGENTS.md notes the safety gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Extend package with CLI

1 participant