@brainervirus/workit-core 0.6.0 → 0.6.1

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 (40) hide show
  1. package/package.json +9 -9
  2. package/scripts/_shared/common.sh +18 -3
  3. package/scripts/install-opencode-plugin.sh +14 -9
  4. package/src/core/branch.ts +143 -48
  5. package/src/core/changelog.ts +17 -14
  6. package/src/core/config-guard.ts +9 -2
  7. package/src/core/config.ts +48 -15
  8. package/src/core/detector.ts +22 -11
  9. package/src/core/docs-repo.ts +49 -14
  10. package/src/core/docs-validate.ts +163 -37
  11. package/src/core/flow-state.ts +6 -2
  12. package/src/core/gitignore.ts +11 -2
  13. package/src/core/handoff-context.ts +18 -5
  14. package/src/core/hygiene.ts +27 -5
  15. package/src/core/init.ts +86 -21
  16. package/src/core/parse-sections.ts +2 -2
  17. package/src/core/plan-tasks.ts +13 -3
  18. package/src/core/ports/youtrack-api.ts +3 -1
  19. package/src/core/ports/youtrack-config.ts +1 -3
  20. package/src/core/pr-create.ts +47 -15
  21. package/src/core/present.ts +11 -2
  22. package/src/core/reminder.ts +1 -2
  23. package/src/core/repo-tool.ts +4 -1
  24. package/src/core/rules.ts +10 -7
  25. package/src/core/scripts.ts +7 -2
  26. package/src/core/sdd.ts +11 -3
  27. package/src/core/templates.ts +14 -4
  28. package/src/core/vcs-config.ts +93 -34
  29. package/src/core/verify-parse.ts +4 -2
  30. package/src/core/workspaces.ts +2 -2
  31. package/src/core/youtrack.ts +231 -56
  32. package/src/core.ts +18 -3
  33. package/src/tools/docs-repo.ts +12 -3
  34. package/src/tools/flow.ts +24 -13
  35. package/src/tools/handoff.ts +28 -23
  36. package/src/tools/present.ts +14 -10
  37. package/src/tools/repo.ts +220 -87
  38. package/src/tools/sdd.ts +93 -66
  39. package/src/tools/youtrack.ts +115 -52
  40. package/templates/superpowers-doc-contract.md +1 -1
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "private": false,
5
5
  "description": "Workit — workflow rails for agentic coding: specs, plans, YouTrack, CI-gated commits (shared core)",
6
- "license": "MIT",
7
6
  "keywords": [
8
- "opencode",
7
+ "agent",
9
8
  "cursor",
10
- "workflow",
11
9
  "mcp",
12
- "youTrack",
13
- "agent"
10
+ "opencode",
11
+ "workflow",
12
+ "youTrack"
14
13
  ],
15
14
  "bugs": {
16
15
  "url": "https://github.com/BrainerVirus/workit/issues"
17
16
  },
17
+ "license": "MIT",
18
18
  "repository": {
19
19
  "type": "git",
20
20
  "url": "https://github.com/BrainerVirus/workit.git"
@@ -37,12 +37,12 @@
37
37
  "./src/*": "./src/*.ts",
38
38
  "./package.json": "./package.json"
39
39
  },
40
- "scripts": {
41
- "typecheck": "tsc --noEmit"
42
- },
43
40
  "publishConfig": {
44
41
  "access": "public"
45
42
  },
