@sr-labs/devkit 0.0.0-stage → 0.1.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.
Files changed (49) hide show
  1. package/.gitlab-ci.yml +24 -0
  2. package/.nvmrc +1 -0
  3. package/AGENTS.md +15 -0
  4. package/CHANGELOG.md +17 -0
  5. package/LICENSE +21 -0
  6. package/README.md +70 -2
  7. package/docs/agents/domain.md +51 -0
  8. package/docs/agents/issue-tracker.md +46 -0
  9. package/docs/agents/triage-labels.md +15 -0
  10. package/docs/specs/devkit-rename-and-layout.md +71 -0
  11. package/package.json +20 -4
  12. package/packages/changelog/README.md +7 -0
  13. package/packages/changelog/package.json +33 -0
  14. package/packages/changelog/src/cli.ts +4 -0
  15. package/packages/changelog/src/commits.ts +29 -0
  16. package/packages/changelog/src/git.ts +11 -0
  17. package/packages/changelog/src/index.ts +4 -0
  18. package/packages/changelog/src/main.ts +33 -0
  19. package/packages/changelog/src/notes.ts +60 -0
  20. package/packages/changelog/test/notes.test.ts +47 -0
  21. package/packages/changelog/tsconfig.json +6 -0
  22. package/packages/core/README.md +3 -0
  23. package/packages/core/package.json +27 -0
  24. package/packages/core/src/git.ts +41 -0
  25. package/packages/core/src/index.ts +1 -0
  26. package/packages/core/tsconfig.json +5 -0
  27. package/packages/devkit/README.md +11 -0
  28. package/packages/devkit/package.json +34 -0
  29. package/packages/devkit/src/cli.ts +4 -0
  30. package/packages/devkit/src/commands.ts +14 -0
  31. package/packages/devkit/src/index.ts +2 -0
  32. package/packages/devkit/src/main.ts +27 -0
  33. package/packages/devkit/test/cli.test.ts +74 -0
  34. package/packages/devkit/tsconfig.json +6 -0
  35. package/packages/release-tag/README.md +6 -0
  36. package/packages/release-tag/package.json +34 -0
  37. package/packages/release-tag/src/cli.ts +4 -0
  38. package/packages/release-tag/src/index.ts +3 -0
  39. package/packages/release-tag/src/main.ts +79 -0
  40. package/packages/release-tag/src/package-files.ts +92 -0
  41. package/packages/release-tag/src/version.ts +41 -0
  42. package/packages/release-tag/test/cli.test.ts +68 -0
  43. package/packages/release-tag/test/package-files.test.ts +70 -0
  44. package/packages/release-tag/test/version.test.ts +32 -0
  45. package/packages/release-tag/tsconfig.json +6 -0
  46. package/pnpm-workspace.yaml +2 -0
  47. package/tsconfig.base.json +18 -0
  48. package/tsconfig.json +9 -0
  49. package/tsconfig.test.json +10 -0
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,24 @@
1
+ image: node:24
2
+
3
+ stages: [test, publish]
4
+
5
+ default:
6
+ before_script:
7
+ - corepack enable
8
+ - pnpm install --frozen-lockfile
9
+
10
+ test:
11
+ stage: test
12
+ script:
13
+ - pnpm test
14
+
15
+ # Publishes every workspace package to npmjs.org when a vX.Y.Z tag is pushed.
16
+ # pnpm rewrites `workspace:^` dependency ranges to the released version.
17
+ publish:
18
+ stage: publish
19
+ rules:
20
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
21
+ script:
22
+ - pnpm run build
23
+ - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
24
+ - pnpm -r publish --no-git-checks
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v24.12.0
package/AGENTS.md ADDED
@@ -0,0 +1,15 @@
1
+ # AGENTS.md
2
+
3
+ ## Agent skills
4
+
5
+ ### Issue tracker
6
+
7
+ Issues are tracked in GitLab Issues (`gitlab.com/sr-labs/devkit`) via the `glab` CLI; external merge requests are not a triage surface. See `docs/agents/issue-tracker.md`.
8
+
9
+ ### Triage labels
10
+
11
+ Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
12
+
13
+ ### Domain docs
14
+
15
+ Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## v0.1.0 - 2026-10-19
4
+
5
+ ### Features
6
+
7
+ - rename to devkit, extract core and add umbrella devkit CLI
8
+
9
+ ## v0.0.3 - 2026-10-15
10
+
11
+ ### Fixes
12
+
13
+ - restore root workspace package.json
14
+
15
+ ## v0.0.2 - 2026-10-15
16
+
17
+ ## v0.0.1 - 2026-10-15
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sr-labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,71 @@
1
- # Temporary Holding Version
1
+ # devkit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A series of tools that help set up the development environment of projects, published as public npm packages under [`@sr-labs`](https://www.npmjs.com/org/sr-labs).
4
+
5
+ > This project was renamed from `dev-tools`; the old GitLab path `sr-labs/dev-tools` now redirects to `sr-labs/devkit`.
6
+
7
+ | Package | Bin | What it does |
8
+ | ---------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- |
9
+ | [`@sr-labs/devkit`](packages/devkit) | `devkit` | Umbrella: one `devkit` command with subcommands (`devkit release`, `devkit changelog`). |
10
+ | [`@sr-labs/release-tag`](packages/release-tag) | `release-tag` | Next `vX.Y.Z` from Conventional Commits; bumps package files, updates `CHANGELOG.md`, commits and tags. |
11
+ | [`@sr-labs/changelog`](packages/changelog) | `changelog` | Regenerates `CHANGELOG.md` from every `vX.Y.Z` tag. |
12
+ | [`@sr-labs/core`](packages/core) | - | Shared git helpers; library only. |
13
+
14
+ Install `@sr-labs/devkit` to get every tool with one dependency. Install `@sr-labs/release-tag` or
15
+ `@sr-labs/changelog` alone when you need a single tool; their bins and flags are the same as the `devkit` subcommands.
16
+
17
+ ## Using the packages in a repository
18
+
19
+ ```sh
20
+ pnpm add -D @sr-labs/devkit # or npm i -D; or just @sr-labs/release-tag / @sr-labs/changelog
21
+ ```
22
+
23
+ ```json
24
+ "scripts": {
25
+ "release:tag": "devkit release",
26
+ "changelog": "devkit changelog"
27
+ }
28
+ ```
29
+
30
+ ```sh
31
+ pnpm release:tag # dry run: prints next version and notes
32
+ pnpm release:tag --apply # bump, CHANGELOG.md, commit, annotated tag
33
+ pnpm release:tag --push # ...and push the tag
34
+ pnpm changelog # rewrite CHANGELOG.md from all tags
35
+ pnpm changelog --unreleased --stdout
36
+ ```
37
+
38
+ ## Rules
39
+
40
+ - Tags are `vX.Y.Z`. `feat` bumps minor, `fix`/`perf` bump patch, `!` or a `BREAKING CHANGE:` footer bumps
41
+ major (minor before 1.0). Other types never trigger a release.
42
+ - `--apply` rewrites `package.json`, `package-lock.json` (if present) and, for workspaces
43
+ (`pnpm-workspace.yaml`, else `package.json` `workspaces`; `dir/*` or literal paths), every workspace
44
+ `package.json` plus sibling dependency ranges, so a monorepo releases in lockstep. `workspace:` ranges are
45
+ left for pnpm to rewrite at publish.
46
+ - An existing `CHANGELOG.md` gets the new section inserted above the newest `## ` heading, so hand edits
47
+ survive. Without one, the full history is generated. `--no-changelog` skips it.
48
+
49
+ ## Developing
50
+
51
+ Uses pnpm (version pinned in `packageManager`; `corepack enable` picks it up).
52
+
53
+ ```sh
54
+ pnpm install
55
+ pnpm test # builds (tsc -b) then runs vitest, including an end-to-end run in a temp git repo
56
+ pnpm run check # typecheck sources and tests
57
+ ```
58
+
59
+ Packages ship compiled JS in `dist/` because Node does not strip TypeScript types inside `node_modules`.
60
+
61
+ ## Releasing
62
+
63
+ This repo releases itself with its own tool. All packages share one version.
64
+
65
+ ```sh
66
+ pnpm release:tag --push
67
+ ```
68
+
69
+ The tag push runs the `publish` job in `.gitlab-ci.yml`, which publishes every workspace package to npmjs.org
70
+ using the `NPM_TOKEN` GitLab CI/CD variable (an npm automation/granular access token with publish access to
71
+ `@sr-labs`). Note `--push` pushes only the tag; push `main` too.
@@ -0,0 +1,51 @@
1
+ # Domain Docs
2
+
3
+ How the engineering skills should consume this repo's domain documentation when exploring the codebase.
4
+
5
+ ## Before exploring, read these
6
+
7
+ - **`CONTEXT.md`** at the repo root, or
8
+ - **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
9
+ - **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
10
+
11
+ If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
12
+
13
+ ## File structure
14
+
15
+ Single-context repo (most repos):
16
+
17
+ ```
18
+ /
19
+ ├── CONTEXT.md
20
+ ├── docs/adr/
21
+ │ ├── 0001-event-sourced-orders.md
22
+ │ └── 0002-postgres-for-write-model.md
23
+ └── src/
24
+ ```
25
+
26
+ Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
27
+
28
+ ```
29
+ /
30
+ ├── CONTEXT-MAP.md
31
+ ├── docs/adr/ ← system-wide decisions
32
+ └── src/
33
+ ├── ordering/
34
+ │ ├── CONTEXT.md
35
+ │ └── docs/adr/ ← context-specific decisions
36
+ └── billing/
37
+ ├── CONTEXT.md
38
+ └── docs/adr/
39
+ ```
40
+
41
+ ## Use the glossary's vocabulary
42
+
43
+ When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
44
+
45
+ If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
46
+
47
+ ## Flag ADR conflicts
48
+
49
+ If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
50
+
51
+ > _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
@@ -0,0 +1,46 @@
1
+ # Issue tracker: GitLab
2
+
3
+ Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations.
4
+
5
+ ## Conventions
6
+
7
+ - **Create an issue**: `glab issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions. Pass `--description -` to open an editor.
8
+ - **Read an issue**: `glab issue view <number> --comments`. Use `-F json` for machine-readable output.
9
+ - **List issues**: `glab issue list -F json` with appropriate `--label` filters.
10
+ - **Comment on an issue**: `glab issue note <number> --message "..."`. GitLab calls comments "notes".
11
+ - **Apply / remove labels**: `glab issue update <number> --label "..."` / `--unlabel "..."`. Multiple labels can be comma-separated or by repeating the flag.
12
+ - **Close**: `glab issue close <number>`. `glab issue close` does not accept a closing comment, so post the explanation first with `glab issue note <number> --message "..."`, then close.
13
+ - **Merge requests**: GitLab calls PRs "merge requests". Use `glab mr create`, `glab mr view`, `glab mr note`, etc. — the same shape as `gh pr ...` with `mr` in place of `pr` and `note`/`--message` in place of `comment`/`--body`.
14
+
15
+ Infer the repo from `git remote -v` — `glab` does this automatically when run inside a clone.
16
+
17
+ ## Merge requests as a triage surface
18
+
19
+ **MRs as a request surface: no.** _(Set to `yes` if this repo treats external merge requests as feature requests; `/triage` reads this flag.)_
20
+
21
+ When set to `yes`, MRs run through the same labels and states as issues, using the `glab mr` equivalents:
22
+
23
+ - **Read an MR**: `glab mr view <number> --comments` and `glab mr diff <number>` for the diff.
24
+ - **List external MRs for triage**: `glab mr list -F json`, then keep only MRs whose author is not a project member/owner (a contributor's MR, not a maintainer's in-flight work).
25
+ - **Comment / label / close**: `glab mr note`, `glab mr update --label`/`--unlabel`, `glab mr close`.
26
+
27
+ Unlike GitHub, GitLab numbers issues and MRs separately, so `#42` is unambiguous once you know which surface the maintainer means.
28
+
29
+ ## When a skill says "publish to the issue tracker"
30
+
31
+ Create a GitLab issue.
32
+
33
+ ## When a skill says "fetch the relevant ticket"
34
+
35
+ Run `glab issue view <number> --comments`.
36
+
37
+ ## Wayfinding operations
38
+
39
+ Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
40
+
41
+ - **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `glab issue create --label wayfinder:map`. (On GitLab tiers with native epics, an epic may hold the map instead; a labelled issue works everywhere.)
42
+ - **Child ticket**: an issue carrying `Part of #<map>` at the top of its description and labels `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
43
+ - **Blocking**: GitLab's **native blocking link** — the canonical, UI-visible representation. Add it with the `/blocked_by #<n>` quick action, posted as a note (`glab issue note <child> --message "/blocked_by #<blocker>"`). Native blocking links are a Premium/Ultimate feature; on the free tier (or where unavailable) fall back to a `Blocked by: #<n>, #<n>` line at the top of the description. A ticket is unblocked when every blocker is closed.
44
+ - **Frontier query**: `glab issue list -F json` scoped to the map's children, drop any with an open blocker — a native `blocked_by` link to an open issue (`glab api projects/:id/issues/:iid/links`), or an open issue in the `Blocked by` line — or an assignee; first in map order wins.
45
+ - **Claim**: `glab issue update <n> --assignee @me` — the session's first write.
46
+ - **Resolve**: `glab issue note <n> --message "<answer>"`, then `glab issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
@@ -0,0 +1,15 @@
1
+ # Triage Labels
2
+
3
+ The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
4
+
5
+ | Label in mattpocock/skills | Label in our tracker | Meaning |
6
+ | -------------------------- | -------------------- | ---------------------------------------- |
7
+ | `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
8
+ | `needs-info` | `needs-info` | Waiting on reporter for more information |
9
+ | `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
10
+ | `ready-for-human` | `ready-for-human` | Requires human implementation |
11
+ | `wontfix` | `wontfix` | Will not be actioned |
12
+
13
+ When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
14
+
15
+ Edit the right-hand column to match whatever vocabulary you actually use.
@@ -0,0 +1,71 @@
1
+ # Spec: rename to devkit and restructure into core + umbrella CLI
2
+
3
+ Status: ready-for-agent
4
+
5
+ ## Problem Statement
6
+
7
+ The repo is called `dev-tools`, a name that is already taken on npm as an unscoped package, is too generic to find, and does not say what the project is for. The intended purpose is a series of tools that help set up the development environment of projects. Today the repo only holds two release tools (`changelog`, `release-tag`), has no single entry point, and shares code between them by one package importing from the other (git helpers live inside `changelog`). Adding more tools would make this worse.
8
+
9
+ ## Solution
10
+
11
+ Rename the project to **devkit** (repo and docs), keep the `@sr-labs` npm scope, and add an umbrella package `@sr-labs/devkit` that exposes one `devkit` command with subcommands (`devkit release`, `devkit changelog`). Extract the shared helpers into `@sr-labs/core` so every tool depends on it rather than on a sibling tool. `@sr-labs/changelog` and `@sr-labs/release-tag` stay published and usable on their own with unchanged bins and flags.
12
+
13
+ ## User Stories
14
+
15
+ 1. As a maintainer, I want the project named devkit, so that its name describes a toolkit for development environments.
16
+ 2. As a maintainer, I want all packages under the `@sr-labs` scope, so that publishing never clashes with unscoped npm names.
17
+ 3. As a user, I want to install one package `@sr-labs/devkit`, so that I get every tool with a single dependency.
18
+ 4. As a user, I want a `devkit` bin with subcommands, so that I have one discoverable entry point.
19
+ 5. As a user, I want `devkit release` to behave exactly like `release-tag`, so that I can switch without relearning flags.
20
+ 6. As a user, I want `devkit changelog` to behave exactly like `changelog`, so that I can switch without relearning flags.
21
+ 7. As a user, I want `devkit --help` to list available subcommands, so that I can discover tools.
22
+ 8. As a user, I want `devkit --version` to print the umbrella version, so that I can report what I run.
23
+ 9. As a user, I want an unknown subcommand to print an error and the help text and exit non-zero, so that typos are obvious in scripts.
24
+ 10. As an existing user of `@sr-labs/release-tag`, I want its bin and flags unchanged, so that my scripts keep working.
25
+ 11. As an existing user of `@sr-labs/changelog`, I want its bin and flags unchanged, so that my scripts keep working.
26
+ 12. As a library consumer, I want `@sr-labs/changelog` to keep exporting its commit parsing and notes API, so that my imports keep working.
27
+ 13. As a tool author, I want shared git and exec helpers in `@sr-labs/core`, so that new tools do not copy code or import a sibling tool.
28
+ 14. As a tool author, I want a new tool to be one new package plus one subcommand registration, so that extending devkit is cheap.
29
+ 15. As a maintainer, I want all packages to share one version and release in lockstep, so that the existing release flow still works.
30
+ 16. As a maintainer, I want `pnpm release:tag` in this repo to keep releasing every package, including the new ones, so that I dogfood the tools.
31
+ 17. As a maintainer, I want repository URLs, READMEs and CI to use the devkit name, so that no stale references remain.
32
+ 18. As a maintainer, I want the old `dev-tools` GitLab path to be documented as moved, so that existing clones and links are understood.
33
+ 19. As a contributor, I want `pnpm test` to build and test every package, so that one command verifies the monorepo.
34
+ 20. As a contributor, I want the README to explain the package layout and which package to install, so that I pick the right one.
35
+ 21. As a user on Node 20+, I want every package to keep `engines.node >=20`, so that support is unchanged.
36
+ 22. As a user, I want compiled JS in `dist/` for every package, so that installs work inside `node_modules`.
37
+
38
+ ## Implementation Decisions
39
+
40
+ - **Naming**: project name `devkit`. npm packages: `@sr-labs/devkit` (umbrella, bin `devkit`), `@sr-labs/core` (shared helpers, library only), `@sr-labs/changelog` and `@sr-labs/release-tag` (unchanged names and bins). The root workspace package is renamed to `devkit` and stays private.
41
+ - **core**: receives the git/exec helpers currently inside the changelog package (the `git` runner, version-tag listing, commit-range reading). No behavior change; pure move. Public API is exported from the package index. Add `core` only with what is actually shared today; no speculative utilities.
42
+ - **changelog**: depends on `@sr-labs/core` for git helpers. Keeps exporting commit parsing and release-notes APIs so release-tag's existing import keeps working. Re-exporting git helpers from changelog is not required.
43
+ - **release-tag**: depends on `core` for git helpers and on `changelog` for commit parsing and notes, as today.
44
+ - **devkit umbrella**: depends on `changelog` and `release-tag`. Its bin dispatches the first argument to a subcommand registry (`release`, `changelog`) and forwards remaining argv, stdio and exit code unchanged. The registry is a plain data table so adding a tool is one entry. Subcommand implementations are imported as modules from the tool packages (each tool exposes its CLI entry as a callable function in addition to its bin), not spawned as child processes.
45
+ - **Tool CLI entry points**: each of `changelog` and `release-tag` splits its bin into a thin executable wrapper plus an exported `main(argv)` that returns an exit code, so the umbrella can call it. Bin names and flags do not change.
46
+ - **Versioning**: all packages stay in lockstep; the new packages start at the current shared version. `workspace:^` ranges between packages as today. The release tool's workspace discovery already covers `packages/*`.
47
+ - **Repo rename**: update repository URLs in all `package.json` files, README, CI config and changelog links from `sr-labs/dev-tools` to the new `devkit` path. The actual GitLab project rename is a manual step by the owner (GitLab redirects the old path); the spec only covers in-repo references. Git history and existing tags are untouched, including old changelog entries.
48
+ - **Docs**: root README describes devkit's purpose (environment setup tooling), the package table with the new packages, and the install recommendation (`@sr-labs/devkit` for everything, individual packages for a single tool). Each new package gets a short README.
49
+ - **Build**: new packages get tsconfigs extending the base and are added to project references; `tsc -b` stays the build.
50
+
51
+ ## Testing Decisions
52
+
53
+ - A good test exercises external behavior only: run the built CLI, assert on stdout, exit code and resulting repo state. No assertions on internal module structure.
54
+ - **One new seam**: the built `devkit` bin, run end-to-end in a throwaway git repo. Cover: `devkit release` dry run and `--apply` match what `release-tag` does; `devkit changelog` regenerates the same output as `changelog`; unknown subcommand exits non-zero with help; `--help` lists subcommands.
55
+ - Existing seams stay: unit tests for notes, version and package-files, and the release-tag end-to-end test. They must keep passing unchanged except for import path updates where code moved to `core`.
56
+ - Tests that cover moved git helpers move with them into `core`; no new core-level seam is added.
57
+ - Prior art: the temp-repo, built-CLI pattern in the release-tag CLI test (`execFileSync` against `dist/cli.js` with fixed git author env).
58
+
59
+ ## Out of Scope
60
+
61
+ - Any new environment-setup tool (hooks, lint/editor config, CI templates). Those come later as their own packages and specs.
62
+ - Renaming the GitLab project itself, or changing remotes on contributors' machines.
63
+ - Changing release rules, changelog format or any flag of the existing tools.
64
+ - Deprecating or republishing old versions of existing packages.
65
+ - A plugin system for third-party subcommands.
66
+
67
+ ## Further Notes
68
+
69
+ - npm availability checked 2026-10-19: unscoped `dev-tools`, `devkit`, `devenv`, `devsetup` are taken; `@sr-labs/devkit` and `@sr-labs/setup` returned 404 (free). Re-check `@sr-labs/core` before first publish.
70
+ - First publish of the two new packages needs `publishConfig.access: public` like the existing ones.
71
+ - Order suggestion: extract `core`, then add `main(argv)` exports, then umbrella, then rename references and docs, so each step keeps `pnpm test` green.
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "@sr-labs/devkit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://gitlab.com/sr-labs/devkit.git"
8
+ },
9
+ "type": "module",
10
+ "packageManager": "pnpm@12.8.1",
11
+ "scripts": {
12
+ "build": "tsc -b",
13
+ "check": "tsc -b && tsc -p tsconfig.test.json",
14
+ "test": "tsc -b && vitest run",
15
+ "release:tag": "tsc -b && node packages/release-tag/dist/cli.js"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^24.13.3",
19
+ "typescript": "~6.0.2",
20
+ "vitest": "^5.0.4"
21
+ }
22
+ }
@@ -0,0 +1,7 @@
1
+ # @sr-labs/changelog
2
+
3
+ `changelog [--stdout] [--unreleased] [--file CHANGELOG.md]` regenerates a changelog from Conventional Commits
4
+ between `vX.Y.Z` tags, grouped into Breaking Changes, Features, Fixes and Performance.
5
+
6
+ Also exports the building blocks (`parseCommit`, `readCommits`, `listVersionTags`, `formatSection`,
7
+ `prependSection`, ...) used by `@sr-labs/release-tag`. See the [devkit README](../../README.md).
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@sr-labs/changelog",
3
+ "version": "0.1.0",
4
+ "description": "Generate CHANGELOG.md from Conventional Commits between vX.Y.Z tags.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://gitlab.com/sr-labs/devkit.git",
10
+ "directory": "packages/changelog"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "bin": {
22
+ "changelog": "./dist/cli.js"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "dependencies": {
31
+ "@sr-labs/core": "workspace:^"
32
+ }
33
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './main.ts';
3
+
4
+ process.exitCode = main(process.argv.slice(2));
@@ -0,0 +1,29 @@
1
+ import type { Commit } from '@sr-labs/core';
2
+
3
+ export type { Commit };
4
+
5
+ export interface ParsedCommit {
6
+ type: string;
7
+ scope?: string;
8
+ description: string;
9
+ breaking: boolean;
10
+ /** First line of a `BREAKING CHANGE:` footer, when present. */
11
+ breakingNote?: string;
12
+ }
13
+
14
+ const HEADER = /^(\w+)(?:\(([^)]*)\))?(!)?:\s*(.+)$/;
15
+ const BREAKING_FOOTER = /^BREAKING[ -]CHANGE:\s*(.*)$/m;
16
+
17
+ /** Parses a Conventional Commits header and breaking footer. Null for non-conventional subjects (e.g. merges). */
18
+ export function parseCommit(c: Commit): ParsedCommit | null {
19
+ const m = HEADER.exec(c.subject);
20
+ if (!m) return null;
21
+ const note = BREAKING_FOOTER.exec(c.body)?.[1].trim();
22
+ return {
23
+ type: m[1],
24
+ scope: m[2] || undefined,
25
+ description: m[4],
26
+ breaking: m[3] === '!' || note !== undefined,
27
+ breakingNote: note || undefined
28
+ };
29
+ }
@@ -0,0 +1,11 @@
1
+ import { listVersionTags, readCommits, type Tag } from '@sr-labs/core';
2
+ import type { Release } from './notes.ts';
3
+
4
+ /** Every tagged release with its commits since the previous tag, newest first. */
5
+ export function readReleases(tags: Tag[] = listVersionTags()): Release[] {
6
+ return tags.map((t, i) => ({
7
+ tag: t.name,
8
+ date: t.date,
9
+ commits: readCommits(tags[i + 1] ? `${tags[i + 1].name}..${t.name}` : t.name)
10
+ }));
11
+ }
@@ -0,0 +1,4 @@
1
+ export * from './commits.ts';
2
+ export * from './git.ts';
3
+ export { main } from './main.ts';
4
+ export * from './notes.ts';
@@ -0,0 +1,33 @@
1
+ // Usage: changelog [--stdout] [--unreleased] [--file CHANGELOG.md]
2
+ // Regenerates the changelog from every vX.Y.Z tag. --unreleased adds commits since the latest tag.
3
+ import { writeFileSync } from 'node:fs';
4
+ import { parseArgs } from 'node:util';
5
+ import { listVersionTags, readCommits } from '@sr-labs/core';
6
+ import { readReleases } from './git.ts';
7
+ import { formatChangelog, type Release } from './notes.ts';
8
+
9
+ /** Runs the changelog CLI with args (without node/script) and returns the exit code. */
10
+ export function main(argv: string[]): number {
11
+ const { values } = parseArgs({
12
+ args: argv,
13
+ options: {
14
+ stdout: { type: 'boolean', default: false },
15
+ unreleased: { type: 'boolean', default: false },
16
+ file: { type: 'string', default: 'CHANGELOG.md' }
17
+ }
18
+ });
19
+
20
+ const tags = listVersionTags();
21
+ const releases: Release[] = readReleases(tags);
22
+ if (values.unreleased) {
23
+ releases.unshift({ tag: 'Unreleased', date: '', commits: readCommits(tags[0] ? `${tags[0].name}..HEAD` : 'HEAD') });
24
+ }
25
+
26
+ const text = formatChangelog(releases);
27
+ if (values.stdout) process.stdout.write(text);
28
+ else {
29
+ writeFileSync(values.file, text);
30
+ console.log(`Wrote ${values.file} (${tags.length} releases)`);
31
+ }
32
+ return 0;
33
+ }
@@ -0,0 +1,60 @@
1
+ import { parseCommit, type Commit, type ParsedCommit } from './commits.ts';
2
+
3
+ export interface Section {
4
+ title: string;
5
+ items: string[];
6
+ }
7
+
8
+ export interface Release {
9
+ tag: string;
10
+ /** YYYY-MM-DD; empty for an unreleased section. */
11
+ date: string;
12
+ commits: Commit[];
13
+ }
14
+
15
+ const SECTIONS: Array<[string, string]> = [
16
+ ['feat', 'Features'],
17
+ ['fix', 'Fixes'],
18
+ ['perf', 'Performance']
19
+ ];
20
+
21
+ const item = (p: ParsedCommit, text = p.description) => `${p.scope ? `${p.scope}: ` : ''}${text}`;
22
+
23
+ /** Release-worthy commits grouped by section; breaking changes first and also listed under their type. */
24
+ export function groupCommits(commits: Commit[]): Section[] {
25
+ const parsed = commits.map(parseCommit).filter((p): p is ParsedCommit => p !== null);
26
+ const sections: Section[] = [];
27
+ const breaking = parsed.filter((p) => p.breaking).map((p) => item(p, p.breakingNote));
28
+ if (breaking.length) sections.push({ title: 'Breaking Changes', items: breaking });
29
+ for (const [type, title] of SECTIONS) {
30
+ const items = parsed.filter((p) => p.type === type).map((p) => item(p));
31
+ if (items.length) sections.push({ title, items });
32
+ }
33
+ return sections;
34
+ }
35
+
36
+ /** Annotated-tag message: title line plus a grouped plain-text changelog. */
37
+ export function formatTagNotes(tag: string, commits: Commit[]): string {
38
+ const lines = [tag, ''];
39
+ for (const s of groupCommits(commits)) lines.push(`${s.title}:`, ...s.items.map((i) => `- ${i}`), '');
40
+ return lines.join('\n').trimEnd() + '\n';
41
+ }
42
+
43
+ /** One `## vX.Y.Z - date` Markdown section. */
44
+ export function formatSection(r: Release): string {
45
+ const lines = [`## ${r.date ? `${r.tag} - ${r.date}` : r.tag}`, ''];
46
+ for (const s of groupCommits(r.commits)) lines.push(`### ${s.title}`, '', ...s.items.map((i) => `- ${i}`), '');
47
+ return lines.join('\n').trimEnd() + '\n';
48
+ }
49
+
50
+ /** Whole CHANGELOG.md; releases newest first. */
51
+ export function formatChangelog(releases: Release[]): string {
52
+ return ['# Changelog\n', ...releases.map(formatSection)].join('\n');
53
+ }
54
+
55
+ /** Inserts a section above the newest existing `## ` heading, keeping any hand-written preamble and edits. */
56
+ export function prependSection(changelog: string, section: string): string {
57
+ const i = changelog.search(/^## /m);
58
+ if (i === -1) return (changelog.trim() ? changelog.trimEnd() + '\n\n' : '# Changelog\n\n') + section;
59
+ return changelog.slice(0, i) + section + '\n' + changelog.slice(i);
60
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseCommit } from '../src/commits.ts';
3
+ import { formatChangelog, formatSection, formatTagNotes, prependSection } from '../src/notes.ts';
4
+
5
+ const c = (subject: string, body = '') => ({ subject, body });
6
+
7
+ describe('parseCommit', () => {
8
+ it('parses type, scope and breaking markers', () => {
9
+ expect(parseCommit(c('feat(ui)!: x'))).toEqual({
10
+ type: 'feat',
11
+ scope: 'ui',
12
+ description: 'x',
13
+ breaking: true,
14
+ breakingNote: undefined
15
+ });
16
+ expect(parseCommit(c('fix: y', 'details\n\nBREAKING CHANGE: drops z\nmore'))?.breakingNote).toBe('drops z');
17
+ expect(parseCommit(c('Merge ticket 04: reverb'))).toBeNull();
18
+ });
19
+ });
20
+
21
+ describe('notes', () => {
22
+ it('groups tag notes', () => {
23
+ const n = formatTagNotes('v0.3.0', [c('feat(engine): x'), c('fix: y'), c('docs: z')]);
24
+ expect(n).toBe('v0.3.0\n\nFeatures:\n- engine: x\n\nFixes:\n- y\n');
25
+ });
26
+
27
+ it('lists breaking changes first, using the footer text when present', () => {
28
+ const n = formatTagNotes('v1.0.0', [c('feat(api)!: new x'), c('fix: y', 'BREAKING CHANGE: y renamed')]);
29
+ expect(n).toBe('v1.0.0\n\nBreaking Changes:\n- api: new x\n- y renamed\n\nFeatures:\n- api: new x\n\nFixes:\n- y\n');
30
+ });
31
+
32
+ it('formats markdown sections and a whole changelog', () => {
33
+ const r = { tag: 'v0.2.0', date: '2026-10-01', commits: [c('perf: faster'), c('chore: tidy')] };
34
+ expect(formatSection(r)).toBe('## v0.2.0 - 2026-10-01\n\n### Performance\n\n- faster\n');
35
+ expect(formatSection({ tag: 'Unreleased', date: '', commits: [] })).toBe('## Unreleased\n');
36
+ expect(formatChangelog([r])).toBe('# Changelog\n\n## v0.2.0 - 2026-10-01\n\n### Performance\n\n- faster\n');
37
+ });
38
+
39
+ it('prepends above the newest release, keeping the preamble', () => {
40
+ const existing = '# Changelog\n\nHand-written intro.\n\n## v0.1.0 - 2026-01-01\n\n- old\n';
41
+ expect(prependSection(existing, '## v0.2.0 - 2026-02-01\n')).toBe(
42
+ '# Changelog\n\nHand-written intro.\n\n## v0.2.0 - 2026-02-01\n\n## v0.1.0 - 2026-01-01\n\n- old\n'
43
+ );
44
+ expect(prependSection('', '## v0.1.0\n')).toBe('# Changelog\n\n## v0.1.0\n');
45
+ expect(prependSection('# Changelog\n', '## v0.1.0\n')).toBe('# Changelog\n\n## v0.1.0\n');
46
+ });
47
+ });
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": { "rootDir": "src", "outDir": "dist" },
4
+ "include": ["src"],
5
+ "references": [{ "path": "../core" }]
6
+ }
@@ -0,0 +1,3 @@
1
+ # @sr-labs/core
2
+
3
+ Shared git helpers (`git`, `listVersionTags`, `readCommits`) used by the [devkit](../../README.md) tools. Library only, no bin.