@brainervirus/workit-core 0.8.11 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "0.8.11",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "description": "Workit — workflow rails for agentic coding: specs, plans, YouTrack, CI-gated commits (shared core)",
6
6
  "keywords": [
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bun
2
+ // AR-16: path-gated releases. Replaces message-only commit analysis: a
3
+ // releasable commit counts only when it touches a PRODUCT PATH (any of the
4
+ // four package dirs). Tooling-only merges produce no release at all.
5
+ import { execFileSync } from "node:child_process";
6
+ import { resolve } from "node:path";
7
+
8
+ export const RELEASE_PACKAGES = [
9
+ "workit-core",
10
+ "workit-opencode",
11
+ "workit-cursor",
12
+ "workit-cli",
13
+ ] as const;
14
+
15
+ const g = (root: string, args: string[]): string =>
16
+ execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim();
17
+
18
+ const SEMVER_TAG = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
19
+
20
+ export function latestTag(root = process.cwd()): string | null {
21
+ const out = g(root, ["tag", "--list", "v*", "--sort=-v:refname"])
22
+ .split("\n")
23
+ .map((l) => l.trim())
24
+ .filter((l) => SEMVER_TAG.test(l));
25
+ return out[0] ?? null;
26
+ }
27
+
28
+ type Level = "major" | "minor" | "patch";
29
+ const LEVEL_RANK: Record<Level, number> = { patch: 1, minor: 2, major: 3 };
30
+ const TYPE_LEVEL: Record<string, Level> = { fix: "patch", perf: "patch", feat: "minor" };
31
+
32
+ const subjectLevel = (commit: string): Level | null => {
33
+ const firstLine = commit.split("\n")[0] ?? "";
34
+ const m = /^(?:fix|perf|feat)(?:\([^)]*\))?!?:/.exec(firstLine);
35
+ if (!m) return null;
36
+ if (m[0].includes("!")) return "major";
37
+ const body = commit.split("\n").slice(1).join("\n");
38
+ return /BREAKING[- ]CHANGE:/.test(body)
39
+ ? "major"
40
+ : TYPE_LEVEL[m[0].split("(")[0].replace("!", "")];
41
+ };
42
+
43
+ // Two-pass collection (sanctioned by the task brief): the single-pass
44
+ // `%H<NUL>%s%n%b` + `--name-only` interleave is brittle because execFileSync
45
+ // rejects NUL bytes inside arguments. Bounded by commit count; acceptable for
46
+ // this repo's cadence.
47
+ //
48
+ // diff-tree with -m unions files across a merge's parents (a plain `show`
49
+ // combined diff drops files identical to either parent — e.g. hotfix-branch
50
+ // back-merges), and -z returns raw NUL-delimited paths so spaces/non-ASCII
51
+ // are never C-quoted. NUL is fine in captured output, never in argv.
52
+ const commitsSince = (root: string, from: string): { message: string; files: string[] }[] => {
53
+ const hashes = g(root, ["log", "--reverse", "--format=%H", `${from}..HEAD`])
54
+ .split("\n")
55
+ .filter(Boolean);
56
+ return hashes.map((h) => ({
57
+ message: g(root, ["show", "-s", "--format=%B", h]),
58
+ files: g(root, ["diff-tree", "--no-commit-id", "--name-only", "-r", "-m", "--root", "-z", h])
59
+ .split("\0")
60
+ .filter(Boolean),
61
+ }));
62
+ };
63
+
64
+ export function analyzeReleaseScope(
65
+ root = process.cwd(),
66
+ ): { level: Level | null; productPkgs: string[] } {
67
+ const from = latestTag(root);
68
+ if (from === null) {
69
+ return { level: "minor", productPkgs: [...RELEASE_PACKAGES] };
70
+ }
71
+ const commits = commitsSince(root, from);
72
+ const levels: Level[] = [];
73
+ const pkgs = new Set<string>();
74
+ for (const { message, files } of commits) {
75
+ const touched = files.filter((f) => RELEASE_PACKAGES.some((p) => f.startsWith(`packages/${p}/`)));
76
+ if (touched.length === 0) continue;
77
+ const lvl = subjectLevel(message);
78
+ if (lvl) levels.push(lvl);
79
+ for (const f of touched) {
80
+ const pkg = RELEASE_PACKAGES.find((p) => f.startsWith(`packages/${p}/`));
81
+ if (pkg) pkgs.add(pkg);
82
+ }
83
+ }
84
+ if (levels.length === 0) return { level: null, productPkgs: [...pkgs] };
85
+ const level = levels.reduce<Level>((best, l) => (LEVEL_RANK[l] > LEVEL_RANK[best] ? l : best), "patch");
86
+ return { level, productPkgs: [...pkgs] };
87
+ }
88
+
89
+ if (import.meta.main) {
90
+ const root = process.argv[2] ? resolve(process.argv[2]) : process.cwd();
91
+ const { level } = analyzeReleaseScope(root);
92
+ if (level) process.stdout.write(`${level}\n`);
93
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bun
2
+ // AR-16: selective publishing. Publishes only packages whose directory
3
+ // changed since the previous v* tag; logs an exact skip line per unchanged
4
+ // package so release logs answer "what shipped?" without leaving the terminal.
5
+ import { execFileSync } from "node:child_process";
6
+ import { resolve } from "node:path";
7
+ import { latestTag, RELEASE_PACKAGES } from "./analyze-release-scope";
8
+
9
+ const git = (root: string, args: string[]): string =>
10
+ execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim();
11
+
12
+ export function changedPackages(root: string, fromTag: string): string[] {
13
+ // Committed state only: <tag>..HEAD, never the working tree — unreviewed
14
+ // local edits must not decide what ships.
15
+ return RELEASE_PACKAGES.filter((pkg) => {
16
+ const out = git(root, ["diff", "--name-only", `${fromTag}..HEAD`, "--", `packages/${pkg}`]);
17
+ return out !== "";
18
+ });
19
+ }
20
+
21
+ export function publishChanged(opts: {
22
+ root: string;
23
+ dryRun?: boolean;
24
+ /**
25
+ * Base tag to diff against. Empty string means first-ever release (ship
26
+ * all). Default: latestTag(root). semantic-release creates the NEW release
27
+ * tag before publish plugins run, so production passes the PREVIOUS tag via
28
+ * `${lastRelease.gitTag}` — diffing against latestTag() there is always
29
+ * empty and would skip every package.
30
+ */
31
+ fromTag?: string;
32
+ run?: (cmd: string, args: string[], o: { cwd: string }) => unknown;
33
+ }): { published: string[]; skipped: string[]; tag: string | null } {
34
+ const { root, dryRun = false } = opts;
35
+ const run =
36
+ opts.run ??
37
+ ((cmd: string, args: string[], o: { cwd: string }) =>
38
+ execFileSync(cmd, args, { cwd: o.cwd, encoding: "utf8", stdio: "inherit" }));
39
+ const tag =
40
+ opts.fromTag !== undefined ? (opts.fromTag === "" ? null : opts.fromTag) : latestTag(root);
41
+ if (tag === null) {
42
+ // First-ever release: everything ships.
43
+ const published: string[] = [];
44
+ for (const pkg of RELEASE_PACKAGES) {
45
+ const cwd = resolve(root, "packages", pkg);
46
+ if (!dryRun) run("npm", ["publish", "--access", "public"], { cwd });
47
+ published.push(pkg);
48
+ console.log(`published ${pkg} @ ${cwd}`);
49
+ }
50
+ return { published, skipped: [], tag: null };
51
+ }
52
+ const changed = new Set(changedPackages(root, tag));
53
+ const published: string[] = [];
54
+ const skipped: string[] = [];
55
+ for (const pkg of RELEASE_PACKAGES) {
56
+ if (!changed.has(pkg)) {
57
+ skipped.push(pkg);
58
+ console.log(`skip ${pkg} (no payload change since ${tag})`);
59
+ continue;
60
+ }
61
+ const cwd = resolve(root, "packages", pkg);
62
+ try {
63
+ if (!dryRun) run("npm", ["publish", "--access", "public"], { cwd });
64
+ } catch (e) {
65
+ console.log(`publish failed ${pkg}: ${e instanceof Error ? e.message : String(e)}`);
66
+ throw e;
67
+ }
68
+ published.push(pkg);
69
+ console.log(`published ${pkg} @ ${cwd}`);
70
+ }
71
+ return { published, skipped, tag };
72
+ }
73
+
74
+ // @semantic-release/exec spawns this Cmd as a shell string whose ONLY optional
75
+ // positional arg is rendered from ${lastRelease.gitTag}: present when a
76
+ // previous release exists, absent on a first-ever release. The repo root is
77
+ // always the spawn cwd (release.config.cjs paths are repo-root-relative), so
78
+ // the CLI takes no root argument.
79
+ if (import.meta.main) {
80
+ publishChanged({
81
+ root: process.cwd(),
82
+ dryRun: process.env.PUBLISH_DRY_RUN === "1",
83
+ ...(process.argv[2] !== undefined ? { fromTag: process.argv[2] } : {}),
84
+ });
85
+ }