43
+ "scripts": {
44
+ "typecheck": "tsc --noEmit"
45
+ },
46
46
  "dependencies": {
47
47
  "@modelcontextprotocol/sdk": "^1.12.0",
48
48
  "zod": "^3.24.0"
@@ -53,7 +53,6 @@ is_pr_branch() {
53
53
  esac
54
54
  }
55
55
 
56
- # ponytail: develop-only base — main is release-only in this workflow; never compare PRs to main
57
56
  resolve_pr_branch_context() {
58
57
  branch=$(current_branch)
59
58
 
@@ -74,7 +73,23 @@ resolve_pr_branch_context() {
74
73
  best_ref=""
75
74
  best_mb=""
76
75
 
77
- for ref in origin/develop develop; do
76
+ resolved=$(bash "$SCRIPT_DIR/vcs/config.sh" resolve 2>/dev/null) || {
77
+ printf 'ERROR: cannot resolve the configured PR target branch\n' >&2
78
+ return 1
79
+ }
80
+ base=$(printf '%s\n' "$resolved" | bun -e '
81
+ const value = JSON.parse(await Bun.stdin.text()).defaultTargetBranch;
82
+ if (typeof value === "string" && value) process.stdout.write(value);
83
+ ') || {
84
+ printf 'ERROR: invalid VCS target-branch configuration\n' >&2
85
+ return 1
86
+ }
87
+ if [ "$base" = "" ] || ! git check-ref-format --branch "$base" >/dev/null 2>&1; then
88
+ printf 'ERROR: invalid configured PR target branch %s\n' "$base" >&2
89
+ return 1
90
+ fi
91
+
92
+ for ref in "origin/$base" "$base"; do
78
93
  git rev-parse --verify "$ref" >/dev/null 2>&1 || continue
79
94
  mb=$(git merge-base "$ref" HEAD 2>/dev/null) || continue
80
95
  best_ref=$ref
@@ -83,7 +98,7 @@ resolve_pr_branch_context() {
83
98
  done
84
99
 
85
100
  if [ "$best_ref" = "" ] || [ "$best_mb" = "" ]; then
86
- printf 'ERROR: develop branch not found — PRs target develop (not main). Fetch/checkout develop or pass an explicit git range\n' >&2
101
+ printf 'ERROR: configured PR target branch %s not found — fetch/checkout it or pass an explicit git range\n' "$base" >&2
87
102
  return 1
88
103
  fi
89
104
 
@@ -3,14 +3,14 @@
3
3
  set -euo pipefail
4
4
 
5
5
  SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
6
- ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
6
+ ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)"
7
+ CORE_SCRIPTS="$ROOT/packages/workit-core/scripts"
7
8
  SHARE="${HOME}/.local/share/workflow-toolkit"
8
- DEV_DEFAULT="${HOME}/Documents/projects/personal/workflow-toolkit"
9
- DEV="${WORKFLOW_TOOLKIT_DEV:-$DEV_DEFAULT}"
9
+ DEV="${WORKFLOW_TOOLKIT_DEV:-$ROOT}"
10
10
  CONFIG="${HOME}/.config/opencode/opencode.json"
11
11
 
12
- chmod +x "$ROOT/scripts/sync-runtime.sh"
13
- WORKFLOW_TOOLKIT_DEV="$ROOT" "$ROOT/scripts/sync-runtime.sh"
12
+ chmod +x "$CORE_SCRIPTS/sync-runtime.sh"
13
+ WORKFLOW_TOOLKIT_DEV="$ROOT" "$CORE_SCRIPTS/sync-runtime.sh"
14
14
 
15
15
  # Prefer monorepo with .git. file:// pins skip opencode's bundled npm installer —
16
16
  # git+file:// installs an EMPTY cache dir and fails silently, so the plugin never
@@ -40,9 +40,14 @@ const pin = process.env.PIN_PATH!;
40
40
  const data = JSON.parse(fs.readFileSync(path, "utf8"));
41
41
  let plugins = data.plugin || [];
42
42
  if (typeof plugins === "string") plugins = [plugins];
43
- // Drop stale workflow-toolkit pins the new file:// pin is written fresh.
44
- data.plugin = plugins.filter((p) => !String(p).includes("workflow-toolkit"));
45
- data.plugin.push(pin);
43
+ // Load the dev pin first and remove every stale/current Workit identity.
44
+ const isWorkit = (p) => {
45
+ const value = String(p);
46
+ return value.includes("workflow-toolkit") ||
47
+ value.includes("@brainervirus/workit-opencode") ||
48
+ value.includes("/packages/workit-opencode/");
49
+ };
50
+ data.plugin = [pin, ...plugins.filter((p) => !isWorkit(p))];
46
51
  // Drop share skills.paths — native ~/.config/opencode/skills links avoid triple-load dups
47
52
  const skills = data.skills;
48
53
  if (skills && typeof skills === "object") {
@@ -73,4 +78,4 @@ if [ ! -s "$PLUGIN_ENTRY" ]; then
73
78
  exit 1
74
79
  fi
75
80
 
76
- echo "OpenCode install done. Fully quit all opencode processes, then restart."
81
+ echo "OpenCode install done. Fully quit all opencode processes, then restart."
@@ -3,22 +3,36 @@ import { execFileSync } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import { gitContext } from "./git";
5
5
  import { readConfig, resolveBranchPolicy } from "./config";
6
+ import { vcsConfig } from "./vcs-config";
6
7
 
7
8
  const policy = () => resolveBranchPolicy(readConfig());
8
9
  const allowedBranch = (name: string) => policy().allowed.some((r) => r.test(name));
9
10
  const isProtected = (name: string) => policy().protected.has(name.toLowerCase());
11
+ const baseBranch = (cwd: string) =>
12
+ String(vcsConfig("resolve", cwd).defaultTargetBranch ?? "develop");
10
13
  const DECLARE_RE = /^\s*\*+Branch:\*+\s*`?([^`\s|]+)`?\s*$/gim;
11
14
  const USE_CURRENT_RE = /^\s*\*+Branch:\*+\s*use-current\s*$/im;
12
15
  const readSafe = (p: string): string | null => {
13
- try { return readFileSync(p, "utf8"); } catch { return null; }
16
+ try {
17
+ return readFileSync(p, "utf8");
18
+ } catch {
19
+ return null;
20
+ }
14
21
  };
15
22
 
16
23
  const normalizeBranch = (name: string): string | null => {
17
24
  const n = name.trim().replace(/`/g, "").replace(/\.+$/, "");
18
25
  if (isProtected(n)) return null;
19
26
  if (!allowedBranch(n)) return null;
20
- const parts = n.toLowerCase().split("/").map((p) =>
21
- p.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-"));
27
+ const parts = n
28
+ .toLowerCase()
29
+ .split("/")
30
+ .map((p) =>
31
+ p
32
+ .replace(/[^\w.-]+/g, "-")
33
+ .replace(/^-+|-+$/g, "")
34
+ .replace(/-{2,}/g, "-"),
35
+ );
22
36
  if (parts.some((p) => !p)) return null;
23
37
  return parts.join("/");
24
38
  };
@@ -29,14 +43,21 @@ const deriveSlug = (planPath: string): string => {
29
43
  return dirName === "." || dirName === "/" || dirName === "" ? "" : dirName;
30
44
  };
31
45
 
32
- const deriveKind = (planPath: string, fallback: "feature" | "bugfix" = "feature"): "feature" | "bugfix" => {
46
+ const deriveKind = (
47
+ planPath: string,
48
+ fallback: "feature" | "bugfix" = "feature",
49
+ ): "feature" | "bugfix" => {
33
50
  const slug = deriveSlug(planPath);
34
51
  const text = readSafe(planPath) ?? "";
35
52
  let kind = fallback;
36
53
  if (/\bbugfix\b/i.test(slug) || /^fix-/i.test(slug)) {
37
54
  kind = "bugfix";
38
55
  } else {
39
- const goal = text.split("\n").find((line) => line.startsWith("**Goal:**"))?.toLowerCase() ?? "";
56
+ const goal =
57
+ text
58
+ .split("\n")
59
+ .find((line) => line.startsWith("**Goal:**"))
60
+ ?.toLowerCase() ?? "";
40
61
  if (/\b(bugfix|bug fix)\b/.test(goal) && !/\b(feat|feature|upgrade|add)\b/.test(goal)) {
41
62
  kind = "bugfix";
42
63
  }
@@ -49,7 +70,11 @@ export const resolveBranch = ({
49
70
  spec_path,
50
71
  plan_path,
51
72
  workspace_root,
52
- }: { spec_path: string; plan_path: string; workspace_root: string }) => {
73
+ }: {
74
+ spec_path: string;
75
+ plan_path: string;
76
+ workspace_root: string;
77
+ }) => {
53
78
  const cwd = path.resolve(workspace_root);
54
79
  const abs = (p: string) => (path.isAbsolute(p) ? p : path.join(cwd, p));
55
80
  const spec = abs(spec_path);
@@ -76,7 +101,8 @@ export const resolveBranch = ({
76
101
  }
77
102
  }
78
103
 
79
- if (current && allowedBranch(current) && !isProtected(current)) return finish(current, "keep-current");
104
+ if (current && allowedBranch(current) && !isProtected(current))
105
+ return finish(current, "keep-current");
80
106
 
81
107
  let declaredButInvalid: string | null = null;
82
108
  for (const file of [spec, plan]) {
@@ -89,7 +115,9 @@ export const resolveBranch = ({
89
115
  }
90
116
  }
91
117
  if (declaredButInvalid) {
92
- return { error: `declared branch ${JSON.stringify(declaredButInvalid)} is not allowed by the branch policy` };
118
+ return {
119
+ error: `declared branch ${JSON.stringify(declaredButInvalid)} is not allowed by the branch policy`,
120
+ };
93
121
  }
94
122
 
95
123
  const slug = deriveSlug(plan);
@@ -103,55 +131,101 @@ export const docsBranch = ({
103
131
  plan_path,
104
132
  kind,
105
133
  workspace_root,
106
- }: { plan_path?: string; kind?: string; workspace_root: string }) => {
134
+ }: {
135
+ plan_path?: string;
136
+ kind?: string;
137
+ workspace_root: string;
138
+ }) => {
107
139
  const cwd = path.resolve(workspace_root);
108
140
  const git = gitContext(cwd);
109
141
  const current = git.branch;
110
142
  const kindArg = (kind ?? "feature").toLowerCase();
143
+ const base = baseBranch(cwd);
111
144
 
112
- if (current && allowedBranch(current) && !isProtected(current)) {
113
- return { branch: current, action: "keep", current_branch: current, base: "develop", dirty: Boolean(git.status_short.trim()) };
114
- }
115
- if (current === "main" || current === "master" || current === "develop") {
145
+ if (current === base || current === "main" || current === "master" || current === "develop") {
116
146
  let slug = "";
117
147
  if (plan_path) {
118
148
  const plan = path.isAbsolute(plan_path) ? plan_path : path.join(cwd, plan_path);
119
149
  slug = deriveSlug(plan);
120
150
  }
121
151
  if (!slug) {
122
- return { error: "plan_path required to derive branch slug when not on feature/* or bugfix/*" };
152
+ return {
153
+ error: "plan_path required to derive branch slug when not on feature/* or bugfix/*",
154
+ };
123
155
  }
124
156
  const branchKind = kindArg === "bugfix" ? "bugfix" : "feature";
125
- return { branch: `${branchKind}/${slug}`, action: "create_from_develop", current_branch: current, base: "develop", dirty: Boolean(git.status_short.trim()) };
157
+ return {
158
+ branch: `${branchKind}/${slug}`,
159
+ action: base === "develop" ? "create_from_develop" : "create_from_base",
160
+ current_branch: current,
161
+ base,
162
+ dirty: Boolean(git.status_short.trim()),
163
+ };
164
+ }
165
+ if (current && allowedBranch(current) && !isProtected(current)) {
166
+ return {
167
+ branch: current,
168
+ action: "keep",
169
+ current_branch: current,
170
+ base,
171
+ dirty: Boolean(git.status_short.trim()),
172
+ };
126
173
  }
127
174
  return { error: `cannot resolve docs branch from HEAD ${JSON.stringify(current)}` };
128
175
  };
129
176
 
130
177
  // Port of scripts/lib/ensure-develop-base.sh
131
- export const ensureDevelopBase = (cwd: string): { ok: boolean; error?: string } => {
178
+ export const ensureBaseBranch = (cwd: string, base: string): { ok: boolean; error?: string } => {
132
179
  const git = gitContext(cwd);
133
- if (!git.branch || git.branch === "unknown") return { ok: false, error: "not in a git repository" };
134
- const run = (args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
180
+ if (!git.branch || git.branch === "unknown")
181
+ return { ok: false, error: "not in a git repository" };
182
+ const run = (args: string[]) =>
183
+ execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
135
184
  try {
136
185
  try {
137
- run(["fetch", "origin", "develop", "--prune"]);
186
+ run(["fetch", "origin", base, "--prune"]);
138
187
  } catch {
139
188
  run(["fetch", "origin", "--prune"]);
140
189
  }
141
- let hasOriginDevelop = true;
142
- try { execFileSync("git", ["show-ref", "--verify", "--quiet", "refs/remotes/origin/develop"], { cwd, stdio: "pipe" }); } catch { hasOriginDevelop = false; }
143
- if (!hasOriginDevelop) return { ok: false, error: "origin/develop missing — push develop before creating feature/* or bugfix/* branches" };
144
- let hasLocalDevelop = true;
145
- try { execFileSync("git", ["show-ref", "--verify", "--quiet", "refs/heads/develop"], { cwd, stdio: "pipe" }); } catch { hasLocalDevelop = false; }
146
- if (hasLocalDevelop) {
147
- run(["checkout", "develop"]);
148
- try { run(["merge", "--ff-only", "origin/develop"]); } catch { /* non-fast-forward: keep local */ }
190
+ let hasOriginBase = true;
191
+ try {
192
+ execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${base}`], {
193
+ cwd,
194
+ stdio: "pipe",
195
+ });
196
+ } catch {
197
+ hasOriginBase = false;
198
+ }
199
+ if (!hasOriginBase)
200
+ return {
201
+ ok: false,
202
+ error: `origin/${base} missing — push ${base} before creating feature/* or bugfix/* branches`,
203
+ };
204
+ let hasLocalBase = true;
205
+ try {
206
+ execFileSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${base}`], {
207
+ cwd,
208
+ stdio: "pipe",
209
+ });
210
+ } catch {
211
+ hasLocalBase = false;
212
+ }
213
+ if (hasLocalBase) {
214
+ run(["checkout", base]);
215
+ try {
216
+ run(["merge", "--ff-only", `origin/${base}`]);
217
+ } catch {
218
+ /* non-fast-forward: keep local */
219
+ }
149
220
  } else {
150
- run(["checkout", "-b", "develop", "--track", "origin/develop"]);
221
+ run(["checkout", "-b", base, "--track", `origin/${base}`]);
151
222
  }
152
223
  return { ok: true };
153
224
  } catch (error) {
154
- return { ok: false, error: error instanceof Error ? error.message : "ensure-develop-base failed" };
225
+ return {
226
+ ok: false,
227
+ error: error instanceof Error ? error.message : "ensure-base-branch failed",
228
+ };
155
229
  }
156
230
  };
157
231
 
@@ -162,17 +236,29 @@ export const branchSetup = ({
162
236
  target_branch,
163
237
  stash,
164
238
  workspace_root,
165
- }: { action?: string; sdd_dir?: string; target_branch?: string; stash?: string; workspace_root: string }) => {
239
+ }: {
240
+ action?: string;
241
+ sdd_dir?: string;
242
+ target_branch?: string;
243
+ stash?: string;
244
+ workspace_root: string;
245
+ }) => {
166
246
  const cwd = path.resolve(workspace_root);
167
247
  const exec = (args: string[]): string =>
168
248
  execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
169
249
  const current = gitContext(cwd).branch;
170
250
  if (!current || current === "unknown") return { error: "not in a git repository" };
171
251
  const sdd = sdd_dir ?? "docs";
172
- const manifestPath = path.isAbsolute(sdd) ? path.join(sdd, "manifest.json") : path.join(cwd, sdd, "manifest.json");
252
+ const manifestPath = path.isAbsolute(sdd)
253
+ ? path.join(sdd, "manifest.json")
254
+ : path.join(cwd, sdd, "manifest.json");
173
255
  mkdirSync(path.dirname(manifestPath), { recursive: true, mode: 0o755 });
174
256
  const readManifest = (): Record<string, unknown> => {
175
- try { return JSON.parse(readFileSync(manifestPath, "utf8")); } catch { return {}; }
257
+ try {
258
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
259
+ } catch {
260
+ return {};
261
+ }
176
262
  };
177
263
  const writeManifest = (data: Record<string, unknown>) =>
178
264
  writeFileSync(manifestPath, JSON.stringify(data, null, 2) + "\n", "utf8");
@@ -181,7 +267,9 @@ export const branchSetup = ({
181
267
  const manifest = readManifest();
182
268
  const ref = manifest.stash_ref;
183
269
  if (!ref) return { error: "no stash_ref in manifest" };
184
- try { exec(["stash", "pop", String(ref)]); } catch (error) {
270
+ try {
271
+ exec(["stash", "pop", String(ref)]);
272
+ } catch (error) {
185
273
  return { error: error instanceof Error ? error.message : "stash pop failed" };
186
274
  }
187
275
  delete manifest.stash_ref;
@@ -193,14 +281,18 @@ export const branchSetup = ({
193
281
  const target = target_branch ?? "";
194
282
  if (!target) return { error: "target branch required" };
195
283
  if (isProtected(target)) return { error: `protected branch ${target}` };
196
- if (!allowedBranch(target)) return { error: `target branch ${target} is not allowed by the branch policy` };
284
+ if (!allowedBranch(target))
285
+ return { error: `target branch ${target} is not allowed by the branch policy` };
197
286
 
198
287
  let stash_ref: string | undefined;
199
288
  if (current !== target) {
200
289
  const dirty = Boolean(gitContext(cwd).status_short.trim());
201
290
  if (dirty) {
202
291
  if (stash !== "yes") {
203
- return { error: "dirty working tree — ask with native question, then call workflow_branch_setup with stash=yes" };
292
+ return {
293
+ error:
294
+ "dirty working tree — ask with native question, then call workflow_branch_setup with stash=yes",
295
+ };
204
296
  }
205
297
  try {
206
298
  exec(["stash", "push", "-u", "-m", `workit: pre-checkout ${target}`, "--", ":!docs/*/sdd"]);
@@ -214,22 +306,18 @@ export const branchSetup = ({
214
306
  } catch (error) {
215
307
  const message = error instanceof Error ? error.message : "checkout failed";
216
308
  if (/worktree/i.test(message)) {
217
- return { error: `branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)` };
309
+ return {
310
+ error: `branch ${target} is locked by an existing git worktree — remove it first (we do not use worktrees)`,
311
+ };
218
312
  }
219
313
  try {
220
- // Branch does not exist yet: base it on develop (never main/master).
221
- const current = gitContext(cwd).branch;
222
- if (current === "main" || current === "master") {
223
- const baseResult = ensureDevelopBase(cwd);
224
- if (!baseResult.ok) return { error: baseResult.error };
225
- } else {
226
- // Already on develop or another feature branch: still require origin/develop to exist.
227
- const baseResult = ensureDevelopBase(cwd);
228
- if (!baseResult.ok) return { error: baseResult.error };
229
- }
314
+ const baseResult = ensureBaseBranch(cwd, baseBranch(cwd));
315
+ if (!baseResult.ok) return { error: baseResult.error };
230
316
  exec(["checkout", "-b", target]);
231
317
  } catch (createError) {
232
- return { error: createError instanceof Error ? createError.message : "branch create failed" };
318
+ return {
319
+ error: createError instanceof Error ? createError.message : "branch create failed",
320
+ };
233
321
  }
234
322
  }
235
323
  }
@@ -242,5 +330,12 @@ export const branchSetup = ({
242
330
  manifest.stash_created_at = new Date().toISOString();
243
331
  }
244
332
  writeManifest(manifest);
245
- return { action: "setup", ok: true, branch: target, previous_branch: current, stash_ref: stash_ref ?? null, manifest: manifestPath };
333
+ return {
334
+ action: "setup",
335
+ ok: true,
336
+ branch: target,
337
+ previous_branch: current,
338
+ stash_ref: stash_ref ?? null,
339
+ manifest: manifestPath,
340
+ };
246
341
  };
@@ -2,14 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { resolveWorkspaceRoot } from "./scripts";
4
4
 
5
- const CATEGORIES = [
6
- "Added",
7
- "Changed",
8
- "Deprecated",
9
- "Removed",
10
- "Fixed",
11
- "Security",
12
- ];
5
+ const CATEGORIES = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"];
13
6
 
14
7
  // Port of scripts/changelog/apply-unreleased.py — merge Keep a Changelog
15
8
  // entries into ## [Unreleased] without duplicating ### headings.
@@ -73,10 +66,21 @@ function splitUnreleased(text: string): [string, string, string] {
73
66
  function canonicalCategory(heading: string): string | null {
74
67
  const match = CAT_RE.exec(heading.replace(/\r?\n$/, ""));
75
68
  if (!match) return null;
76
- return CATEGORIES.find((c) => c.toLowerCase() === match[0].replace(/^###\s+/, "").trim().toLowerCase()) ?? null;
69
+ return (
70
+ CATEGORIES.find(
71
+ (c) =>
72
+ c.toLowerCase() ===
73
+ match[0]
74
+ .replace(/^###\s+/, "")
75
+ .trim()
76
+ .toLowerCase(),
77
+ ) ?? null
78
+ );
77
79
  }
78
80
 
79
- function splitSections(body: string): [string[], Array<{ heading: string; category: string | null; body: string[] }>] {
81
+ function splitSections(
82
+ body: string,
83
+ ): [string[], Array<{ heading: string; category: string | null; body: string[] }>] {
80
84
  const preamble: string[] = [];
81
85
  const sections: Array<{ heading: string; category: string | null; body: string[] }> = [];
82
86
  let current: { heading: string; category: string | null; body: string[] } | null = null;
@@ -207,7 +211,8 @@ function applyChangelog(
207
211
  break;
208
212
  }
209
213
  }
210
- text = lines.slice(0, insertAt).join("") + "## [Unreleased]\n\n" + lines.slice(insertAt).join("");
214
+ text =
215
+ lines.slice(0, insertAt).join("") + "## [Unreleased]\n\n" + lines.slice(insertAt).join("");
211
216
  }
212
217
 
213
218
  const [before, body, after] = splitUnreleased(text);
@@ -290,9 +295,7 @@ export function changelogApply({
290
295
 
291
296
  export function changelogUnreleasedStats(workspace_root: string, changelogPath = "CHANGELOG.md") {
292
297
  const cwd = resolveWorkspaceRoot(workspace_root);
293
- const abs = path.isAbsolute(changelogPath)
294
- ? changelogPath
295
- : path.join(cwd, changelogPath);
298
+ const abs = path.isAbsolute(changelogPath) ? changelogPath : path.join(cwd, changelogPath);
296
299
  if (!fs.existsSync(abs)) return { exists: false };
297
300
  const text = fs.readFileSync(abs, "utf8");
298
301
  const m = text.match(/##\s+\[Unreleased\]([\s\S]*?)(?=\n##\s+\[|$)/i);
@@ -1,13 +1,20 @@
1
1
  import { initStatus } from "./init";
2
2
 
3
- export const ALL_ITEM_IDS = ["youtrack_json", "youtrack_token", "vcs_json", "gitlab_token", "github_token"];
3
+ export const ALL_ITEM_IDS = [
4
+ "youtrack_json",
5
+ "youtrack_token",
6
+ "vcs_json",
7
+ "gitlab_token",
8
+ "github_token",
9
+ ];
4
10
  export const CONFIG_GAP_MARKER = "workflow config missing";
5
11
 
6
12
  export function describeConfigGaps(scope?: string[]): { missing: string[]; ok: boolean } {
7
13
  const all = scope ?? ALL_ITEM_IDS;
8
14
  try {
9
15
  const status: unknown = initStatus();
10
- if (!status || typeof status !== "object" || (status as Record<string, unknown>).error) return { missing: all, ok: false };
16
+ if (!status || typeof status !== "object" || (status as Record<string, unknown>).error)
17
+ return { missing: all, ok: false };
11
18
  const items = (status as Record<string, unknown>).items;
12
19
  if (!Array.isArray(items) || items.length === 0) return { missing: all, ok: false };
13
20
  const known = all;
@@ -1,4 +1,12 @@
1
- import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
1
+ import {
2
+ copyFileSync,
3
+ cpSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ writeFileSync,
9
+ } from "node:fs";
2
10
  import os from "node:os";
3
11
  import path from "node:path";
4
12
 
@@ -12,16 +20,19 @@ export type ToolkitConfig = {
12
20
  };
13
21
 
14
22
  export const PRESETS: Record<BranchPreset, { allowed: string[]; protected: string[] }> = {
15
- gitflow: { allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"], protected: ["main", "develop", "master", "prod", "production"] },
23
+ gitflow: {
24
+ allowed: ["feature/*", "bugfix/*", "hotfix/*", "release/*"],
25
+ protected: ["main", "develop", "master", "prod", "production"],
26
+ },
16
27
  "github-flow": { allowed: ["*"], protected: ["main"] },
17
28
  "trunk-based": { allowed: ["*"], protected: ["main"] },
18
29
  custom: { allowed: [], protected: [] },
19
30
  };
20
31
 
21
32
  export const resolveConfigDir = (): string =>
22
- process.env.WORKFLOW_TOOLKIT_CONFIG
23
- ?? process.env.WORKFLOW_TOOLKIT_CONFIG_DIR
24
- ?? path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "workit");
33
+ process.env.WORKFLOW_TOOLKIT_CONFIG ??
34
+ process.env.WORKFLOW_TOOLKIT_CONFIG_DIR ??
35
+ path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "workit");
25
36
 
26
37
  // One-time lazy migration from the legacy ~/.config/workflow-toolkit dir.
27
38
  // migratedDir remembers the resolved dir already checked: configDir() is on
@@ -41,7 +52,10 @@ export const ensureConfigDir = (dir: string = resolveConfigDir()): string => {
41
52
  migratedDir = dir;
42
53
  return dir;
43
54
  }
44
- const legacy = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "workflow-toolkit");
55
+ const legacy = path.join(
56
+ process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"),
57
+ "workflow-toolkit",
58
+ );
45
59
  if (!existsSync(legacy)) {
46
60
  migratedDir = dir;
47
61
  return dir;
@@ -76,11 +90,19 @@ const DEFAULTS: ToolkitConfig = {
76
90
  locale: "en",
77
91
  localeOptions: ["en", "es-CL", "es-MX", "es-AR", "pt-BR"],
78
92
  timezone: "America/Santiago",
79
- branchPolicy: { preset: "gitflow", allowed: [...PRESETS.gitflow.allowed], protected: [...PRESETS.gitflow.protected] },
93
+ branchPolicy: {
94
+ preset: "gitflow",
95
+ allowed: [...PRESETS.gitflow.allowed],
96
+ protected: [...PRESETS.gitflow.protected],
97
+ },
80
98
  };
81
99
 
82
100
  const readSafe = (p: string): string | null => {
83
- try { return readFileSync(p, "utf8"); } catch { return null; }
101
+ try {
102
+ return readFileSync(p, "utf8");
103
+ } catch {
104
+ return null;
105
+ }
84
106
  };
85
107
 
86
108
  export const readConfig = (): ToolkitConfig => {
@@ -88,18 +110,26 @@ export const readConfig = (): ToolkitConfig => {
88
110
  if (!raw) return DEFAULTS;
89
111
  try {
90
112
  const parsed = JSON.parse(raw) as Partial<ToolkitConfig>;
91
- const locale = LOCALE_RE.test(String(parsed.locale ?? "")) ? parsed.locale as string : DEFAULTS.locale;
113
+ const locale = LOCALE_RE.test(String(parsed.locale ?? ""))
114
+ ? (parsed.locale as string)
115
+ : DEFAULTS.locale;
92
116
  const preset = (parsed.branchPolicy?.preset ?? "gitflow") as BranchPreset;
93
117
  const presetOk = Object.hasOwn(PRESETS, preset) ? preset : "gitflow";
94
118
  const presetDefs = PRESETS[presetOk];
95
119
  return {
96
120
  locale,
97
- localeOptions: Array.isArray(parsed.localeOptions) ? parsed.localeOptions : DEFAULTS.localeOptions,
121
+ localeOptions: Array.isArray(parsed.localeOptions)
122
+ ? parsed.localeOptions
123
+ : DEFAULTS.localeOptions,
98
124
  timezone: parsed.timezone ?? DEFAULTS.timezone,
99
125
  branchPolicy: {
100
126
  preset: presetOk,
101
- allowed: Array.isArray(parsed.branchPolicy?.allowed) ? parsed.branchPolicy.allowed : presetDefs.allowed,
102
- protected: Array.isArray(parsed.branchPolicy?.protected) ? parsed.branchPolicy.protected : presetDefs.protected,
127
+ allowed: Array.isArray(parsed.branchPolicy?.allowed)
128
+ ? parsed.branchPolicy.allowed
129
+ : presetDefs.allowed,
130
+ protected: Array.isArray(parsed.branchPolicy?.protected)
131
+ ? parsed.branchPolicy.protected
132
+ : presetDefs.protected,
103
133
  },
104
134
  };
105
135
  } catch {
@@ -113,8 +143,11 @@ export const writeConfig = (config: ToolkitConfig): void => {
113
143
  writeFileSync(path.join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
114
144
  };
115
145
 
116
- export const resolveBranchPolicy = (config: ToolkitConfig): { allowed: RegExp[]; protected: Set<string> } => {
117
- const allowed = config.branchPolicy.allowed.map((p) =>
118
- new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"));
146
+ export const resolveBranchPolicy = (
147
+ config: ToolkitConfig,
148
+ ): { allowed: RegExp[]; protected: Set<string> } => {
149
+ const allowed = config.branchPolicy.allowed.map(
150
+ (p) => new RegExp(`^${p.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i"),
151
+ );
119
152
  return { allowed, protected: new Set(config.branchPolicy.protected.map((p) => p.toLowerCase())) };
120
153
  };