@brainervirus/workit-opencode 2.0.2 → 2.1.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 (2) hide show
  1. package/dist/plugin.js +1491 -2144
  2. package/package.json +2 -2
package/dist/plugin.js CHANGED
@@ -14,8 +14,8 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // packages/workit-opencode/src/v1/server.ts
17
- import { existsSync as existsSync13 } from "node:fs";
18
- import path26 from "node:path";
17
+ import { existsSync as existsSync12 } from "node:fs";
18
+ import path25 from "node:path";
19
19
 
20
20
  // packages/workit-core/src/core.ts
21
21
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -10514,13 +10514,6 @@ var workspaceConfigSchema = object({
10514
10514
  profiles: record(nonBlank, workspaceProfileSchema).optional(),
10515
10515
  releaseTracks: record(nonBlank, releaseTrackSchema).optional()
10516
10516
  }).passthrough().superRefine((workspace, context) => {
10517
- if (workspace.youtrack && workspace.vcs?.provider !== "gitlab") {
10518
- context.addIssue({
10519
- code: "custom",
10520
- path: ["youtrack"],
10521
- message: `YouTrack issue linking requires the gitlab provider, got ${workspace.vcs?.provider ?? "unset"}`
10522
- });
10523
- }
10524
10517
  if (workspace.issues && workspace.vcs?.provider !== "github") {
10525
10518
  context.addIssue({
10526
10519
  code: "custom",
@@ -10712,13 +10705,10 @@ var selectWorkspaceMatch = (matches, cwd, workspaceName) => {
10712
10705
  }
10713
10706
  return mostSpecific[0];
10714
10707
  };
10715
- var resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
10716
- const result = readWorkspacesResult(dir);
10717
- if (result.status === "malformed" || result.status === "invalid")
10718
- throw new Error(result.error);
10708
+ var resolveWorkspaceFromEntries = (cwd, entries, workspaceName) => {
10719
10709
  const targets = [cwd, realpathOf(cwd)].map((p) => p.replaceAll("\\", "/"));
10720
10710
  const matches = [];
10721
- for (const ws of result.entries) {
10711
+ for (const ws of entries) {
10722
10712
  const glob = ws.glob.replaceAll("\\", "/");
10723
10713
  const canonical = canonicalGlob(glob);
10724
10714
  let matched = false;
@@ -10733,6 +10723,12 @@ var resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
10733
10723
  }
10734
10724
  return selectWorkspaceMatch(matches, cwd, workspaceName) ?? null;
10735
10725
  };
10726
+ var resolveWorkspaceFrom = (cwd, dir, workspaceName) => {
10727
+ const result = readWorkspacesResult(dir);
10728
+ if (result.status === "malformed" || result.status === "invalid")
10729
+ throw new Error(result.error);
10730
+ return resolveWorkspaceFromEntries(cwd, result.entries, workspaceName);
10731
+ };
10736
10732
  var runtimeWorkspacePolicySchema = (kind) => {
10737
10733
  const key = kind === "branch" ? "branchPolicy" : kind === "commit" ? "commitPolicy" : "vcs";
10738
10734
  const policySchema = kind === "branch" ? workspaceBranchPolicySchema : kind === "commit" ? workspaceCommitPolicySchema : object({
@@ -11011,8 +11007,8 @@ function vcsConfig(mode, cwd) {
11011
11007
  const hasWorkspacePolicy = typeof wp.preset === "string" && Object.hasOwn(PRESETS, wp.preset);
11012
11008
  const policyDefault = resolveBranchPolicyFor(root).defaultTargetBranch;
11013
11009
  const defaultTarget = String(wsVcs.defaultTargetBranch ?? (hasWorkspacePolicy ? policyDefault : cfg.defaultTargetBranch ?? policyDefault) ?? "develop");
11014
- const linkIssues = provider === "gitlab" && typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
11015
- const youtrackBaseUrl = provider === "gitlab" && typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
11010
+ const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
11011
+ const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
11016
11012
  let issuesProvider = null;
11017
11013
  let linkOnPr = null;
11018
11014
  if (provider === "github" && typeof wsIssues.provider === "string" && wsIssues.provider.toLowerCase() === "github") {
@@ -11107,9 +11103,6 @@ function vcsCliIdentity(cwd) {
11107
11103
  return { ok: false, provider, error: `invalid JSON from ${bin} api user` };
11108
11104
  }
11109
11105
  }
11110
- async function vcsVerifyToken() {
11111
- return vcsCliIdentity();
11112
- }
11113
11106
  function mergedPrStyle(limit = 6, cwd) {
11114
11107
  const cfg = vcsConfig("load", cwd);
11115
11108
  if (!cfg.ok)
@@ -27840,2257 +27833,1611 @@ function tool(input) {
27840
27833
  return input;
27841
27834
  }
27842
27835
  tool.schema = exports_external2;
27843
- // packages/workit-core/src/core/parse-sections.ts
27844
- function parseSections(stdout) {
27845
- const sections = {};
27846
- const parts = stdout.split(/\n## /);
27847
- for (const part of parts.slice(1)) {
27848
- const nl = part.indexOf(`
27849
- `);
27850
- const title = part.slice(0, nl).trim();
27851
- sections[title] = part.slice(nl + 1).trim();
27836
+ // packages/workit-opencode/src/shared/init-apply.ts
27837
+ import path20 from "node:path";
27838
+
27839
+ // packages/workit-core/src/core/gitignore.ts
27840
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
27841
+ import path15 from "node:path";
27842
+ var GITIGNORE_ENTRIES = [
27843
+ "# workit: SDD working state (never commit)",
27844
+ "docs/*/sdd/",
27845
+ "",
27846
+ "# OS / editor cruft",
27847
+ ".DS_Store",
27848
+ "Thumbs.db",
27849
+ "*.swp",
27850
+ ".idea/",
27851
+ ".vscode/",
27852
+ ".env",
27853
+ "node_modules/",
27854
+ "dist/",
27855
+ "*.log",
27856
+ ".cache/"
27857
+ ];
27858
+ var ensureProjectGitignore = (workspaceRoot, confirmed) => {
27859
+ if (!confirmed)
27860
+ return { ok: false, error: "confirmed: true required" };
27861
+ const file = path15.join(workspaceRoot, ".gitignore");
27862
+ const existing = existsSync8(file) ? readFileSync7(file, "utf8") : "";
27863
+ const existingLines = new Set(existing.split(`
27864
+ `).map((l) => l.trim()).filter(Boolean));
27865
+ const added = [];
27866
+ const append = [];
27867
+ for (const entry of GITIGNORE_ENTRIES) {
27868
+ if (entry.trim() === "" || existingLines.has(entry.trim()))
27869
+ continue;
27870
+ append.push(entry);
27871
+ added.push(entry);
27852
27872
  }
27853
- return sections;
27854
- }
27855
- function parseKeyValueLines(text, keys) {
27856
- const out = {};
27857
- for (const line of text.split(`
27858
- `)) {
27859
- for (const key of keys) {
27860
- const prefix = `${key}: `;
27861
- if (line.startsWith(prefix)) {
27862
- out[key] = line.slice(prefix.length).trim();
27863
- }
27864
- }
27873
+ if (append.length) {
27874
+ const separator = existing && !existing.endsWith(`
27875
+ `) ? `
27876
+ ` : "";
27877
+ writeFileSync3(file, existing + separator + (existing ? `
27878
+ ` : "") + append.join(`
27879
+ `) + `
27880
+ `, "utf8");
27881
+ } else if (!existsSync8(file)) {
27882
+ writeFileSync3(file, "", "utf8");
27865
27883
  }
27866
- return out;
27867
- }
27884
+ return { ok: true, path: file, added };
27885
+ };
27868
27886
 
27869
- // packages/workit-core/src/core/verify-parse.ts
27870
- function parseVerifyOutput(stdout) {
27871
- const commands = [];
27872
- const parts = stdout.split(/\n## /);
27873
- for (const part of parts.slice(1)) {
27874
- const nl = part.indexOf(`
27875
- `);
27876
- const label = part.slice(0, nl).trim();
27877
- const body = part.slice(nl + 1);
27878
- const cmdMatch = body.match(/^command: (.+)$/m);
27879
- const statusMatch = body.match(/^status: (pass|fail|skipped)(?: \((.+)\))?/m);
27880
- if (cmdMatch && statusMatch) {
27881
- commands.push({
27882
- label,
27883
- command: cmdMatch[1].trim(),
27884
- status: statusMatch[1],
27885
- reason: statusMatch[2]?.trim()
27886
- });
27887
- }
27888
- }
27889
- const summaryMatch = stdout.match(/# Summary[\s\S]*?passed: (\d+)[\s\S]*?failed: (\d+)[\s\S]*?skipped: (\d+)/);
27890
- const passed = summaryMatch ? Number(summaryMatch[1]) : 0;
27891
- const failed = summaryMatch ? Number(summaryMatch[2]) : 0;
27892
- const skipped = summaryMatch ? Number(summaryMatch[3]) : 0;
27893
- return { passed, failed, skipped, commands };
27894
- }
27887
+ // packages/workit-core/src/core/hygiene.ts
27888
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
27889
+ import path16 from "node:path";
27895
27890
 
27896
- // packages/workit-core/src/core/repo-context.ts
27897
- import { spawnSync as spawnSync4 } from "node:child_process";
27898
- import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync6 } from "node:fs";
27899
- import path15 from "node:path";
27900
- var isSafeContextRange = (value) => value.length <= 4096 && (() => {
27891
+ // packages/workit-core/src/core/scripts.ts
27892
+ var PLUGIN_ROOT = packageRoot();
27893
+ var ASSET_ROOT = assetRoot();
27894
+ var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
27895
+
27896
+ // packages/workit-core/src/core/hygiene.ts
27897
+ var repoRoot = assetRoot();
27898
+ var templatesDir = () => path16.join(repoRoot, "templates", "hygiene");
27899
+ var packageJson = (root) => {
27900
+ if (!existsSync9(path16.join(root, "package.json")))
27901
+ return null;
27901
27902
  try {
27902
- gitRevisionParts(value);
27903
- return true;
27903
+ return JSON.parse(readFileSync8(path16.join(root, "package.json"), "utf8"));
27904
27904
  } catch {
27905
- return false;
27905
+ return null;
27906
27906
  }
27907
- })();
27908
- var isResolvableContextRange = (cwd, value) => {
27909
- if (!isSafeContextRange(value))
27910
- return false;
27911
- try {
27912
- resolveGitRevision(cwd, value);
27907
+ };
27908
+ var isOpenSource = (root) => {
27909
+ if (existsSync9(path16.join(root, "LICENSE")))
27913
27910
  return true;
27914
- } catch {
27915
- return false;
27916
- }
27911
+ const pkg = packageJson(root);
27912
+ if (pkg && !pkg.private)
27913
+ return true;
27914
+ return path16.basename(root) === "workflow-toolkit";
27917
27915
  };
27918
- var invalidRange = (cwd) => ({
27919
- stdout: "",
27920
- stderr: `ERROR: invalid revision range
27921
- `,
27922
- exitCode: 2,
27923
- cwd
27924
- });
27925
- var runGit = (cwd, args) => {
27926
- const result = spawnSync4("git", args, { cwd, encoding: "utf8" });
27927
- return {
27928
- stdout: (result.stdout ?? "").trimEnd(),
27929
- stderr: (result.stderr ?? "").trimEnd(),
27930
- exitCode: result.status ?? 1
27931
- };
27916
+ var licenseHolder = (root) => {
27917
+ const pkg = packageJson(root);
27918
+ if (pkg) {
27919
+ const author = pkg.author;
27920
+ if (typeof author === "string" && author.trim())
27921
+ return author.trim();
27922
+ if (author && typeof author === "object") {
27923
+ const name = author.name;
27924
+ if (typeof name === "string" && name.trim())
27925
+ return name.trim();
27926
+ }
27927
+ }
27928
+ return "";
27932
27929
  };
27933
- var repoRoot = (cwd) => {
27934
- const r = runGit(cwd, ["rev-parse", "--show-toplevel"]);
27935
- return r.exitCode === 0 && r.stdout ? r.stdout : cwd;
27930
+ var planHygieneFiles = (root, opts = {}) => {
27931
+ const openSource = opts.includeOpenSource ?? isOpenSource(root);
27932
+ const files = ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes"];
27933
+ if (openSource)
27934
+ files.push("LICENSE", "CONTRIBUTING.md");
27935
+ const planned = [];
27936
+ for (const file of files) {
27937
+ if (existsSync9(path16.join(root, file)))
27938
+ continue;
27939
+ const tpl = path16.join(templatesDir(), file);
27940
+ if (!existsSync9(tpl))
27941
+ continue;
27942
+ const content = readFileSync8(tpl, "utf8").replace(/<PROJECT>/g, path16.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
27943
+ planned.push({ path: path16.join(root, file), content });
27944
+ }
27945
+ return planned;
27936
27946
  };
27937
- var currentBranch = (cwd) => {
27938
- const r = runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
27939
- return r.exitCode === 0 && r.stdout ? r.stdout : "unknown";
27947
+ var ensureHygieneFiles = (root, opts) => {
27948
+ if (!opts.confirmed)
27949
+ return { ok: false, error: "confirmed: true required" };
27950
+ const created = [];
27951
+ for (const planned of planHygieneFiles(root, opts)) {
27952
+ writeFileSync4(planned.path, planned.content, "utf8");
27953
+ created.push(path16.basename(planned.path));
27954
+ }
27955
+ return { ok: true, created };
27940
27956
  };
27941
- var defaultBase = (cwd) => {
27942
- for (const ref of [
27943
- "origin/main",
27944
- "main",
27945
- "origin/master",
27946
- "master",
27947
- "origin/develop",
27948
- "develop"
27949
- ]) {
27950
- if (runGit(cwd, ["rev-parse", "--verify", ref]).exitCode === 0)
27951
- return ref;
27957
+
27958
+ // packages/workit-core/src/core/init.ts
27959
+ import fs9 from "node:fs";
27960
+ import path19 from "node:path";
27961
+ import { spawnSync as spawnSync5 } from "node:child_process";
27962
+
27963
+ // packages/workit-core/src/core/safe-write.ts
27964
+ import {
27965
+ closeSync as closeSync2,
27966
+ fchmodSync,
27967
+ fsyncSync as fsyncSync2,
27968
+ lstatSync as lstatSync4,
27969
+ mkdirSync as mkdirSync4,
27970
+ openSync as openSync2,
27971
+ renameSync as renameSync2,
27972
+ unlinkSync as unlinkSync3,
27973
+ writeFileSync as writeFileSync5
27974
+ } from "node:fs";
27975
+ function writeFileExclusive(file, content, mode) {
27976
+ try {
27977
+ writeFileSync5(file, content, { encoding: "utf8", flag: "wx", mode });
27978
+ return "created";
27979
+ } catch (err) {
27980
+ if (err.code === "EEXIST")
27981
+ return "preserved";
27982
+ throw err;
27952
27983
  }
27953
- return "HEAD~1";
27984
+ }
27985
+
27986
+ // packages/workit-core/src/core/setup.ts
27987
+ import {
27988
+ copyFileSync as copyFileSync2,
27989
+ cpSync as cpSync2,
27990
+ existsSync as existsSync10,
27991
+ lstatSync as lstatSync5,
27992
+ mkdirSync as mkdirSync5,
27993
+ mkdtempSync,
27994
+ readFileSync as readFileSync9,
27995
+ readdirSync as readdirSync6,
27996
+ realpathSync as realpathSync9,
27997
+ renameSync as renameSync3,
27998
+ rmSync,
27999
+ statSync as statSync3,
28000
+ writeFileSync as writeFileSync6
28001
+ } from "node:fs";
28002
+ import path17 from "node:path";
28003
+ import { isDeepStrictEqual } from "node:util";
28004
+
28005
+ // packages/workit-core/src/core/branch-policy.ts
28006
+ import { spawnSync as spawnSync4 } from "node:child_process";
28007
+ var PREFIXES = {
28008
+ feature: "feature/*",
28009
+ bugfix: "bugfix/*",
28010
+ release: "release/*",
28011
+ hotfix: "hotfix/*"
27954
28012
  };
27955
- var rangeArgOrDefault = (arg, cwd) => arg ? arg : `${defaultBase(cwd)}...HEAD`;
27956
- var isProtectedBranch = (branch) => ["main", "master", "develop", "prod", "production"].includes(branch);
27957
- var isPrBranch = (branch) => branch.startsWith("feature/") || branch.startsWith("bugfix/");
27958
- function resolvePrBranchContext(cwd) {
27959
- const branch = currentBranch(cwd);
27960
- if (isProtectedBranch(branch)) {
28013
+ var detectBranchPolicy = (workspaceRoot) => {
28014
+ const branchExists = (name) => {
28015
+ const r = spawnSync4("git", ["branch", "--list", name], {
28016
+ cwd: workspaceRoot,
28017
+ encoding: "utf8"
28018
+ });
28019
+ return r.status === 0 && (r.stdout ?? "").trim() !== "";
28020
+ };
28021
+ const develop = branchExists("develop");
28022
+ const main = branchExists("main");
28023
+ const master = branchExists("master");
28024
+ const root = main ? "main" : master ? "master" : null;
28025
+ if (develop) {
27961
28026
  return {
27962
- ok: false,
27963
- error: `cannot build PR context on protected branch ${branch} — PRs are for feature/* or bugfix/* only`
28027
+ preset: "gitflow",
28028
+ developBranch: "develop",
28029
+ integration: "merge",
28030
+ protected: root ? [root, "develop"] : ["develop"],
28031
+ allowed: [...PRESETS.gitflow.allowed],
28032
+ prefixes: PREFIXES
27964
28033
  };
27965
28034
  }
27966
- if (!isPrBranch(branch)) {
27967
- if (branch === "unknown") {
27968
- return {
27969
- ok: false,
27970
- error: `not in a git repository at ${repoRoot(cwd)} — open the target repository as the OpenCode session directory`
27971
- };
27972
- }
28035
+ if (root === "main") {
27973
28036
  return {
27974
- ok: false,
27975
- error: `branch ${branch} is not feature/* or bugfix/* — checkout a feature branch or pass an explicit git range`
28037
+ preset: "github-flow",
28038
+ developBranch: null,
28039
+ integration: "pr",
28040
+ protected: [root],
28041
+ allowed: ["*"],
28042
+ prefixes: PREFIXES
27976
28043
  };
27977
28044
  }
27978
- const resolved = vcsConfig("resolve", cwd);
27979
- if (resolved.ok === false) {
27980
- return { ok: false, error: String(resolved.error) };
27981
- }
27982
- const base = String(resolved.defaultTargetBranch ?? "");
27983
- if (!base || runGit(cwd, ["check-ref-format", "--branch", base]).exitCode !== 0) {
27984
- return { ok: false, error: `invalid configured PR target branch ${base}` };
27985
- }
27986
- for (const ref of [`origin/${base}`, base]) {
27987
- if (runGit(cwd, ["rev-parse", "--verify", ref]).exitCode !== 0)
27988
- continue;
27989
- const mb = runGit(cwd, ["merge-base", ref, "HEAD"]);
27990
- if (mb.exitCode !== 0 || !mb.stdout)
27991
- continue;
28045
+ if (root === "master") {
27992
28046
  return {
27993
- ok: true,
27994
- value: {
27995
- baseRef: ref,
27996
- mergeBase: mb.stdout,
27997
- range: `${ref}..HEAD`,
27998
- diffRange: `${mb.stdout}..HEAD`
27999
- }
28047
+ preset: "trunk-based",
28048
+ developBranch: null,
28049
+ integration: "pr",
28050
+ protected: [root],
28051
+ allowed: ["*"],
28052
+ prefixes: PREFIXES
28000
28053
  };
28001
28054
  }
28002
28055
  return {
28003
- ok: false,
28004
- error: `configured PR target branch ${base} not found — fetch/checkout it or pass an explicit git range`
28056
+ preset: "gitflow",
28057
+ developBranch: null,
28058
+ integration: "merge",
28059
+ protected: [],
28060
+ allowed: [],
28061
+ prefixes: PREFIXES
28005
28062
  };
28006
- }
28007
- var printSection = (title) => `
28008
- ## ${title}
28009
-
28010
- `;
28011
- var probeCaseInsensitive = (dir, wanted) => {
28012
- try {
28013
- for (const entry of readdirSync6(dir)) {
28014
- if (entry.toLowerCase() === wanted.toLowerCase())
28015
- return entry;
28016
- }
28017
- } catch {}
28018
- return null;
28019
- };
28020
- var findPrTemplate = (cwd) => {
28021
- for (const rel of [
28022
- ".gitlab/merge_request_templates/Default.md",
28023
- ".gitlab/merge_request_templates/default.md",
28024
- ".gitlab/merge_request_templates/merge_request_template.md",
28025
- ".github/PULL_REQUEST_TEMPLATE.md",
28026
- ".github/pull_request_template.md",
28027
- ".github/PULL_REQUEST_TEMPLATE/pull_request_template.md",
28028
- "docs/PULL_REQUEST_TEMPLATE.md",
28029
- "PULL_REQUEST_TEMPLATE.md"
28030
- ]) {
28031
- const parts = rel.split("/");
28032
- const base = parts[parts.length - 1];
28033
- const dir = parts.length > 1 ? path15.join(cwd, ...parts.slice(0, -1)) : cwd;
28034
- const actual = probeCaseInsensitive(dir, base);
28035
- if (actual)
28036
- return [...parts.slice(0, -1), actual].join("/");
28037
- }
28038
- return null;
28039
28063
  };
28040
- var fallbackPrTemplate = () => `## Summary
28041
- -
28042
28064
 
28043
- ## Validation
28044
- - [ ] Not run`;
28045
- var commitLogForRange = (cwd, range) => {
28046
- const r = runGit(cwd, ["log", "--oneline", "--decorate", "--no-merges", range, "--"]);
28047
- if (r.exitCode === 0)
28048
- return r.stdout;
28049
- return runGit(cwd, ["log", "--oneline", "--decorate", "-10", "--"]).stdout;
28050
- };
28051
- var changedFilesForRange = (cwd, range) => {
28052
- const r = runGit(cwd, ["diff", "--name-only", range, "--"]);
28053
- if (r.exitCode === 0)
28054
- return r.stdout;
28055
- return runGit(cwd, ["diff", "--name-only", "--"]).stdout;
28056
- };
28057
- var diffStatForRange = (cwd, range) => {
28058
- const r = runGit(cwd, ["diff", "--stat", range, "--"]);
28059
- if (r.exitCode === 0)
28060
- return r.stdout;
28061
- return runGit(cwd, ["diff", "--stat", "--"]).stdout;
28062
- };
28063
- var packageScripts = (cwd, keys) => {
28064
- const pkgPath = path15.join(cwd, "package.json");
28065
- if (!existsSync8(pkgPath))
28066
- return [];
28065
+ // packages/workit-core/src/core/registration.ts
28066
+ var CURSOR_RUNTIME_PACKAGE = "@brainervirus/workit-cursor@latest";
28067
+ var CURSOR_HOOK_RUN_COMMAND = `npx -y --prefer-online --min-release-age=0 --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-hook`;
28068
+ function cursorHooksEntry(_packageDir) {
28069
+ return {
28070
+ command: `npx -y --prefer-online --min-release-age=0 --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-session-start`,
28071
+ args: []
28072
+ };
28073
+ }
28074
+
28075
+ // packages/workit-core/src/core/doctor.ts
28076
+ var canonicalCursorHook = cursorHooksEntry("").command;
28077
+ var INSTALLER_REQUIRED = new Set([
28078
+ "runtime",
28079
+ "assets",
28080
+ "launcher",
28081
+ "utility",
28082
+ "stale_pin",
28083
+ "stale_install",
28084
+ "duplicate_registration",
28085
+ "malformed_config"
28086
+ ]);
28087
+ // packages/workit-core/src/core/setup.ts
28088
+ var YT_OVERRIDES = [
28089
+ { envKey: "WORKFLOW_YT_BASE_URL", affects: "youtrack.json baseUrl" },
28090
+ { envKey: "WORKFLOW_YT_TOKEN_FILE", affects: "youtrack.json tokenFile" },
28091
+ { envKey: "WORKFLOW_YT_TIMEZONE", affects: "youtrack.json timezone" },
28092
+ { envKey: "WORKFLOW_YT_MENTION", affects: "youtrack.json defaultMention" },
28093
+ { envKey: "WORKFLOW_YT_MEETING_ISSUE", affects: "youtrack.json meetingIssue" },
28094
+ { envKey: "WORKFLOW_YT_WEB_MEETING_ISSUE", affects: "youtrack.json web meeting issue" }
28095
+ ];
28096
+ var VCS_OVERRIDES = [
28097
+ { envKey: "WORKFLOW_VCS_PROVIDER", affects: "vcs.json provider" },
28098
+ { envKey: "WORKFLOW_VCS_TARGET_BRANCH", affects: "vcs.json defaultTargetBranch" },
28099
+ { envKey: "WORKFLOW_GITLAB_HOST", affects: "vcs.json gitlab.host" },
28100
+ { envKey: "WORKFLOW_GITLAB_API_URL", affects: "vcs.json gitlab.apiUrl" },
28101
+ { envKey: "WORKFLOW_GITHUB_HOST", affects: "vcs.json github.host" }
28102
+ ];
28103
+ var SETUP_OVERRIDES = [...YT_OVERRIDES, ...VCS_OVERRIDES];
28104
+ var readFileSafe = (p) => {
28067
28105
  try {
28068
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
28069
- return keys.filter((k) => pkg.scripts && typeof pkg.scripts[k] === "string").map((k) => `${k}: ${pkg.scripts?.[k]}`);
28106
+ return readFileSync9(p, "utf8");
28070
28107
  } catch {
28071
- return [];
28108
+ return null;
28072
28109
  }
28073
28110
  };
28074
- var documentationFiles = (cwd) => {
28075
- const excluded = new Set([".git", "node_modules", "target", "dist"]);
28076
- const matches = [];
28077
- const walk = (dir, depth) => {
28078
- if (depth > 3)
28079
- return;
28080
- let entries;
28111
+ function applyWorkspaceBranchPolicy(opts) {
28112
+ const { workspace_root, env = process.env } = opts;
28113
+ const dir = path17.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
28114
+ const {
28115
+ status,
28116
+ path: wsPath,
28117
+ entries,
28118
+ document,
28119
+ revision,
28120
+ error: workspacesError
28121
+ } = readWorkspacesResult(dir);
28122
+ if (status === "malformed" || status === "invalid")
28123
+ return { ok: false, error: workspacesError ?? `invalid workspaces.json: ${wsPath}` };
28124
+ const detection = detectBranchPolicy(workspace_root);
28125
+ const name = String(env.WORKFLOW_BP_NAME ?? path17.basename(workspace_root));
28126
+ const integration = env.WORKFLOW_BP_INTEGRATION ?? detection.integration;
28127
+ const policy = {
28128
+ preset: detection.preset,
28129
+ developBranch: env.WORKFLOW_BP_DEVELOP ?? detection.developBranch ?? undefined,
28130
+ prefixes: detection.prefixes,
28131
+ allowed: detection.allowed,
28132
+ protected: detection.protected,
28133
+ integration
28134
+ };
28135
+ const glob = `${workspace_root.replace(/[\\/]+$/, "")}/**`;
28136
+ if (!validateWorkspaceGlob(glob).ok)
28137
+ return { ok: false, error: `invalid workspace glob: ${glob}` };
28138
+ let existing;
28139
+ try {
28140
+ existing = resolveWorkspaceFrom(workspace_root, dir);
28141
+ } catch (error) {
28142
+ const selectedName = env.WORKFLOW_BP_NAME?.trim();
28143
+ if (!selectedName)
28144
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
28081
28145
  try {
28082
- entries = readdirSync6(dir, { withFileTypes: true });
28083
- } catch {
28084
- return;
28146
+ existing = resolveWorkspaceFrom(workspace_root, dir, selectedName);
28147
+ } catch (selectionError) {
28148
+ return {
28149
+ ok: false,
28150
+ error: selectionError instanceof Error ? selectionError.message : String(selectionError)
28151
+ };
28085
28152
  }
28086
- for (const entry of entries) {
28087
- if (entry.isDirectory()) {
28088
- if (excluded.has(entry.name))
28089
- continue;
28090
- walk(path15.join(dir, entry.name), depth + 1);
28091
- } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
28092
- matches.push("./" + path15.relative(cwd, path15.join(dir, entry.name)).replaceAll("\\", "/"));
28093
- }
28153
+ }
28154
+ if (existing?.branchPolicy && isDeepStrictEqual(existing.branchPolicy, policy)) {
28155
+ return {
28156
+ ok: true,
28157
+ status: "already-configured",
28158
+ workspace: existing,
28159
+ policy,
28160
+ config_path: wsPath
28161
+ };
28162
+ }
28163
+ const next = existing ? entries.map((w) => w.name === existing?.name ? { ...w, branchPolicy: policy } : w) : [...entries, { name, glob, branchPolicy: policy }];
28164
+ if (!existing && entries.some((entry) => entry.name === name))
28165
+ return { ok: false, error: `workspace name ${JSON.stringify(name)} is already configured` };
28166
+ const proposed = validateWorkspacesDocument({ ...document, workspaces: next }, wsPath);
28167
+ if (proposed.status !== "valid")
28168
+ return { ok: false, error: proposed.error };
28169
+ if (workspacesRevision(readFileSafe(wsPath)) !== revision)
28170
+ return {
28171
+ ok: false,
28172
+ error: `workspaces.json changed while applying the branch policy: ${wsPath}`
28173
+ };
28174
+ mkdirSync5(path17.dirname(wsPath), { recursive: true });
28175
+ const tmp = `${wsPath}.${process.pid}.tmp`;
28176
+ try {
28177
+ writeFileSync6(tmp, JSON.stringify({ ...document, workspaces: next }, null, 2) + `
28178
+ `, "utf8");
28179
+ if (workspacesRevision(readFileSafe(wsPath)) !== revision) {
28180
+ rmSync(tmp, { force: true });
28181
+ return {
28182
+ ok: false,
28183
+ error: `workspaces.json changed while applying the branch policy: ${wsPath}`
28184
+ };
28094
28185
  }
28186
+ renameSync3(tmp, wsPath);
28187
+ } catch (error) {
28188
+ try {
28189
+ rmSync(tmp, { force: true });
28190
+ } catch {}
28191
+ return {
28192
+ ok: false,
28193
+ error: `failed to write ${wsPath}: ${error instanceof Error ? error.message : String(error)}`
28194
+ };
28195
+ }
28196
+ return {
28197
+ ok: true,
28198
+ status: existing ? "updated" : "configured",
28199
+ workspace: existing ? { ...existing, branchPolicy: policy } : { name, glob, branchPolicy: policy },
28200
+ policy,
28201
+ config_path: wsPath
28095
28202
  };
28096
- walk(cwd, 1);
28097
- return matches.sort().slice(0, 200);
28098
- };
28099
- var readTrimmed = (file, maxLines) => {
28100
- if (!existsSync8(file))
28101
- return "";
28102
- const lines = readFileSync7(file, "utf8").split(`
28103
- `);
28104
- return lines.slice(0, maxLines).join(`
28105
- `);
28203
+ }
28204
+
28205
+ // packages/workit-core/src/core/youtrack.ts
28206
+ import fs8 from "node:fs";
28207
+ import path18 from "node:path";
28208
+
28209
+ // packages/workit-core/src/core/templates.ts
28210
+ var repoRoot2 = assetRoot();
28211
+
28212
+ // packages/workit-core/src/core/youtrack.ts
28213
+ var ISSUE_RE = /^[A-Z]+-\d+$/;
28214
+ var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
28215
+ var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(configDir(), "youtrack.json");
28216
+ var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path18.join(resolveConfigDir(), "youtrack.json");
28217
+ var youTrackTokenModeOk = (p) => {
28218
+ if (process.platform === "win32")
28219
+ return true;
28220
+ const mode = fs8.statSync(p).mode & 511;
28221
+ return mode === 384;
28106
28222
  };
28107
- function prReadyContext(root, range) {
28108
- const cwd = path15.resolve(repoRoot(root));
28109
- if (range !== undefined && !isResolvableContextRange(cwd, range))
28110
- return invalidRange(cwd);
28111
- let stdout = "";
28112
- const autoRange = range === undefined;
28113
- let ctx = null;
28114
- if (autoRange) {
28115
- ctx = resolvePrBranchContext(cwd);
28116
- if (!ctx.ok)
28117
- return { stdout: "", stderr: `ERROR: ${ctx.error}
28118
- `, exitCode: 1, cwd };
28119
- range = ctx.value.range;
28120
- }
28121
- stdout += printSection("Repository");
28122
- stdout += `root: ${cwd}
28123
- `;
28124
- stdout += `branch: ${currentBranch(cwd)}
28125
- `;
28126
- stdout += `range: ${range}
28127
- `;
28128
- if (autoRange && ctx?.ok) {
28129
- stdout += `base_ref: ${ctx.value.baseRef}
28130
- `;
28131
- stdout += `merge_base: ${ctx.value.mergeBase}
28132
- `;
28133
- stdout += `diff_range: ${ctx.value.diffRange}
28134
- `;
28135
- stdout += `range_mode: branch-exclusive
28136
- `;
28137
- if (process.env.PR_SYNC_NOTES)
28138
- stdout += `git_sync: ${process.env.PR_SYNC_NOTES}
28139
- `;
28140
- }
28141
- const diffRange = autoRange && ctx?.ok ? ctx.value.diffRange : range ?? "";
28142
- stdout += printSection("Working Tree");
28143
- const status = runGit(cwd, ["status", "--short"]);
28144
- stdout += status.stdout + (status.stderr ? `
28145
- ${status.stderr}` : "");
28146
- stdout += `
28147
- `;
28148
- stdout += printSection("Commits");
28149
- stdout += commitLogForRange(cwd, range ?? "");
28150
- stdout += `
28151
- `;
28152
- stdout += printSection("Diff Stat");
28153
- stdout += diffStatForRange(cwd, diffRange);
28154
- stdout += `
28155
- `;
28156
- stdout += printSection("Changed Files");
28157
- stdout += changedFilesForRange(cwd, diffRange);
28158
- stdout += `
28159
- `;
28160
- stdout += printSection("PR Template");
28161
- const template = findPrTemplate(cwd);
28162
- if (template) {
28163
- stdout += `template_path: ${template}
28164
-
28165
- `;
28166
- stdout += readTrimmed(path15.join(cwd, template), 220);
28167
- } else {
28168
- stdout += `template_path: none
28169
-
28170
- `;
28171
- stdout += fallbackPrTemplate();
28172
- }
28173
- stdout += `
28174
- `;
28175
- stdout += printSection("Recent Validation Signals");
28176
- if (existsSync8(path15.join(cwd, "package.json"))) {
28177
- stdout += `package.json detected. Common scripts:
28178
- `;
28179
- const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
28180
- if (found.length)
28181
- stdout += found.join(`
28182
- `) + `
28183
- `;
28184
- }
28185
- if (existsSync8(path15.join(cwd, "Cargo.toml")) || existsSync8(path15.join(cwd, "src-tauri/Cargo.toml"))) {
28186
- stdout += `Rust project detected.
28187
- `;
28223
+ function readYouTrackConfig(required) {
28224
+ const cfgPath = youTrackConfigPath();
28225
+ if (!fs8.existsSync(cfgPath)) {
28226
+ return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
28188
28227
  }
28189
- stdout += printSection("VCS Config");
28190
- const vc = vcsConfig("resolve", cwd);
28191
- if (vc.ok) {
28192
- stdout += `workspace: ${String(vc.workspace_name ?? "none")}
28193
- `;
28194
- stdout += `provider: ${String(vc.provider ?? "unconfigured")}
28195
- `;
28196
- } else {
28197
- stdout += `vcs: unreadable (malformed) — ${String(vc.error ?? "cannot read vcs.json")}
28198
- `;
28228
+ let parsed;
28229
+ try {
28230
+ parsed = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
28231
+ } catch {
28232
+ return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
28199
28233
  }
28200
- if (!vcsConfig("summary", cwd).ok) {
28201
- stdout += `vcs: not configured — run /wk-init action vcs_scaffold
28202
- `;
28234
+ if (!isConfigObject(parsed)) {
28235
+ return { error: `${cfgPath} is not a JSON object`, path: cfgPath };
28203
28236
  }
28204
- stdout += printSection("Merged PR Style");
28205
- const style = mergedPrStyle(6, cwd);
28206
- stdout += JSON.stringify(style, null, 2) + `
28207
- `;
28208
- return { stdout, stderr: "", exitCode: 0, cwd };
28237
+ return { config: parsed, path: cfgPath };
28209
28238
  }
28210
- var CHANGELOG_RULES = `- Use an [Unreleased] section.
28211
- - Use Added, Changed, Deprecated, Removed, Fixed, Security.
28212
- - Entries should be human-readable and user-facing.
28213
- - Do not use raw commit messages as changelog bullets.
28214
- - MERGE into existing ### Category under [Unreleased] — never append a second ### Added / ### Fixed block.
28215
- - Apply with the host-native editor, preserving the repository changelog conventions.
28216
- - If Unreleased already has duplicate category headings, normalize_only first.`;
28217
- function changelogContext(root, range) {
28218
- const cwd = path15.resolve(repoRoot(root));
28219
- if (range !== undefined && !isResolvableContextRange(cwd, range))
28220
- return invalidRange(cwd);
28221
- const resolvedRange = rangeArgOrDefault(range, cwd);
28222
- if (!isResolvableContextRange(cwd, resolvedRange))
28223
- return invalidRange(cwd);
28224
- let stdout = "";
28225
- stdout += printSection("Repository");
28226
- stdout += `root: ${cwd}
28227
- `;
28228
- stdout += `branch: ${currentBranch(cwd)}
28229
- `;
28230
- stdout += `range: ${resolvedRange}
28231
- `;
28232
- stdout += printSection("Keep a Changelog Rules");
28233
- stdout += CHANGELOG_RULES + `
28234
- `;
28235
- stdout += printSection("Existing CHANGELOG.md");
28236
- const changelogPath = path15.join(cwd, "CHANGELOG.md");
28237
- if (existsSync8(changelogPath)) {
28238
- stdout += readTrimmed(changelogPath, 260) + `
28239
- `;
28240
- } else {
28241
- stdout += `CHANGELOG.md not found.
28242
- `;
28239
+ var readYouTrackContextConfig = () => {
28240
+ const cfgPath = youTrackReadOnlyConfigPath();
28241
+ if (!fs8.existsSync(cfgPath))
28242
+ return { data: {} };
28243
+ let parsed;
28244
+ try {
28245
+ parsed = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
28246
+ } catch {
28247
+ return { error: "YouTrack configuration is unavailable" };
28243
28248
  }
28244
- stdout += printSection("Commits");
28245
- stdout += commitLogForRange(cwd, resolvedRange) + `
28246
- `;
28247
- stdout += printSection("Diff Stat");
28248
- stdout += diffStatForRange(cwd, resolvedRange) + `
28249
- `;
28250
- stdout += printSection("Changed Files");
28251
- stdout += changedFilesForRange(cwd, resolvedRange) + `
28252
- `;
28253
- return { stdout, stderr: "", exitCode: 0, cwd };
28249
+ if (!isConfigObject(parsed))
28250
+ return { error: "YouTrack configuration is unavailable" };
28251
+ return { data: readOnlyYouTrackConfig(parsed) };
28252
+ };
28253
+ function tzParts(date, tz) {
28254
+ const parts = new Intl.DateTimeFormat("en-GB", {
28255
+ timeZone: tz,
28256
+ year: "numeric",
28257
+ month: "2-digit",
28258
+ day: "2-digit",
28259
+ hour: "2-digit",
28260
+ minute: "2-digit",
28261
+ hourCycle: "h23"
28262
+ }).formatToParts(date);
28263
+ const map = Object.fromEntries(parts.map((p) => [p.type, p.value]));
28264
+ return { y: map.year, m: map.month, d: map.day, hour: map.hour, minute: map.minute };
28254
28265
  }
28255
- function docsRefreshContext(root, range) {
28256
- const cwd = path15.resolve(repoRoot(root));
28257
- if (range !== undefined && !isResolvableContextRange(cwd, range))
28258
- return invalidRange(cwd);
28259
- const resolvedRange = rangeArgOrDefault(range, cwd);
28260
- if (!isResolvableContextRange(cwd, resolvedRange))
28261
- return invalidRange(cwd);
28262
- let stdout = "";
28263
- stdout += printSection("Repository");
28264
- stdout += `root: ${cwd}
28265
- `;
28266
- stdout += `branch: ${currentBranch(cwd)}
28267
- `;
28268
- stdout += `range: ${resolvedRange}
28269
- `;
28270
- stdout += printSection("Changed Files");
28271
- stdout += changedFilesForRange(cwd, resolvedRange) + `
28272
- `;
28273
- stdout += printSection("Documentation Files");
28274
- stdout += documentationFiles(cwd).join(`
28275
- `) + `
28276
- `;
28277
- stdout += printSection("README Preview");
28278
- const readme = path15.join(cwd, "README.md");
28279
- if (existsSync8(readme)) {
28280
- stdout += readTrimmed(readme, 220) + `
28281
- `;
28282
- } else {
28283
- stdout += `README.md not found.
28284
- `;
28266
+ function youTrackGreeting(configOverride) {
28267
+ const cfgPath = configOverride ?? youTrackConfigPath();
28268
+ let parsed;
28269
+ try {
28270
+ parsed = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
28271
+ } catch {
28272
+ return {
28273
+ stdout: "",
28274
+ exitCode: 1,
28275
+ stderr: fs8.existsSync(cfgPath) ? `${cfgPath} is not valid JSON` : `missing youtrack.json: ${cfgPath}`
28276
+ };
28285
28277
  }
28286
- stdout += printSection("Package Scripts");
28287
- const pkgPath = path15.join(cwd, "package.json");
28288
- if (existsSync8(pkgPath)) {
28289
- try {
28290
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
28291
- stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
28292
- `;
28293
- } catch {}
28294
- } else {
28295
- stdout += `package.json not found.
28296
- `;
28278
+ if (!isConfigObject(parsed)) {
28279
+ return { stdout: "", exitCode: 1, stderr: `${cfgPath} is not a JSON object` };
28297
28280
  }
28298
- return { stdout, stderr: "", exitCode: 0, cwd };
28299
- }
28300
- function releaseNotesContext(root, rangeOrTag) {
28301
- const cwd = path15.resolve(repoRoot(root));
28302
- if (!rangeOrTag) {
28303
- return { stdout: "", stderr: `ERROR: release tag or range required
28304
- `, exitCode: 1, cwd };
28281
+ const config = parsed;
28282
+ try {
28283
+ const tz = String(config.timezone ?? "America/Santiago");
28284
+ const now = new Date;
28285
+ const { y, m, d, hour, minute } = tzParts(now, tz);
28286
+ const cutoff = String(config.greetingCutoff ?? "12:00").split(":");
28287
+ const cutoffHour = Number(cutoff[0]);
28288
+ const cutoffMinute = Number(cutoff[1] ?? 0);
28289
+ const greetings = config.greetings ?? {};
28290
+ const isMorning = Number(hour) < cutoffHour || Number(hour) === cutoffHour && Number(minute) < cutoffMinute;
28291
+ const greeting = isMorning ? greetings.morning ?? "buenos días" : greetings.afternoon ?? "buenas tardes";
28292
+ const mention = String(config.defaultMention ?? "Alejandra.Flores");
28293
+ return { stdout: `@${mention} Hola, ${greeting}.
28294
+ `, exitCode: 0, stderr: "" };
28295
+ } catch (err) {
28296
+ return {
28297
+ stdout: "",
28298
+ exitCode: 1,
28299
+ stderr: err instanceof Error ? err.message : "greeting failed"
28300
+ };
28305
28301
  }
28306
- if (!isResolvableContextRange(cwd, rangeOrTag))
28307
- return invalidRange(cwd);
28308
- const resolvedRange = rangeArgOrDefault(rangeOrTag, cwd);
28309
- if (!isResolvableContextRange(cwd, resolvedRange))
28310
- return invalidRange(cwd);
28311
- let stdout = "";
28312
- stdout += printSection("Repository");
28313
- stdout += `root: ${cwd}
28314
- `;
28315
- stdout += `branch: ${currentBranch(cwd)}
28316
- `;
28317
- stdout += `requested: ${rangeOrTag}
28318
- `;
28319
- stdout += `range: ${resolvedRange}
28320
- `;
28321
- stdout += printSection("Tags");
28322
- const tags = runGit(cwd, ["tag", "--sort=-creatordate"]);
28323
- if (tags.exitCode === 0)
28324
- stdout += tags.stdout.split(`
28325
- `).slice(0, 20).join(`
28326
- `) + `
28327
- `;
28328
- stdout += printSection("Commits");
28329
- stdout += commitLogForRange(cwd, resolvedRange) + `
28330
- `;
28331
- stdout += printSection("Diff Stat");
28332
- stdout += diffStatForRange(cwd, resolvedRange) + `
28333
- `;
28334
- stdout += printSection("Changed Files");
28335
- stdout += changedFilesForRange(cwd, resolvedRange) + `
28336
- `;
28337
- stdout += printSection("Existing Release Files");
28338
- for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
28339
- if (existsSync8(path15.join(cwd, rel)))
28340
- stdout += rel + `
28341
- `;
28342
- }
28343
- stdout += printSection("Release Notes Draft");
28344
- stdout += `## Release notes (${resolvedRange})
28345
-
28346
- `;
28347
- const commits = commitLogForRange(cwd, resolvedRange).split(`
28348
- `).map((line) => line.trim()).filter(Boolean);
28349
- stdout += commits.length ? `${commits.map((line) => `- ${line}`).join(`
28350
- `)}
28351
- ` : `- No commits in range.
28352
- `;
28353
- stdout += `
28354
- ### Changed files
28355
-
28356
- `;
28357
- const files = changedFilesForRange(cwd, resolvedRange).split(`
28358
- `).map((line) => line.trim()).filter(Boolean);
28359
- stdout += files.length ? `${files.map((line) => `- ${line}`).join(`
28360
- `)}
28361
- ` : `- None.
28362
- `;
28363
- return { stdout, stderr: "", exitCode: 0, cwd };
28364
28302
  }
28365
-
28366
- // packages/workit-core/src/core/verify-project.ts
28367
- import { spawnSync as spawnSync5 } from "node:child_process";
28368
- import { existsSync as existsSync9, readFileSync as readFileSync8, statSync as statSync3 } from "node:fs";
28369
- import path16 from "node:path";
28370
- var commandOnPath = (name) => {
28371
- const dirs = (process.env.PATH ?? "").split(path16.delimiter);
28372
- const names = process.platform === "win32" ? [name, `${name}.exe`] : [name];
28373
- for (const dir of dirs) {
28374
- if (!dir)
28375
- continue;
28376
- for (const candidateName of names) {
28377
- const candidate = path16.join(dir, candidateName);
28378
- try {
28379
- statSync3(candidate);
28380
- if (process.platform !== "win32") {
28381
- const mode = statSync3(candidate).mode & 73;
28382
- if (mode === 0)
28383
- continue;
28384
- }
28385
- return true;
28386
- } catch {}
28387
- }
28303
+ function youTrackParseDuration(text) {
28304
+ const lower = String(text).toLowerCase().trim();
28305
+ let total = 0;
28306
+ for (const match of lower.matchAll(/(\d+)\s*h/g))
28307
+ total += Number(match[1]) * 60;
28308
+ for (const match of lower.matchAll(/(\d+)\s*m/g))
28309
+ total += Number(match[1]);
28310
+ if (total === 0 && /^\d+$/.test(lower))
28311
+ total = Number(lower);
28312
+ if (total <= 0)
28313
+ return { error: "could not parse duration" };
28314
+ return { data: { minutes: total, text: String(text).trim() } };
28315
+ }
28316
+ var youTrackToken = () => {
28317
+ const loaded = readYouTrackConfig(true);
28318
+ if ("error" in loaded)
28319
+ return loaded;
28320
+ const tokenFile = String(loaded.config.tokenFile ?? "");
28321
+ const tokenPath = tokenFile ? path18.isAbsolute(tokenFile) ? path18.resolve(tokenFile) : path18.resolve(process.cwd(), tokenFile) : "";
28322
+ if (!tokenPath || !fs8.existsSync(tokenPath))
28323
+ return { error: "missing youtrack.token" };
28324
+ if (!youTrackTokenModeOk(tokenPath))
28325
+ return { error: "youtrack.token mode must be 0600" };
28326
+ const token = fs8.readFileSync(tokenPath, "utf8").trim();
28327
+ if (!token)
28328
+ return { error: "empty token file" };
28329
+ if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
28330
+ return {
28331
+ error: "token file still has placeholder YOUR_TOKEN_HERE — edit the file locally, then run /wk-status"
28332
+ };
28388
28333
  }
28389
- return false;
28334
+ const base = String(loaded.config.baseUrl ?? "").replace(/\/+$/, "");
28335
+ if (!base)
28336
+ return { error: "baseUrl missing in config" };
28337
+ return { token, base };
28390
28338
  };
28391
- var hasScript = (cwd, name) => {
28392
- const pkgPath = path16.join(cwd, "package.json");
28393
- if (!existsSync9(pkgPath))
28394
- return false;
28339
+ async function youTrackRequest(url, init) {
28340
+ const headers = {
28341
+ Authorization: `Bearer ${init.token}`,
28342
+ Accept: "application/json"
28343
+ };
28344
+ let body;
28345
+ if (init.body !== undefined) {
28346
+ headers["Content-Type"] = "application/json";
28347
+ body = JSON.stringify(init.body);
28348
+ }
28395
28349
  try {
28396
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
28397
- return Boolean(pkg.scripts && typeof pkg.scripts[name] === "string");
28398
- } catch {
28399
- return false;
28350
+ const res = await fetch(url, { method: init.method, headers, body });
28351
+ const text = await res.text();
28352
+ if (!res.ok) {
28353
+ return { status: res.status, stdout: "", stderr: text.slice(0, 200) };
28354
+ }
28355
+ return { status: 0, stdout: text, stderr: "" };
28356
+ } catch (err) {
28357
+ return { status: 1, stdout: "", stderr: err instanceof Error ? err.message : "network error" };
28400
28358
  }
28401
- };
28402
- var packageRunner = (cwd) => {
28403
- if (existsSync9(path16.join(cwd, "pnpm-lock.yaml")))
28404
- return "pnpm";
28405
- const pkgPath = path16.join(cwd, "package.json");
28406
- if (existsSync9(pkgPath)) {
28407
- try {
28408
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
28409
- if (typeof pkg.packageManager === "string" && pkg.packageManager.startsWith("pnpm")) {
28410
- return "pnpm";
28411
- }
28412
- } catch {}
28359
+ }
28360
+ function youTrackTokenCreateUrl() {
28361
+ const tokenName = process.env.WORKFLOW_YT_TOKEN_NAME ?? "workit";
28362
+ const loaded = readYouTrackConfig(false);
28363
+ if ("error" in loaded) {
28364
+ return { data: { error: loaded.error } };
28413
28365
  }
28414
- if (existsSync9(path16.join(cwd, "yarn.lock")))
28415
- return "yarn";
28416
- return "npm";
28417
- };
28418
- function runVerifyProject(root, dryRun = false) {
28419
- const cwd = repoRoot(root);
28420
- let passed = 0;
28421
- let failed = 0;
28422
- let skipped = 0;
28423
- const lines = [];
28424
- const stderrParts = [];
28425
- const runCheck = (label, cmd, args) => {
28426
- lines.push(`
28427
- ## ${label}
28428
-
28429
- `);
28430
- lines.push(`command: ${[cmd, ...args].join(" ")}
28431
-
28432
- `);
28433
- if (dryRun) {
28434
- lines.push(`status: skipped (dry run)
28435
- `);
28436
- skipped += 1;
28437
- return;
28438
- }
28439
- const r = spawnSync5(cmd, args, { cwd, encoding: "utf8" });
28440
- const so = (r.stdout ?? "").trimEnd();
28441
- if (so)
28442
- lines.push(so + `
28443
- `);
28444
- const se = (r.stderr ?? "").trimEnd();
28445
- if (se)
28446
- stderrParts.push(se);
28447
- if (r.status === 0) {
28448
- lines.push(`
28449
- status: pass
28450
- `);
28451
- passed += 1;
28452
- } else {
28453
- lines.push(`
28454
- status: fail (exit ${r.status})
28455
- `);
28456
- failed += 1;
28366
+ const config = loaded.config;
28367
+ const defaults = config.tokenDefaults ?? {};
28368
+ const name = String(defaults.name ?? tokenName);
28369
+ const desc = String(defaults.description ?? "OpenCode workit — /wk-issue-update and /wk-meetings");
28370
+ const scopes = Array.isArray(defaults.scopes) ? defaults.scopes : ["YouTrack"];
28371
+ const base = String(config.baseUrl ?? "https://enghouseamg.youtrack.cloud").replace(/\/+$/, "");
28372
+ const tokenFile = String(config.tokenFile ?? path18.join(path18.dirname(loaded.path), "youtrack.token"));
28373
+ const tab = String(defaults.profileTab ?? "account-security");
28374
+ const createUrl = `${base}/users/me?${new URLSearchParams({ tab })}`;
28375
+ const docsUrl = "https://www.jetbrains.com/help/youtrack/cloud/manage-permanent-token.html";
28376
+ return {
28377
+ data: {
28378
+ tokenName: name,
28379
+ tokenDescription: desc,
28380
+ scopes,
28381
+ tokenFile: path18.resolve(tokenFile),
28382
+ createUrl,
28383
+ docsUrl,
28384
+ prefillSupported: false,
28385
+ steps: [
28386
+ "Profile → Account Security → **New token** (or open createUrl)",
28387
+ `Name: **${name}**`,
28388
+ `Scope: **${scopes.join(", ")}** only — remove other services`,
28389
+ "**Create token** → copy immediately (shown once)",
28390
+ "Paste into token file → save → `/wk-status`"
28391
+ ]
28457
28392
  }
28458
28393
  };
28459
- const skipCheck = (label, reason) => {
28460
- lines.push(`
28461
- ## ${label}
28462
-
28463
- status: skipped (${reason})
28464
- `);
28465
- skipped += 1;
28466
- };
28467
- lines.push(`# Verification Context
28468
- `);
28469
- lines.push(`
28470
- root: ${cwd}
28471
- `);
28472
- lines.push(`dry_run: ${String(dryRun)}
28473
- `);
28474
- const pkgPath = path16.join(cwd, "package.json");
28475
- if (existsSync9(pkgPath)) {
28476
- const runner = packageRunner(cwd);
28477
- for (const script of ["lint", "format:check", "test", "build"]) {
28478
- if (hasScript(cwd, script)) {
28479
- if (runner === "npm")
28480
- runCheck(script, "npm", ["run", script]);
28481
- else
28482
- runCheck(script, runner, [script]);
28483
- } else {
28484
- skipCheck(script, `package.json has no ${script} script`);
28485
- }
28486
- }
28487
- } else {
28488
- skipCheck("javascript", "package.json not found");
28489
- }
28490
- const cargoManifest = existsSync9(path16.join(cwd, "Cargo.toml")) ? "Cargo.toml" : existsSync9(path16.join(cwd, "src-tauri/Cargo.toml")) ? "src-tauri/Cargo.toml" : "";
28491
- if (cargoManifest) {
28492
- runCheck("cargo fmt", "cargo", ["fmt", "--manifest-path", cargoManifest, "--", "--check"]);
28493
- runCheck("cargo clippy", "cargo", [
28494
- "clippy",
28495
- "--manifest-path",
28496
- cargoManifest,
28497
- "--all-targets",
28498
- "--",
28499
- "-D",
28500
- "warnings"
28501
- ]);
28502
- runCheck("cargo test", "cargo", ["test", "--manifest-path", cargoManifest, "--all-targets"]);
28503
- } else {
28504
- skipCheck("rust", "Cargo.toml not found");
28394
+ }
28395
+ function parseIssueRef(input) {
28396
+ const trimmed = String(input ?? "").trim();
28397
+ if (!trimmed)
28398
+ return { error: "empty issue reference" };
28399
+ if (ISSUE_RE.test(trimmed)) {
28400
+ return { issueId: trimmed, source: "id" };
28505
28401
  }
28506
- if (existsSync9(path16.join(cwd, "pyproject.toml")) || existsSync9(path16.join(cwd, "pytest.ini")) || existsSync9(path16.join(cwd, "tests"))) {
28507
- if (commandOnPath("pytest"))
28508
- runCheck("pytest", "pytest", []);
28509
- else
28510
- skipCheck("pytest", "pytest not available");
28511
- if (commandOnPath("ruff"))
28512
- runCheck("ruff check", "ruff", ["check", "."]);
28513
- else
28514
- skipCheck("ruff check", "ruff not available");
28402
+ const fromPath = trimmed.match(/\/(?:issue|issues)\/([A-Z]+-\d+)/i);
28403
+ if (fromPath && ISSUE_RE.test(fromPath[1])) {
28404
+ return { issueId: fromPath[1], source: "url" };
28515
28405
  }
28516
- lines.push(`
28517
- ## CHANGELOG.md format
28518
-
28519
- `);
28520
- lines.push(`command: grep -qi "## [Unreleased]" CHANGELOG.md
28521
-
28522
- `);
28523
- const changelogPath = path16.join(cwd, "CHANGELOG.md");
28524
- if (dryRun) {
28525
- lines.push(`status: skipped (dry run)
28526
- `);
28527
- skipped += 1;
28528
- } else if (existsSync9(changelogPath)) {
28529
- if (/## \[Unreleased\]/i.test(readFileSync8(changelogPath, "utf8"))) {
28530
- lines.push(`status: pass
28531
- `);
28532
- passed += 1;
28533
- } else {
28534
- lines.push(`status: fail (missing ## [Unreleased])
28535
- `);
28536
- failed += 1;
28406
+ const anywhere = trimmed.match(/([A-Z]+-\d+)/);
28407
+ if (anywhere && ISSUE_RE.test(anywhere[1])) {
28408
+ return { issueId: anywhere[1], source: "url" };
28409
+ }
28410
+ return { error: `could not parse issue id from: ${trimmed}` };
28411
+ }
28412
+ var contextScripts = {
28413
+ config: readYouTrackContextConfig,
28414
+ greeting: () => youTrackGreeting(youTrackReadOnlyConfigPath()),
28415
+ parseDuration: (text) => youTrackParseDuration(text),
28416
+ api: () => ({ error: "YouTrack context is read-only" })
28417
+ };
28418
+ function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
28419
+ const root = resolveWorkspaceRoot(workspace_root);
28420
+ for (const rel of [spec_path, plan_path].filter(Boolean)) {
28421
+ let full;
28422
+ try {
28423
+ full = resolveInside(root, rel);
28424
+ } catch {
28425
+ continue;
28537
28426
  }
28538
- } else {
28539
- lines.push(`status: fail (missing CHANGELOG.md)
28540
- `);
28541
- failed += 1;
28427
+ if (!fs8.existsSync(full))
28428
+ continue;
28429
+ const text = fs8.readFileSync(full, "utf8");
28430
+ const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
28431
+ if (m)
28432
+ return m[1];
28542
28433
  }
28543
- lines.push(`
28544
- # Summary
28545
-
28546
- `);
28547
- lines.push(`passed: ${passed}
28548
- `);
28549
- lines.push(`failed: ${failed}
28550
- `);
28551
- lines.push(`skipped: ${skipped}
28552
- `);
28553
- return {
28554
- stdout: lines.join(""),
28555
- stderr: stderrParts.join(`
28556
- `),
28557
- exitCode: failed > 0 ? 1 : 0,
28558
- cwd
28559
- };
28434
+ return null;
28560
28435
  }
28561
-
28562
- // packages/workit-core/src/core/init.ts
28563
- import fs9 from "node:fs";
28564
- import path21 from "node:path";
28565
- import { spawnSync as spawnSync7 } from "node:child_process";
28566
-
28567
- // packages/workit-core/src/core/scripts.ts
28568
- var PLUGIN_ROOT = packageRoot();
28569
- var ASSET_ROOT = assetRoot();
28570
- var resolveWorkspaceRoot = (explicit) => explicit || process.cwd();
28571
-
28572
- // packages/workit-core/src/core/safe-write.ts
28573
- import {
28574
- closeSync as closeSync2,
28575
- fchmodSync,
28576
- fsyncSync as fsyncSync2,
28577
- lstatSync as lstatSync4,
28578
- mkdirSync as mkdirSync4,
28579
- openSync as openSync2,
28580
- renameSync as renameSync2,
28581
- unlinkSync as unlinkSync3,
28582
- writeFileSync as writeFileSync3
28583
- } from "node:fs";
28584
- function writeFileExclusive(file, content, mode) {
28585
- try {
28586
- writeFileSync3(file, content, { encoding: "utf8", flag: "wx", mode });
28587
- return "created";
28588
- } catch (err) {
28589
- if (err.code === "EEXIST")
28590
- return "preserved";
28591
- throw err;
28436
+ function meetingOptionsFromConfig(cfg) {
28437
+ const base = (cfg.baseUrl || "").replace(/\/$/, "");
28438
+ if (cfg.meetingIssues && typeof cfg.meetingIssues === "object") {
28439
+ return Object.entries(cfg.meetingIssues).map(([key, item]) => ({
28440
+ key,
28441
+ issue: item.issue,
28442
+ label: item.label ?? item.issue,
28443
+ workItemText: item.workItemText ?? "Reuniones",
28444
+ url: item.url ?? (base && item.issue ? `${base}/issue/${item.issue}` : null)
28445
+ }));
28592
28446
  }
28447
+ const issue = cfg.meetingIssue;
28448
+ return [
28449
+ {
28450
+ key: "general",
28451
+ issue,
28452
+ label: "General meetings",
28453
+ workItemText: "Reuniones",
28454
+ url: base && issue ? `${base}/issue/${issue}` : null
28455
+ }
28456
+ ];
28593
28457
  }
28594
-
28595
- // packages/workit-core/src/core/setup.ts
28596
- import {
28597
- copyFileSync as copyFileSync2,
28598
- cpSync as cpSync2,
28599
- existsSync as existsSync12,
28600
- lstatSync as lstatSync5,
28601
- mkdirSync as mkdirSync5,
28602
- mkdtempSync,
28603
- readFileSync as readFileSync11,
28604
- readdirSync as readdirSync7,
28605
- realpathSync as realpathSync9,
28606
- renameSync as renameSync3,
28607
- rmSync,
28608
- statSync as statSync4,
28609
- writeFileSync as writeFileSync6
28610
- } from "node:fs";
28611
- import path19 from "node:path";
28612
- import { isDeepStrictEqual } from "node:util";
28613
-
28614
- // packages/workit-core/src/core/branch-policy.ts
28615
- import { spawnSync as spawnSync6 } from "node:child_process";
28616
- var PREFIXES = {
28617
- feature: "feature/*",
28618
- bugfix: "bugfix/*",
28619
- release: "release/*",
28620
- hotfix: "hotfix/*"
28621
- };
28622
- var detectBranchPolicy = (workspaceRoot) => {
28623
- const branchExists = (name) => {
28624
- const r = spawnSync6("git", ["branch", "--list", name], {
28625
- cwd: workspaceRoot,
28626
- encoding: "utf8"
28627
- });
28628
- return r.status === 0 && (r.stdout ?? "").trim() !== "";
28458
+ var readOnlyYouTrackConfig = (cfg) => {
28459
+ const publicUrl = (value) => {
28460
+ if (typeof value !== "string")
28461
+ return;
28462
+ try {
28463
+ const url = new URL(value);
28464
+ if (url.username || url.password)
28465
+ return `${url.protocol}//${url.host}${url.pathname}${url.search}${url.hash}`;
28466
+ return value;
28467
+ } catch {
28468
+ return;
28469
+ }
28629
28470
  };
28630
- const develop = branchExists("develop");
28631
- const main = branchExists("main");
28632
- const master = branchExists("master");
28633
- const root = main ? "main" : master ? "master" : null;
28634
- if (develop) {
28635
- return {
28636
- preset: "gitflow",
28637
- developBranch: "develop",
28638
- integration: "merge",
28639
- protected: root ? [root, "develop"] : ["develop"],
28640
- allowed: [...PRESETS.gitflow.allowed],
28641
- prefixes: PREFIXES
28642
- };
28471
+ const safe = {};
28472
+ const baseUrl = publicUrl(cfg.baseUrl);
28473
+ if (baseUrl)
28474
+ safe.baseUrl = baseUrl;
28475
+ for (const key of ["timezone", "greetingCutoff", "defaultMention", "meetingIssue"])
28476
+ if (typeof cfg[key] === "string")
28477
+ safe[key] = cfg[key];
28478
+ if (cfg.greetings && typeof cfg.greetings === "object" && !Array.isArray(cfg.greetings)) {
28479
+ const greetings = {};
28480
+ for (const key of ["morning", "afternoon"])
28481
+ if (typeof cfg.greetings[key] === "string")
28482
+ greetings[key] = cfg.greetings[key];
28483
+ if (Object.keys(greetings).length)
28484
+ safe.greetings = greetings;
28643
28485
  }
28644
- if (root === "main") {
28486
+ if (cfg.meetingIssues && typeof cfg.meetingIssues === "object" && !Array.isArray(cfg.meetingIssues)) {
28487
+ safe.meetingIssues = Object.fromEntries(Object.entries(cfg.meetingIssues).flatMap(([key, value]) => {
28488
+ if (!value || typeof value !== "object" || Array.isArray(value))
28489
+ return [];
28490
+ const item = value;
28491
+ const safeItem = {};
28492
+ for (const field of ["issue", "label", "workItemText"])
28493
+ if (typeof item[field] === "string")
28494
+ safeItem[field] = item[field];
28495
+ const url = publicUrl(item.url);
28496
+ if (url)
28497
+ safeItem.url = url;
28498
+ return [[key, safeItem]];
28499
+ }));
28500
+ }
28501
+ return safe;
28502
+ };
28503
+ function context({
28504
+ spec_path,
28505
+ plan_path,
28506
+ issue_id,
28507
+ issue_url,
28508
+ issue_ref,
28509
+ mode,
28510
+ workspace_root
28511
+ }, scripts = contextScripts) {
28512
+ const cfg = scripts.config();
28513
+ if (cfg.error)
28514
+ return { error: cfg.error };
28515
+ const greeting = scripts.greeting();
28516
+ if (greeting.exitCode !== 0) {
28517
+ return { error: (greeting.stderr || greeting.stdout || "greeting failed").trim() };
28518
+ }
28519
+ const safeConfig = readOnlyYouTrackConfig(cfg.data);
28520
+ const meetingOptions = meetingOptionsFromConfig(safeConfig);
28521
+ if (mode === "meetings" && !issue_id && !issue_url && !issue_ref) {
28645
28522
  return {
28646
- preset: "github-flow",
28647
- developBranch: null,
28648
- integration: "pr",
28649
- protected: [root],
28650
- allowed: ["*"],
28651
- prefixes: PREFIXES
28523
+ config: safeConfig,
28524
+ greeting: greeting.stdout.trim(),
28525
+ mode: "meetings",
28526
+ requiresMeetingChoice: true,
28527
+ meetingOptions,
28528
+ issueId: null
28652
28529
  };
28653
28530
  }
28654
- if (root === "master") {
28531
+ let issue = issue_id;
28532
+ if (!issue && (issue_url || issue_ref)) {
28533
+ const parsed = parseIssueRef(issue_url ?? issue_ref);
28534
+ if ("error" in parsed)
28535
+ return { error: parsed.error };
28536
+ issue = parsed.issueId;
28537
+ }
28538
+ if (!issue && mode === "meetings")
28539
+ issue = meetingOptions[0]?.issue ?? safeConfig.meetingIssue;
28540
+ if (!issue)
28541
+ issue = resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) ?? undefined;
28542
+ if (!issue || !ISSUE_RE.test(issue)) {
28655
28543
  return {
28656
- preset: "trunk-based",
28657
- developBranch: null,
28658
- integration: "pr",
28659
- protected: [root],
28660
- allowed: ["*"],
28661
- prefixes: PREFIXES
28544
+ error: "invalid or missing issue id — pass issue_url, issue_id, or spec/plan with **YouTrack:**",
28545
+ requiresIssueInput: true
28662
28546
  };
28663
28547
  }
28548
+ const base = (safeConfig.baseUrl || "").replace(/\/$/, "");
28549
+ const issueUrl = base ? `${base}/issue/${issue}` : null;
28550
+ const selectedMeeting = meetingOptions.find((m) => m.issue === issue);
28664
28551
  return {
28665
- preset: "gitflow",
28666
- developBranch: null,
28667
- integration: "merge",
28668
- protected: [],
28669
- allowed: [],
28670
- prefixes: PREFIXES
28552
+ config: safeConfig,
28553
+ greeting: greeting.stdout.trim(),
28554
+ issueId: issue,
28555
+ issueUrl,
28556
+ mode: mode ?? (selectedMeeting ? "meetings" : "task"),
28557
+ meetingOptions,
28558
+ workItemText: selectedMeeting?.workItemText ?? null
28671
28559
  };
28672
- };
28673
-
28674
- // packages/workit-core/src/core/gitignore.ts
28675
- import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
28676
- import path17 from "node:path";
28677
- var GITIGNORE_ENTRIES = [
28678
- "# workit: SDD working state (never commit)",
28679
- "docs/*/sdd/",
28680
- "",
28681
- "# OS / editor cruft",
28682
- ".DS_Store",
28683
- "Thumbs.db",
28684
- "*.swp",
28685
- ".idea/",
28686
- ".vscode/",
28687
- ".env",
28688
- "node_modules/",
28689
- "dist/",
28690
- "*.log",
28691
- ".cache/"
28692
- ];
28693
- var ensureProjectGitignore = (workspaceRoot, confirmed) => {
28694
- if (!confirmed)
28695
- return { ok: false, error: "confirmed: true required" };
28696
- const file = path17.join(workspaceRoot, ".gitignore");
28697
- const existing = existsSync10(file) ? readFileSync9(file, "utf8") : "";
28698
- const existingLines = new Set(existing.split(`
28699
- `).map((l) => l.trim()).filter(Boolean));
28700
- const added = [];
28701
- const append = [];
28702
- for (const entry of GITIGNORE_ENTRIES) {
28703
- if (entry.trim() === "" || existingLines.has(entry.trim()))
28704
- continue;
28705
- append.push(entry);
28706
- added.push(entry);
28707
- }
28708
- if (append.length) {
28709
- const separator = existing && !existing.endsWith(`
28710
- `) ? `
28711
- ` : "";
28712
- writeFileSync4(file, existing + separator + (existing ? `
28713
- ` : "") + append.join(`
28714
- `) + `
28715
- `, "utf8");
28716
- } else if (!existsSync10(file)) {
28717
- writeFileSync4(file, "", "utf8");
28560
+ }
28561
+ var fetchYouTrackIssueBody = async (issue, creds = youTrackToken, request = youTrackRequest) => {
28562
+ const credentials = creds();
28563
+ if ("error" in credentials)
28564
+ return { error: credentials.error, kind: "creds" };
28565
+ const { token, base } = credentials;
28566
+ const out = await request(`${base}/api/issues/${encodeURIComponent(issue)}?fields=idReadable,summary,description,customFields(name,value(name))`, { method: "GET", token });
28567
+ if (out.status !== 0)
28568
+ return { error: out.stderr || "YouTrack issue fetch failed", kind: "request" };
28569
+ try {
28570
+ const parsed = JSON.parse(out.stdout);
28571
+ const customFields = Array.isArray(parsed.customFields) ? parsed.customFields : [];
28572
+ const stateField = customFields.find((field) => typeof field === "object" && field !== null && field.name === "State");
28573
+ const stateValue = stateField?.value;
28574
+ const stateName = typeof stateValue === "object" && stateValue !== null ? stateValue.name : undefined;
28575
+ return {
28576
+ data: {
28577
+ idReadable: String(parsed.idReadable ?? issue),
28578
+ summary: String(parsed.summary ?? ""),
28579
+ description: typeof parsed.description === "string" ? parsed.description : null,
28580
+ state: typeof stateValue === "string" ? stateValue : typeof stateName === "string" && stateName ? stateName : null
28581
+ }
28582
+ };
28583
+ } catch {
28584
+ return { error: "invalid JSON from YouTrack API", kind: "request" };
28718
28585
  }
28719
- return { ok: true, path: file, added };
28720
28586
  };
28721
28587
 
28722
- // packages/workit-core/src/core/hygiene.ts
28723
- import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "node:fs";
28724
- import path18 from "node:path";
28725
- var repoRoot2 = assetRoot();
28726
- var templatesDir = () => path18.join(repoRoot2, "templates", "hygiene");
28727
- var packageJson = (root) => {
28728
- if (!existsSync11(path18.join(root, "package.json")))
28729
- return null;
28588
+ // packages/workit-core/src/core/init.ts
28589
+ var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE";
28590
+ var readJson = (p) => {
28730
28591
  try {
28731
- return JSON.parse(readFileSync10(path18.join(root, "package.json"), "utf8"));
28592
+ const parsed = JSON.parse(fs9.readFileSync(p, "utf8"));
28593
+ return isConfigObject(parsed) ? parsed : null;
28732
28594
  } catch {
28733
28595
  return null;
28734
28596
  }
28735
28597
  };
28736
- var isOpenSource = (root) => {
28737
- if (existsSync11(path18.join(root, "LICENSE")))
28738
- return true;
28739
- const pkg = packageJson(root);
28740
- if (pkg && !pkg.private)
28741
- return true;
28742
- return path18.basename(root) === "workflow-toolkit";
28743
- };
28744
- var licenseHolder = (root) => {
28745
- const pkg = packageJson(root);
28746
- if (pkg) {
28747
- const author = pkg.author;
28748
- if (typeof author === "string" && author.trim())
28749
- return author.trim();
28750
- if (author && typeof author === "object") {
28751
- const name = author.name;
28752
- if (typeof name === "string" && name.trim())
28753
- return name.trim();
28598
+ var resolveInitProvider = (cwd) => {
28599
+ const env = process.env.WORKFLOW_VCS_PROVIDER?.trim();
28600
+ if (env)
28601
+ return env.toLowerCase();
28602
+ const root = cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
28603
+ try {
28604
+ const out = spawnSync5("git", ["remote", "get-url", "origin"], {
28605
+ cwd: root,
28606
+ encoding: "utf8"
28607
+ });
28608
+ if (out.status === 0) {
28609
+ const url = (out.stdout ?? "").trim();
28610
+ if (/github\.com[:/]/.test(url))
28611
+ return "github";
28612
+ if (/gitlab\.com[:/]/.test(url))
28613
+ return "gitlab";
28754
28614
  }
28755
- }
28756
- return "";
28757
- };
28758
- var planHygieneFiles = (root, opts = {}) => {
28759
- const openSource = opts.includeOpenSource ?? isOpenSource(root);
28760
- const files = ["CHANGELOG.md", "README.md", ".editorconfig", ".gitattributes"];
28761
- if (openSource)
28762
- files.push("LICENSE", "CONTRIBUTING.md");
28763
- const planned = [];
28764
- for (const file of files) {
28765
- if (existsSync11(path18.join(root, file)))
28766
- continue;
28767
- const tpl = path18.join(templatesDir(), file);
28768
- if (!existsSync11(tpl))
28769
- continue;
28770
- const content = readFileSync10(tpl, "utf8").replace(/<PROJECT>/g, path18.basename(root)).replace(/<YEAR>/g, String(new Date().getFullYear())).replace(/<HOLDER>\s*/g, licenseHolder(root));
28771
- planned.push({ path: path18.join(root, file), content });
28772
- }
28773
- return planned;
28615
+ } catch {}
28616
+ return null;
28774
28617
  };
28775
- var ensureHygieneFiles = (root, opts) => {
28776
- if (!opts.confirmed)
28777
- return { ok: false, error: "confirmed: true required" };
28778
- const created = [];
28779
- for (const planned of planHygieneFiles(root, opts)) {
28780
- writeFileSync5(planned.path, planned.content, "utf8");
28781
- created.push(path18.basename(planned.path));
28618
+ var youtrackJsonContent = (dir) => ({
28619
+ baseUrl: process.env.WORKFLOW_YT_BASE_URL ?? "https://enghouseamg.youtrack.cloud",
28620
+ tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path19.join(dir, "youtrack.token"),
28621
+ timezone: process.env.WORKFLOW_YT_TIMEZONE ?? "America/Santiago",
28622
+ locale: "es-CL",
28623
+ defaultMention: process.env.WORKFLOW_YT_MENTION ?? "Alejandra.Flores",
28624
+ greetings: { morning: "buenos días", afternoon: "buenas tardes" },
28625
+ greetingCutoff: "12:00",
28626
+ meetingIssue: process.env.WORKFLOW_YT_MEETING_ISSUE ?? "IRPT-12",
28627
+ meetingIssues: {
28628
+ general: {
28629
+ issue: process.env.WORKFLOW_YT_MEETING_ISSUE ?? "IRPT-12",
28630
+ label: "General meetings (Reuniones internas Team IRP)",
28631
+ workItemText: "Reuniones"
28632
+ },
28633
+ web: {
28634
+ issue: process.env.WORKFLOW_YT_WEB_MEETING_ISSUE ?? "NSXFT-21",
28635
+ label: "Web meetings",
28636
+ workItemText: "Reuniones web",
28637
+ url: "https://enghouseamg.youtrack.cloud/projects/NSXFT/issues/NSXFT-21"
28638
+ }
28639
+ },
28640
+ commentHeader: "# Actualización",
28641
+ attachmentsHeaderImages: "## Adjunto capturas",
28642
+ attachmentsHeaderFiles: "## Archivos adjuntos",
28643
+ attachmentsHeaderMixed: "## Adjuntos",
28644
+ tokenDefaults: {
28645
+ name: "workit",
28646
+ description: "OpenCode workit — /wk-issue-update and /wk-meetings",
28647
+ scopes: ["YouTrack"],
28648
+ profileTab: "account-security"
28782
28649
  }
28783
- return { ok: true, created };
28784
- };
28785
-
28786
- // packages/workit-core/src/core/registration.ts
28787
- var CURSOR_RUNTIME_PACKAGE = "@brainervirus/workit-cursor@latest";
28788
- var CURSOR_HOOK_RUN_COMMAND = `npx -y --prefer-online --min-release-age=0 --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-hook`;
28789
- function cursorHooksEntry(_packageDir) {
28650
+ });
28651
+ var vcsJsonContent = () => {
28652
+ const provider = resolveInitProvider();
28790
28653
  return {
28791
- command: `npx -y --prefer-online --min-release-age=0 --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-session-start`,
28792
- args: []
28654
+ ...provider ? { provider } : {},
28655
+ defaultTargetBranch: process.env.WORKFLOW_VCS_TARGET_BRANCH ?? "develop",
28656
+ gitlab: {
28657
+ host: process.env.WORKFLOW_GITLAB_HOST ?? "gitlab.com",
28658
+ apiUrl: process.env.WORKFLOW_GITLAB_API_URL ?? "https://gitlab.com/api/v4"
28659
+ },
28660
+ github: {
28661
+ host: process.env.WORKFLOW_GITHUB_HOST ?? "github.com"
28662
+ },
28663
+ pr: { squashOnMerge: true, removeSourceBranch: true, pushBranch: true, confirmSkip: true }
28793
28664
  };
28794
- }
28795
-
28796
- // packages/workit-core/src/core/doctor.ts
28797
- var canonicalCursorHook = cursorHooksEntry("").command;
28798
- var INSTALLER_REQUIRED = new Set([
28799
- "runtime",
28800
- "assets",
28801
- "launcher",
28802
- "utility",
28803
- "stale_pin",
28804
- "stale_install",
28805
- "duplicate_registration",
28806
- "malformed_config"
28807
- ]);
28808
- // packages/workit-core/src/core/setup.ts
28809
- var YT_OVERRIDES = [
28810
- { envKey: "WORKFLOW_YT_BASE_URL", affects: "youtrack.json baseUrl" },
28811
- { envKey: "WORKFLOW_YT_TOKEN_FILE", affects: "youtrack.json tokenFile" },
28812
- { envKey: "WORKFLOW_YT_TIMEZONE", affects: "youtrack.json timezone" },
28813
- { envKey: "WORKFLOW_YT_MENTION", affects: "youtrack.json defaultMention" },
28814
- { envKey: "WORKFLOW_YT_MEETING_ISSUE", affects: "youtrack.json meetingIssue" },
28815
- { envKey: "WORKFLOW_YT_WEB_MEETING_ISSUE", affects: "youtrack.json web meeting issue" }
28816
- ];
28817
- var VCS_OVERRIDES = [
28818
- { envKey: "WORKFLOW_VCS_PROVIDER", affects: "vcs.json provider" },
28819
- { envKey: "WORKFLOW_VCS_TARGET_BRANCH", affects: "vcs.json defaultTargetBranch" },
28820
- { envKey: "WORKFLOW_GITLAB_HOST", affects: "vcs.json gitlab.host" },
28821
- { envKey: "WORKFLOW_GITLAB_API_URL", affects: "vcs.json gitlab.apiUrl" },
28822
- { envKey: "WORKFLOW_GITHUB_HOST", affects: "vcs.json github.host" }
28823
- ];
28824
- var SETUP_OVERRIDES = [...YT_OVERRIDES, ...VCS_OVERRIDES];
28825
- var readFileSafe = (p) => {
28826
- try {
28827
- return readFileSync11(p, "utf8");
28828
- } catch {
28829
- return null;
28830
- }
28831
28665
  };
28832
- function applyWorkspaceBranchPolicy(opts) {
28833
- const { workspace_root, env = process.env } = opts;
28834
- const dir = path19.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
28835
- const {
28836
- status,
28837
- path: wsPath,
28838
- entries,
28839
- document,
28840
- revision,
28841
- error: workspacesError
28842
- } = readWorkspacesResult(dir);
28843
- if (status === "malformed" || status === "invalid")
28844
- return { ok: false, error: workspacesError ?? `invalid workspaces.json: ${wsPath}` };
28845
- const detection = detectBranchPolicy(workspace_root);
28846
- const name = String(env.WORKFLOW_BP_NAME ?? path19.basename(workspace_root));
28847
- const integration = env.WORKFLOW_BP_INTEGRATION ?? detection.integration;
28848
- const policy = {
28849
- preset: detection.preset,
28850
- developBranch: env.WORKFLOW_BP_DEVELOP ?? detection.developBranch ?? undefined,
28851
- prefixes: detection.prefixes,
28852
- allowed: detection.allowed,
28853
- protected: detection.protected,
28854
- integration
28855
- };
28856
- const glob = `${workspace_root.replace(/[\\/]+$/, "")}/**`;
28857
- if (!validateWorkspaceGlob(glob).ok)
28858
- return { ok: false, error: `invalid workspace glob: ${glob}` };
28859
- let existing;
28860
- try {
28861
- existing = resolveWorkspaceFrom(workspace_root, dir);
28862
- } catch (error) {
28863
- const selectedName = env.WORKFLOW_BP_NAME?.trim();
28864
- if (!selectedName)
28865
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
28866
- try {
28867
- existing = resolveWorkspaceFrom(workspace_root, dir, selectedName);
28868
- } catch (selectionError) {
28869
- return {
28870
- ok: false,
28871
- error: selectionError instanceof Error ? selectionError.message : String(selectionError)
28872
- };
28873
- }
28874
- }
28875
- if (existing?.branchPolicy && isDeepStrictEqual(existing.branchPolicy, policy)) {
28876
- return {
28877
- ok: true,
28878
- status: "already-configured",
28879
- workspace: existing,
28880
- policy,
28881
- config_path: wsPath
28882
- };
28883
- }
28884
- const next = existing ? entries.map((w) => w.name === existing?.name ? { ...w, branchPolicy: policy } : w) : [...entries, { name, glob, branchPolicy: policy }];
28885
- if (!existing && entries.some((entry) => entry.name === name))
28886
- return { ok: false, error: `workspace name ${JSON.stringify(name)} is already configured` };
28887
- const proposed = validateWorkspacesDocument({ ...document, workspaces: next }, wsPath);
28888
- if (proposed.status !== "valid")
28889
- return { ok: false, error: proposed.error };
28890
- if (workspacesRevision(readFileSafe(wsPath)) !== revision)
28891
- return {
28892
- ok: false,
28893
- error: `workspaces.json changed while applying the branch policy: ${wsPath}`
28894
- };
28895
- mkdirSync5(path19.dirname(wsPath), { recursive: true });
28896
- const tmp = `${wsPath}.${process.pid}.tmp`;
28897
- try {
28898
- writeFileSync6(tmp, JSON.stringify({ ...document, workspaces: next }, null, 2) + `
28666
+ function initApplyData(action, env = process.env) {
28667
+ const dir = String(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
28668
+ fs9.mkdirSync(dir, { recursive: true });
28669
+ switch (action) {
28670
+ case "youtrack_json": {
28671
+ const out = path19.join(dir, "youtrack.json");
28672
+ fs9.writeFileSync(out, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
28899
28673
  `, "utf8");
28900
- if (workspacesRevision(readFileSafe(wsPath)) !== revision) {
28901
- rmSync(tmp, { force: true });
28674
+ return { action, ok: true, path: out };
28675
+ }
28676
+ case "youtrack_token_placeholder": {
28677
+ const p = path19.join(dir, "youtrack.token");
28678
+ const preserved = writeFileExclusive(p, TOKEN_PLACEHOLDER2 + `
28679
+ `, 384) === "preserved";
28680
+ const abs = path19.resolve(p);
28902
28681
  return {
28903
- ok: false,
28904
- error: `workspaces.json changed while applying the branch policy: ${wsPath}`
28682
+ action,
28683
+ ok: true,
28684
+ path: abs,
28685
+ token_edit_path: abs,
28686
+ placeholder: TOKEN_PLACEHOLDER2,
28687
+ preserved,
28688
+ instruction: `Open ${abs} in your editor, replace YOUR_TOKEN_HERE with your YouTrack permanent token, save, then run /wk-status`
28905
28689
  };
28906
28690
  }
28907
- renameSync3(tmp, wsPath);
28908
- } catch (error) {
28909
- try {
28910
- rmSync(tmp, { force: true });
28911
- } catch {}
28912
- return {
28913
- ok: false,
28914
- error: `failed to write ${wsPath}: ${error instanceof Error ? error.message : String(error)}`
28915
- };
28916
- }
28917
- return {
28918
- ok: true,
28919
- status: existing ? "updated" : "configured",
28920
- workspace: existing ? { ...existing, branchPolicy: policy } : { name, glob, branchPolicy: policy },
28921
- policy,
28922
- config_path: wsPath
28923
- };
28924
- }
28925
-
28926
- // packages/workit-core/src/core/youtrack.ts
28927
- import fs8 from "node:fs";
28928
- import path20 from "node:path";
28929
-
28930
- // packages/workit-core/src/core/templates.ts
28931
- var repoRoot3 = assetRoot();
28932
-
28933
- // packages/workit-core/src/core/youtrack.ts
28934
- var ISSUE_RE = /^[A-Z]+-\d+$/;
28935
- var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
28936
- var youTrackConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path20.join(configDir(), "youtrack.json");
28937
- var youTrackReadOnlyConfigPath = () => process.env.WORKFLOW_YOUTRACK_CONFIG ?? path20.join(resolveConfigDir(), "youtrack.json");
28938
- var youTrackTokenModeOk = (p) => {
28939
- if (process.platform === "win32")
28940
- return true;
28941
- const mode = fs8.statSync(p).mode & 511;
28942
- return mode === 384;
28943
- };
28944
- function readYouTrackConfig(required) {
28945
- const cfgPath = youTrackConfigPath();
28946
- if (!fs8.existsSync(cfgPath)) {
28947
- return required ? { error: "ERROR: missing youtrack.json", path: cfgPath } : { config: {}, path: cfgPath };
28948
- }
28949
- let parsed;
28950
- try {
28951
- parsed = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
28952
- } catch {
28953
- return { error: `${cfgPath} is not valid JSON`, path: cfgPath };
28954
- }
28955
- if (!isConfigObject(parsed)) {
28956
- return { error: `${cfgPath} is not a JSON object`, path: cfgPath };
28957
- }
28958
- return { config: parsed, path: cfgPath };
28959
- }
28960
- var readYouTrackContextConfig = () => {
28961
- const cfgPath = youTrackReadOnlyConfigPath();
28962
- if (!fs8.existsSync(cfgPath))
28963
- return { data: {} };
28964
- let parsed;
28965
- try {
28966
- parsed = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
28967
- } catch {
28968
- return { error: "YouTrack configuration is unavailable" };
28969
- }
28970
- if (!isConfigObject(parsed))
28971
- return { error: "YouTrack configuration is unavailable" };
28972
- return { data: readOnlyYouTrackConfig(parsed) };
28973
- };
28974
- function tzParts(date, tz) {
28975
- const parts = new Intl.DateTimeFormat("en-GB", {
28976
- timeZone: tz,
28977
- year: "numeric",
28978
- month: "2-digit",
28979
- day: "2-digit",
28980
- hour: "2-digit",
28981
- minute: "2-digit",
28982
- hourCycle: "h23"
28983
- }).formatToParts(date);
28984
- const map = Object.fromEntries(parts.map((p) => [p.type, p.value]));
28985
- return { y: map.year, m: map.month, d: map.day, hour: map.hour, minute: map.minute };
28986
- }
28987
- function youTrackGreeting(configOverride) {
28988
- const cfgPath = configOverride ?? youTrackConfigPath();
28989
- let parsed;
28990
- try {
28991
- parsed = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
28992
- } catch {
28993
- return {
28994
- stdout: "",
28995
- exitCode: 1,
28996
- stderr: fs8.existsSync(cfgPath) ? `${cfgPath} is not valid JSON` : `missing youtrack.json: ${cfgPath}`
28997
- };
28998
- }
28999
- if (!isConfigObject(parsed)) {
29000
- return { stdout: "", exitCode: 1, stderr: `${cfgPath} is not a JSON object` };
29001
- }
29002
- const config = parsed;
29003
- try {
29004
- const tz = String(config.timezone ?? "America/Santiago");
29005
- const now = new Date;
29006
- const { y, m, d, hour, minute } = tzParts(now, tz);
29007
- const cutoff = String(config.greetingCutoff ?? "12:00").split(":");
29008
- const cutoffHour = Number(cutoff[0]);
29009
- const cutoffMinute = Number(cutoff[1] ?? 0);
29010
- const greetings = config.greetings ?? {};
29011
- const isMorning = Number(hour) < cutoffHour || Number(hour) === cutoffHour && Number(minute) < cutoffMinute;
29012
- const greeting = isMorning ? greetings.morning ?? "buenos días" : greetings.afternoon ?? "buenas tardes";
29013
- const mention = String(config.defaultMention ?? "Alejandra.Flores");
29014
- return { stdout: `@${mention} Hola, ${greeting}.
29015
- `, exitCode: 0, stderr: "" };
29016
- } catch (err) {
29017
- return {
29018
- stdout: "",
29019
- exitCode: 1,
29020
- stderr: err instanceof Error ? err.message : "greeting failed"
29021
- };
29022
- }
29023
- }
29024
- function youTrackParseDuration(text) {
29025
- const lower = String(text).toLowerCase().trim();
29026
- let total = 0;
29027
- for (const match of lower.matchAll(/(\d+)\s*h/g))
29028
- total += Number(match[1]) * 60;
29029
- for (const match of lower.matchAll(/(\d+)\s*m/g))
29030
- total += Number(match[1]);
29031
- if (total === 0 && /^\d+$/.test(lower))
29032
- total = Number(lower);
29033
- if (total <= 0)
29034
- return { error: "could not parse duration" };
29035
- return { data: { minutes: total, text: String(text).trim() } };
29036
- }
29037
- var youTrackToken = () => {
29038
- const loaded = readYouTrackConfig(true);
29039
- if ("error" in loaded)
29040
- return loaded;
29041
- const tokenFile = String(loaded.config.tokenFile ?? "");
29042
- const tokenPath = tokenFile ? path20.isAbsolute(tokenFile) ? path20.resolve(tokenFile) : path20.resolve(process.cwd(), tokenFile) : "";
29043
- if (!tokenPath || !fs8.existsSync(tokenPath))
29044
- return { error: "missing youtrack.token" };
29045
- if (!youTrackTokenModeOk(tokenPath))
29046
- return { error: "youtrack.token mode must be 0600" };
29047
- const token = fs8.readFileSync(tokenPath, "utf8").trim();
29048
- if (!token)
29049
- return { error: "empty token file" };
29050
- if (token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER)) {
29051
- return {
29052
- error: "token file still has placeholder YOUR_TOKEN_HERE — edit the file locally, then run /wk-status"
29053
- };
29054
- }
29055
- const base = String(loaded.config.baseUrl ?? "").replace(/\/+$/, "");
29056
- if (!base)
29057
- return { error: "baseUrl missing in config" };
29058
- return { token, base };
29059
- };
29060
- async function youTrackRequest(url, init) {
29061
- const headers = {
29062
- Authorization: `Bearer ${init.token}`,
29063
- Accept: "application/json"
29064
- };
29065
- let body;
29066
- if (init.body !== undefined) {
29067
- headers["Content-Type"] = "application/json";
29068
- body = JSON.stringify(init.body);
29069
- }
29070
- try {
29071
- const res = await fetch(url, { method: init.method, headers, body });
29072
- const text = await res.text();
29073
- if (!res.ok) {
29074
- return { status: res.status, stdout: "", stderr: text.slice(0, 200) };
28691
+ case "youtrack_scaffold": {
28692
+ const jsonOut = path19.join(dir, "youtrack.json");
28693
+ const tokenOut = path19.join(dir, "youtrack.token");
28694
+ fs9.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
28695
+ `, "utf8");
28696
+ const preserved = writeFileExclusive(tokenOut, TOKEN_PLACEHOLDER2 + `
28697
+ `, 384) === "preserved";
28698
+ const configPath = path19.resolve(jsonOut);
28699
+ const tokenPath = path19.resolve(tokenOut);
28700
+ const prev = process.env.WORKFLOW_YOUTRACK_CONFIG;
28701
+ process.env.WORKFLOW_YOUTRACK_CONFIG = configPath;
28702
+ let tokenCreate = {};
28703
+ try {
28704
+ const cfg = readJson(configPath) ?? {};
28705
+ const base = String(cfg.baseUrl ?? "").replace(/\/+$/, "");
28706
+ const meeting = String(cfg.meetingIssue ?? "");
28707
+ tokenCreate = youTrackTokenCreateUrl().data;
28708
+ return {
28709
+ action,
28710
+ ok: true,
28711
+ youtrack_json: configPath,
28712
+ youtrack_token: tokenPath,
28713
+ token_edit_path: tokenPath,
28714
+ token_create_url: tokenCreate.createUrl,
28715
+ token_create: tokenCreate,
28716
+ config_edit_path: configPath,
28717
+ placeholder: TOKEN_PLACEHOLDER2,
28718
+ preserved,
28719
+ youtrack_config: {
28720
+ config_edit_path: configPath,
28721
+ baseUrl: base,
28722
+ meetingIssue: meeting,
28723
+ meetingIssueUrl: base && meeting ? `${base}/issue/${meeting}` : null,
28724
+ defaultMention: cfg.defaultMention,
28725
+ timezone: cfg.timezone,
28726
+ locale: cfg.locale,
28727
+ tokenCreate,
28728
+ timeLogging: {
28729
+ meetings: {
28730
+ issue: meeting,
28731
+ skill: "/wk-meetings",
28732
+ logsTime: true,
28733
+ postsComment: false
28734
+ },
28735
+ taskWork: {
28736
+ issueSource: "active spec/plan **YouTrack:** field or --issue",
28737
+ skill: "/wk-issue-update",
28738
+ logsTime: true,
28739
+ postsComment: true
28740
+ }
28741
+ }
28742
+ },
28743
+ instruction: `Open the create-token URL, New token → name workit, scope YouTrack, paste into ${tokenPath}, then /wk-status.`
28744
+ };
28745
+ } finally {
28746
+ if (prev === undefined)
28747
+ delete process.env.WORKFLOW_YOUTRACK_CONFIG;
28748
+ else
28749
+ process.env.WORKFLOW_YOUTRACK_CONFIG = prev;
28750
+ }
29075
28751
  }
29076
- return { status: 0, stdout: text, stderr: "" };
29077
- } catch (err) {
29078
- return { status: 1, stdout: "", stderr: err instanceof Error ? err.message : "network error" };
29079
- }
29080
- }
29081
- async function youTrackVerifyToken() {
29082
- const cfgPath = youTrackConfigPath();
29083
- if (!fs8.existsSync(cfgPath))
29084
- return { error: "missing youtrack.json" };
29085
- const creds = youTrackToken();
29086
- if ("error" in creds)
29087
- return { error: creds.error };
29088
- const { token, base } = creds;
29089
- const me = await youTrackRequest(`${base}/api/users/me?fields=id,login,name,email`, {
29090
- method: "GET",
29091
- token
29092
- });
29093
- if (me.status !== 0) {
29094
- const err = me.status === 401 || me.status === 403 ? "authentication failed (401/403)" : `HTTP error: ${me.stderr.slice(0, 200)}`;
29095
- return { error: err, http_status: me.status };
29096
- }
29097
- let user;
29098
- try {
29099
- user = JSON.parse(me.stdout);
29100
- } catch {
29101
- return { error: "invalid JSON from YouTrack /api/users/me" };
29102
- }
29103
- const result = {
29104
- ok: true,
29105
- method: "GET /api/users/me",
29106
- baseUrl: base,
29107
- login: user.login,
29108
- name: user.name,
29109
- email: user.email,
29110
- id: user.id
29111
- };
29112
- const meeting = readYouTrackConfig(false);
29113
- const meetingIssue = "config" in meeting ? meeting.config.meetingIssue : undefined;
29114
- if (meetingIssue) {
29115
- const issue = await youTrackRequest(`${base}/api/issues/${meetingIssue}?fields=id,idReadable,summary`, { method: "GET", token });
29116
- if (issue.status === 0) {
28752
+ case "vcs_scaffold": {
28753
+ const jsonOut = path19.join(dir, "vcs.json");
28754
+ fs9.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(), null, 2) + `
28755
+ `, "utf8");
28756
+ const configPath = path19.resolve(jsonOut);
28757
+ const prev = process.env.WORKFLOW_VCS_CONFIG;
28758
+ process.env.WORKFLOW_VCS_CONFIG = configPath;
29117
28759
  try {
29118
- const parsed = JSON.parse(issue.stdout);
29119
- result.meetingIssue = meetingIssue;
29120
- result.meetingIssueReadable = true;
29121
- result.meetingIssueSummary = parsed.summary;
29122
- } catch {}
29123
- } else {
29124
- result.meetingIssue = meetingIssue;
29125
- result.meetingIssueReadable = false;
29126
- result.warning = `token valid but cannot read issue ${meetingIssue}`;
28760
+ const cfg = readJson(configPath) ?? {};
28761
+ const rawProvider = cfg.provider;
28762
+ const provider = typeof rawProvider === "string" && rawProvider.trim() ? rawProvider : null;
28763
+ return {
28764
+ action,
28765
+ ok: true,
28766
+ vcs_json: configPath,
28767
+ config_edit_path: configPath,
28768
+ vcs_config: {
28769
+ config_edit_path: configPath,
28770
+ provider,
28771
+ defaultTargetBranch: cfg.defaultTargetBranch,
28772
+ pr: cfg.pr,
28773
+ switchHint: 'Change "provider" to "github" when you migrate; authenticate with gh/glab',
28774
+ skill: "/wk-pr"
28775
+ },
28776
+ instruction: provider === null ? `No provider resolved — set provider to gitlab or github in ${configPath}, then /wk-status.` : `Run ${provider === "gitlab" ? "glab" : "gh"} auth login, then /wk-status.`
28777
+ };
28778
+ } finally {
28779
+ if (prev === undefined)
28780
+ delete process.env.WORKFLOW_VCS_CONFIG;
28781
+ else
28782
+ process.env.WORKFLOW_VCS_CONFIG = prev;
28783
+ }
29127
28784
  }
29128
- }
29129
- return { data: result };
29130
- }
29131
- function youTrackTokenCreateUrl() {
29132
- const tokenName = process.env.WORKFLOW_YT_TOKEN_NAME ?? "workit";
29133
- const loaded = readYouTrackConfig(false);
29134
- if ("error" in loaded) {
29135
- return { data: { error: loaded.error } };
29136
- }
29137
- const config = loaded.config;
29138
- const defaults = config.tokenDefaults ?? {};
29139
- const name = String(defaults.name ?? tokenName);
29140
- const desc = String(defaults.description ?? "OpenCode workit — /wk-issue-update and /wk-meetings");
29141
- const scopes = Array.isArray(defaults.scopes) ? defaults.scopes : ["YouTrack"];
29142
- const base = String(config.baseUrl ?? "https://enghouseamg.youtrack.cloud").replace(/\/+$/, "");
29143
- const tokenFile = String(config.tokenFile ?? path20.join(path20.dirname(loaded.path), "youtrack.token"));
29144
- const tab = String(defaults.profileTab ?? "account-security");
29145
- const createUrl = `${base}/users/me?${new URLSearchParams({ tab })}`;
29146
- const docsUrl = "https://www.jetbrains.com/help/youtrack/cloud/manage-permanent-token.html";
29147
- return {
29148
- data: {
29149
- tokenName: name,
29150
- tokenDescription: desc,
29151
- scopes,
29152
- tokenFile: path20.resolve(tokenFile),
29153
- createUrl,
29154
- docsUrl,
29155
- prefillSupported: false,
29156
- steps: [
29157
- "Profile → Account Security → **New token** (or open createUrl)",
29158
- `Name: **${name}**`,
29159
- `Scope: **${scopes.join(", ")}** only — remove other services`,
29160
- "**Create token** → copy immediately (shown once)",
29161
- "Paste into token file → save → `/wk-status`"
29162
- ]
28785
+ case "branch_policy": {
28786
+ const root = env.WORKFLOW_WORKSPACE_ROOT?.trim() ? env.WORKFLOW_WORKSPACE_ROOT : process.cwd();
28787
+ return applyWorkspaceBranchPolicy({ workspace_root: root, env });
29163
28788
  }
29164
- };
29165
- }
29166
- function parseIssueRef(input) {
29167
- const trimmed = String(input ?? "").trim();
29168
- if (!trimmed)
29169
- return { error: "empty issue reference" };
29170
- if (ISSUE_RE.test(trimmed)) {
29171
- return { issueId: trimmed, source: "id" };
29172
- }
29173
- const fromPath = trimmed.match(/\/(?:issue|issues)\/([A-Z]+-\d+)/i);
29174
- if (fromPath && ISSUE_RE.test(fromPath[1])) {
29175
- return { issueId: fromPath[1], source: "url" };
29176
- }
29177
- const anywhere = trimmed.match(/([A-Z]+-\d+)/);
29178
- if (anywhere && ISSUE_RE.test(anywhere[1])) {
29179
- return { issueId: anywhere[1], source: "url" };
29180
- }
29181
- return { error: `could not parse issue id from: ${trimmed}` };
29182
- }
29183
- var contextScripts = {
29184
- config: readYouTrackContextConfig,
29185
- greeting: () => youTrackGreeting(youTrackReadOnlyConfigPath()),
29186
- parseDuration: (text) => youTrackParseDuration(text),
29187
- api: () => ({ error: "YouTrack context is read-only" })
29188
- };
29189
- function resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) {
29190
- const root = resolveWorkspaceRoot(workspace_root);
29191
- for (const rel of [spec_path, plan_path].filter(Boolean)) {
29192
- let full;
29193
- try {
29194
- full = resolveInside(root, rel);
29195
- } catch {
29196
- continue;
28789
+ default: {
28790
+ return {
28791
+ error: `unknown action ${action} (youtrack_scaffold|youtrack_json|youtrack_token_placeholder|vcs_scaffold)`
28792
+ };
29197
28793
  }
29198
- if (!fs8.existsSync(full))
29199
- continue;
29200
- const text = fs8.readFileSync(full, "utf8");
29201
- const m = text.match(/^\*\*YouTrack:\*\*\s*`?([A-Z]+-\d+)`?/m);
29202
- if (m)
29203
- return m[1];
29204
28794
  }
29205
- return null;
29206
28795
  }
29207
- function meetingOptionsFromConfig(cfg) {
29208
- const base = (cfg.baseUrl || "").replace(/\/$/, "");
29209
- if (cfg.meetingIssues && typeof cfg.meetingIssues === "object") {
29210
- return Object.entries(cfg.meetingIssues).map(([key, item]) => ({
29211
- key,
29212
- issue: item.issue,
29213
- label: item.label ?? item.issue,
29214
- workItemText: item.workItemText ?? "Reuniones",
29215
- url: item.url ?? (base && item.issue ? `${base}/issue/${item.issue}` : null)
29216
- }));
29217
- }
29218
- const issue = cfg.meetingIssue;
29219
- return [
29220
- {
29221
- key: "general",
29222
- issue,
29223
- label: "General meetings",
29224
- workItemText: "Reuniones",
29225
- url: base && issue ? `${base}/issue/${issue}` : null
29226
- }
29227
- ];
28796
+ function initApply({
28797
+ action,
28798
+ confirmed,
28799
+ env
28800
+ }) {
28801
+ if (!confirmed)
28802
+ return { error: "confirmed: true required" };
28803
+ return { data: initApplyData(action, env ? { ...process.env, ...env } : process.env) };
29228
28804
  }
29229
- var readOnlyYouTrackConfig = (cfg) => {
29230
- const publicUrl = (value) => {
29231
- if (typeof value !== "string")
29232
- return;
29233
- try {
29234
- const url = new URL(value);
29235
- if (url.username || url.password)
29236
- return `${url.protocol}//${url.host}${url.pathname}${url.search}${url.hash}`;
29237
- return value;
29238
- } catch {
29239
- return;
29240
- }
29241
- };
29242
- const safe = {};
29243
- const baseUrl = publicUrl(cfg.baseUrl);
29244
- if (baseUrl)
29245
- safe.baseUrl = baseUrl;
29246
- for (const key of ["timezone", "greetingCutoff", "defaultMention", "meetingIssue"])
29247
- if (typeof cfg[key] === "string")
29248
- safe[key] = cfg[key];
29249
- if (cfg.greetings && typeof cfg.greetings === "object" && !Array.isArray(cfg.greetings)) {
29250
- const greetings = {};
29251
- for (const key of ["morning", "afternoon"])
29252
- if (typeof cfg.greetings[key] === "string")
29253
- greetings[key] = cfg.greetings[key];
29254
- if (Object.keys(greetings).length)
29255
- safe.greetings = greetings;
29256
- }
29257
- if (cfg.meetingIssues && typeof cfg.meetingIssues === "object" && !Array.isArray(cfg.meetingIssues)) {
29258
- safe.meetingIssues = Object.fromEntries(Object.entries(cfg.meetingIssues).flatMap(([key, value]) => {
29259
- if (!value || typeof value !== "object" || Array.isArray(value))
29260
- return [];
29261
- const item = value;
29262
- const safeItem = {};
29263
- for (const field of ["issue", "label", "workItemText"])
29264
- if (typeof item[field] === "string")
29265
- safeItem[field] = item[field];
29266
- const url = publicUrl(item.url);
29267
- if (url)
29268
- safeItem.url = url;
29269
- return [[key, safeItem]];
29270
- }));
29271
- }
29272
- return safe;
28805
+
28806
+ // packages/workit-opencode/src/shared/repo-result.ts
28807
+ var output = (value) => JSON.stringify(value, null, 2);
28808
+ var diagnostics = ({ stdout, stderr, exitCode }) => ({
28809
+ stdout,
28810
+ stderr,
28811
+ exitCode
28812
+ });
28813
+ var requireConfirmed = (confirmed) => {
28814
+ if (confirmed === true)
28815
+ return null;
28816
+ return output(fail("confirmed: true required"));
29273
28817
  };
29274
- function context({
29275
- spec_path,
29276
- plan_path,
29277
- issue_id,
29278
- issue_url,
29279
- issue_ref,
29280
- mode,
29281
- workspace_root
29282
- }, scripts = contextScripts) {
29283
- const cfg = scripts.config();
29284
- if (cfg.error)
29285
- return { error: cfg.error };
29286
- const greeting = scripts.greeting();
29287
- if (greeting.exitCode !== 0) {
29288
- return { error: (greeting.stderr || greeting.stdout || "greeting failed").trim() };
28818
+ var legacyScriptResult = (result) => {
28819
+ let parsed = null;
28820
+ try {
28821
+ parsed = JSON.parse(result.stdout.trim());
28822
+ } catch {}
28823
+ if (result.exitCode !== 0 || parsed?.error || parsed?.ok === false) {
28824
+ const error = parsed?.error ? String(parsed.error) : parsed?.ok === false ? "legacy operation reported failure" : result.stderr.trim() || result.stdout.trim() || "workflow script failed";
28825
+ return fail(error, diagnostics(result));
29289
28826
  }
29290
- const safeConfig = readOnlyYouTrackConfig(cfg.data);
29291
- const meetingOptions = meetingOptionsFromConfig(safeConfig);
29292
- if (mode === "meetings" && !issue_id && !issue_url && !issue_ref) {
28827
+ if (!parsed)
28828
+ return fail("workflow output parse failed", diagnostics(result));
28829
+ const { ok: _legacyOk, ...data } = parsed;
28830
+ return ok({ ...data, exitCode: 0, ...result.stderr ? { stderr: result.stderr } : {} });
28831
+ };
28832
+
28833
+ // packages/workit-opencode/src/shared/init-apply.ts
28834
+ var initApplyRuntime = {
28835
+ initApply: (root, action, env) => {
28836
+ const out = initApply({ action, confirmed: true, env });
29293
28837
  return {
29294
- config: safeConfig,
29295
- greeting: greeting.stdout.trim(),
29296
- mode: "meetings",
29297
- requiresMeetingChoice: true,
29298
- meetingOptions,
29299
- issueId: null
28838
+ exitCode: out.error ? 1 : 0,
28839
+ stdout: JSON.stringify(out.data ?? out),
28840
+ stderr: "",
28841
+ cwd: root
29300
28842
  };
29301
28843
  }
29302
- let issue = issue_id;
29303
- if (!issue && (issue_url || issue_ref)) {
29304
- const parsed = parseIssueRef(issue_url ?? issue_ref);
29305
- if ("error" in parsed)
29306
- return { error: parsed.error };
29307
- issue = parsed.issueId;
28844
+ };
28845
+ var executeInitApply = (args, directory, runtime) => {
28846
+ const rejected = requireConfirmed(args.confirmed);
28847
+ if (rejected)
28848
+ return rejected;
28849
+ const action = args.action;
28850
+ if (action === "hygiene") {
28851
+ const result = ensureHygieneFiles(directory, {
28852
+ confirmed: args.confirmed,
28853
+ includeOpenSource: args.include_open_source
28854
+ });
28855
+ return output(result.ok ? ok(result) : fail(result.error));
29308
28856
  }
29309
- if (!issue && mode === "meetings")
29310
- issue = meetingOptions[0]?.issue ?? safeConfig.meetingIssue;
29311
- if (!issue)
29312
- issue = resolveYouTrackFromPaths(spec_path, plan_path, workspace_root) ?? undefined;
29313
- if (!issue || !ISSUE_RE.test(issue)) {
29314
- return {
29315
- error: "invalid or missing issue id — pass issue_url, issue_id, or spec/plan with **YouTrack:**",
29316
- requiresIssueInput: true
29317
- };
28857
+ if (action === "gitignore") {
28858
+ const result = ensureProjectGitignore(directory, args.confirmed);
28859
+ return output(result.ok ? ok(result) : fail(result.error));
29318
28860
  }
29319
- const base = (safeConfig.baseUrl || "").replace(/\/$/, "");
29320
- const issueUrl = base ? `${base}/issue/${issue}` : null;
29321
- const selectedMeeting = meetingOptions.find((m) => m.issue === issue);
28861
+ if (action === "config") {
28862
+ const LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
28863
+ if (args.locale !== undefined && !LOCALE_RE.test(args.locale)) {
28864
+ return output(fail(`invalid locale: ${JSON.stringify(args.locale)} — expected BCP-47 like en or es-CL`));
28865
+ }
28866
+ const current = readConfig();
28867
+ const next = mergeConfigValues({
28868
+ locale: args.locale,
28869
+ localeOptions: args.locale_options,
28870
+ timezone: args.timezone,
28871
+ preset: args.branch_policy_preset,
28872
+ allowed: args.branch_policy_allowed,
28873
+ protectedNames: args.branch_policy_protected
28874
+ }, current);
28875
+ writeConfig(next);
28876
+ return output(ok({ action: "config", path: path20.join(configDir(), "config.json"), ...next }));
28877
+ }
28878
+ const env = Object.fromEntries(Object.entries({
28879
+ WORKFLOW_YT_BASE_URL: args.base_url,
28880
+ WORKFLOW_YT_MENTION: args.default_mention,
28881
+ WORKFLOW_YT_MEETING_ISSUE: args.meeting_issue,
28882
+ WORKFLOW_VCS_PROVIDER: args.vcs_provider,
28883
+ WORKFLOW_VCS_TARGET_BRANCH: args.vcs_target_branch,
28884
+ WORKFLOW_WORKSPACE_ROOT: directory,
28885
+ WORKFLOW_BP_NAME: args.name,
28886
+ WORKFLOW_BP_DEVELOP: args.develop_branch,
28887
+ WORKFLOW_BP_INTEGRATION: args.integration
28888
+ }).filter((entry) => entry[1] !== undefined));
28889
+ return output(legacyScriptResult(runtime.initApply(directory, action, env)));
28890
+ };
28891
+
28892
+ // packages/workit-opencode/src/tools/repo.ts
28893
+ function createRepoTools(runtime = initApplyRuntime) {
29322
28894
  return {
29323
- config: safeConfig,
29324
- greeting: greeting.stdout.trim(),
29325
- issueId: issue,
29326
- issueUrl,
29327
- mode: mode ?? (selectedMeeting ? "meetings" : "task"),
29328
- meetingOptions,
29329
- workItemText: selectedMeeting?.workItemText ?? null
28895
+ workit_init_apply: tool({
28896
+ description: "Apply a confirmed toolkit initialization action",
28897
+ args: {
28898
+ confirmed: tool.schema.boolean(),
28899
+ action: tool.schema.enum([
28900
+ "youtrack_scaffold",
28901
+ "youtrack_json",
28902
+ "youtrack_token_placeholder",
28903
+ "vcs_scaffold",
28904
+ "config",
28905
+ "gitignore",
28906
+ "hygiene",
28907
+ "branch_policy"
28908
+ ]),
28909
+ base_url: tool.schema.string().optional(),
28910
+ default_mention: tool.schema.string().optional(),
28911
+ meeting_issue: tool.schema.string().optional(),
28912
+ vcs_provider: tool.schema.enum(["gitlab", "github"]).optional(),
28913
+ vcs_target_branch: tool.schema.string().optional(),
28914
+ name: tool.schema.string().optional(),
28915
+ develop_branch: tool.schema.string().optional(),
28916
+ integration: tool.schema.enum(["pr", "merge"]).optional(),
28917
+ locale: tool.schema.string().optional(),
28918
+ locale_options: tool.schema.array(tool.schema.string()).optional(),
28919
+ timezone: tool.schema.string().optional(),
28920
+ branch_policy_preset: tool.schema.enum(["gitflow", "github-flow", "trunk-based", "custom"]).optional(),
28921
+ branch_policy_allowed: tool.schema.array(tool.schema.string()).optional(),
28922
+ branch_policy_protected: tool.schema.array(tool.schema.string()).optional(),
28923
+ include_open_source: tool.schema.boolean().optional()
28924
+ },
28925
+ execute: async (args, context) => executeInitApply(args, context.directory, runtime)
28926
+ })
29330
28927
  };
29331
28928
  }
29332
- var fetchYouTrackIssueBody = async (issue, creds = youTrackToken, request = youTrackRequest) => {
29333
- const credentials = creds();
29334
- if ("error" in credentials)
29335
- return { error: credentials.error, kind: "creds" };
29336
- const { token, base } = credentials;
29337
- const out = await request(`${base}/api/issues/${encodeURIComponent(issue)}?fields=idReadable,summary,description,customFields(name,value(name))`, { method: "GET", token });
29338
- if (out.status !== 0)
29339
- return { error: out.stderr || "YouTrack issue fetch failed", kind: "request" };
28929
+
28930
+ // packages/workit-opencode/src/tools/context.ts
28931
+ import { realpathSync as realpathSync10 } from "node:fs";
28932
+ import path23 from "node:path";
28933
+
28934
+ // packages/workit-core/src/core/pr-create.ts
28935
+ import fs10 from "node:fs";
28936
+ import path21 from "node:path";
28937
+ import { spawnSync as spawnSync6 } from "node:child_process";
28938
+ function parseGhIssue(value) {
28939
+ const m = /issues\/(\d+)/.exec(value);
28940
+ return m ? m[1] : String(value).trim().replace(/^#/, "");
28941
+ }
28942
+ function whichOnPath(tool) {
28943
+ const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
28944
+ for (const dir of (process.env.PATH ?? "").split(path21.delimiter)) {
28945
+ if (!dir)
28946
+ continue;
28947
+ for (const name of names) {
28948
+ const candidate = path21.join(dir, name);
28949
+ try {
28950
+ fs10.accessSync(candidate, fs10.constants.X_OK);
28951
+ return candidate;
28952
+ } catch {}
28953
+ }
28954
+ }
28955
+ return null;
28956
+ }
28957
+ var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
28958
+ var pushRemote = (cwd) => {
28959
+ const result = spawnSync6("git", ["remote", "get-url", "--push", "--all", "origin"], {
28960
+ cwd,
28961
+ encoding: "utf8"
28962
+ });
28963
+ if (result.status !== 0)
28964
+ return null;
28965
+ const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
28966
+ return urls.length === 1 ? urls[0] : null;
28967
+ };
28968
+
28969
+ // packages/workit-core/src/core/repo-context.ts
28970
+ import { spawnSync as spawnSync7 } from "node:child_process";
28971
+ import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync7 } from "node:fs";
28972
+ import path22 from "node:path";
28973
+ var isSafeContextRange = (value) => value.length <= 4096 && (() => {
29340
28974
  try {
29341
- const parsed = JSON.parse(out.stdout);
29342
- const customFields = Array.isArray(parsed.customFields) ? parsed.customFields : [];
29343
- const stateField = customFields.find((field) => typeof field === "object" && field !== null && field.name === "State");
29344
- const stateValue = stateField?.value;
29345
- const stateName = typeof stateValue === "object" && stateValue !== null ? stateValue.name : undefined;
29346
- return {
29347
- data: {
29348
- idReadable: String(parsed.idReadable ?? issue),
29349
- summary: String(parsed.summary ?? ""),
29350
- description: typeof parsed.description === "string" ? parsed.description : null,
29351
- state: typeof stateValue === "string" ? stateValue : typeof stateName === "string" && stateName ? stateName : null
29352
- }
29353
- };
28975
+ gitRevisionParts(value);
28976
+ return true;
29354
28977
  } catch {
29355
- return { error: "invalid JSON from YouTrack API", kind: "request" };
28978
+ return false;
28979
+ }
28980
+ })();
28981
+ var isResolvableContextRange = (cwd, value) => {
28982
+ if (!isSafeContextRange(value))
28983
+ return false;
28984
+ try {
28985
+ resolveGitRevision(cwd, value);
28986
+ return true;
28987
+ } catch {
28988
+ return false;
29356
28989
  }
29357
28990
  };
29358
-
29359
- // packages/workit-core/src/core/init.ts
29360
- var TOKEN_PLACEHOLDER2 = "YOUR_TOKEN_HERE";
29361
- var readJson = (p) => {
29362
- try {
29363
- const parsed = JSON.parse(fs9.readFileSync(p, "utf8"));
29364
- return isConfigObject(parsed) ? parsed : null;
29365
- } catch {
29366
- return null;
28991
+ var invalidRange = (cwd) => ({
28992
+ stdout: "",
28993
+ stderr: `ERROR: invalid revision range
28994
+ `,
28995
+ exitCode: 2,
28996
+ cwd
28997
+ });
28998
+ var runGit = (cwd, args) => {
28999
+ const result = spawnSync7("git", args, { cwd, encoding: "utf8" });
29000
+ return {
29001
+ stdout: (result.stdout ?? "").trimEnd(),
29002
+ stderr: (result.stderr ?? "").trimEnd(),
29003
+ exitCode: result.status ?? 1
29004
+ };
29005
+ };
29006
+ var repoRoot3 = (cwd) => {
29007
+ const r = runGit(cwd, ["rev-parse", "--show-toplevel"]);
29008
+ return r.exitCode === 0 && r.stdout ? r.stdout : cwd;
29009
+ };
29010
+ var currentBranch = (cwd) => {
29011
+ const r = runGit(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
29012
+ return r.exitCode === 0 && r.stdout ? r.stdout : "unknown";
29013
+ };
29014
+ var defaultBase = (cwd) => {
29015
+ for (const ref of [
29016
+ "origin/main",
29017
+ "main",
29018
+ "origin/master",
29019
+ "master",
29020
+ "origin/develop",
29021
+ "develop"
29022
+ ]) {
29023
+ if (runGit(cwd, ["rev-parse", "--verify", ref]).exitCode === 0)
29024
+ return ref;
29367
29025
  }
29026
+ return "HEAD~1";
29368
29027
  };
29369
- var isPlaceholder = (text) => text === TOKEN_PLACEHOLDER2 || text.startsWith(TOKEN_PLACEHOLDER2);
29370
- var modeOk = (p) => process.platform === "win32" || (fs9.statSync(p).mode & 511) === 384;
29371
- var resolveInitProvider = (cwd) => {
29372
- const env = process.env.WORKFLOW_VCS_PROVIDER?.trim();
29373
- if (env)
29374
- return env.toLowerCase();
29375
- const root = cwd ?? process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd();
29376
- try {
29377
- const out = spawnSync7("git", ["remote", "get-url", "origin"], {
29378
- cwd: root,
29379
- encoding: "utf8"
29380
- });
29381
- if (out.status === 0) {
29382
- const url = (out.stdout ?? "").trim();
29383
- if (/github\.com[:/]/.test(url))
29384
- return "github";
29385
- if (/gitlab\.com[:/]/.test(url))
29386
- return "gitlab";
29387
- }
29388
- } catch {}
29389
- return null;
29390
- };
29391
- var resolvePath = (p) => {
29392
- try {
29393
- return fs9.realpathSync(p);
29394
- } catch {
29395
- return path21.resolve(p);
29396
- }
29397
- };
29398
- function initStatusData(configDirPath = configDir()) {
29399
- const ytJson = path21.join(configDirPath, "youtrack.json");
29400
- const vcsJson = path21.join(configDirPath, "vcs.json");
29401
- const items = [];
29402
- let youtrackConfig = null;
29403
- let youtrackTokenCreate = null;
29404
- const ytParsed = readJson(ytJson);
29405
- if (ytParsed) {
29406
- const base = String(ytParsed.baseUrl ?? "").replace(/\/+$/, "");
29407
- const meeting = String(ytParsed.meetingIssue ?? "");
29408
- const tokenFile = String(ytParsed.tokenFile ?? "");
29409
- const meetingIssues = [];
29410
- const rawMeetings = ytParsed.meetingIssues ?? {};
29411
- if (rawMeetings && typeof rawMeetings === "object") {
29412
- for (const [key, item] of Object.entries(rawMeetings)) {
29413
- const iss = String(item?.issue ?? "");
29414
- meetingIssues.push({
29415
- key,
29416
- issue: iss,
29417
- label: item?.label ?? iss,
29418
- workItemText: item?.workItemText ?? "Reuniones",
29419
- url: item?.url ?? (base && iss ? `${base}/issue/${iss}` : null)
29420
- });
29421
- }
29422
- }
29423
- const expanded = tokenFile ? path21.resolve(tokenFile) : null;
29424
- const resolvedTokenFile = expanded && fs9.existsSync(expanded) ? resolvePath(expanded) : expanded ? path21.isAbsolute(expanded) ? expanded : path21.resolve(configDirPath, expanded) : null;
29425
- youtrackConfig = {
29426
- config_edit_path: resolvePath(ytJson),
29427
- baseUrl: base,
29428
- meetingIssue: meeting,
29429
- meetingIssues,
29430
- meetingIssueUrl: base && meeting ? `${base}/issue/${meeting}` : null,
29431
- defaultMention: ytParsed.defaultMention,
29432
- timezone: ytParsed.timezone,
29433
- locale: ytParsed.locale,
29434
- tokenFile: resolvedTokenFile,
29435
- tokenDefaults: ytParsed.tokenDefaults,
29436
- timeLogging: {
29437
- meetings: {
29438
- options: meetingIssues,
29439
- skill: "/wk-meetings",
29440
- logsTime: true,
29441
- postsComment: false
29442
- },
29443
- taskWork: {
29444
- issueSource: "active spec/plan **YouTrack:** field or --issue",
29445
- skill: "/wk-issue-update",
29446
- logsTime: true,
29447
- postsComment: true
29448
- }
29449
- }
29028
+ var rangeArgOrDefault = (arg, cwd) => arg ? arg : `${defaultBase(cwd)}...HEAD`;
29029
+ var isProtectedBranch = (branch) => ["main", "master", "develop", "prod", "production"].includes(branch);
29030
+ var isPrBranch = (branch) => branch.startsWith("feature/") || branch.startsWith("bugfix/");
29031
+ function resolvePrBranchContext(cwd) {
29032
+ const branch = currentBranch(cwd);
29033
+ if (isProtectedBranch(branch)) {
29034
+ return {
29035
+ ok: false,
29036
+ error: `cannot build PR context on protected branch ${branch} — PRs are for feature/* or bugfix/* only`
29450
29037
  };
29451
- youtrackTokenCreate = youTrackTokenCreateUrl().data;
29452
- if (youtrackTokenCreate && !youtrackConfig.error) {
29453
- youtrackConfig.tokenCreate = youtrackTokenCreate;
29454
- }
29455
- } else if (fs9.existsSync(ytJson)) {
29456
- let parseFailed = false;
29457
- try {
29458
- JSON.parse(fs9.readFileSync(ytJson, "utf8"));
29459
- } catch {
29460
- parseFailed = true;
29038
+ }
29039
+ if (!isPrBranch(branch)) {
29040
+ if (branch === "unknown") {
29041
+ return {
29042
+ ok: false,
29043
+ error: `not in a git repository at ${repoRoot3(cwd)} — open the target repository as the OpenCode session directory`
29044
+ };
29461
29045
  }
29462
- youtrackConfig = {
29463
- config_edit_path: resolvePath(ytJson),
29464
- error: parseFailed ? "invalid youtrack.json" : `${resolvePath(ytJson)} is not a JSON object`
29046
+ return {
29047
+ ok: false,
29048
+ error: `branch ${branch} is not feature/* or bugfix/* — checkout a feature branch or pass an explicit git range`
29465
29049
  };
29466
29050
  }
29467
- items.push({
29468
- id: "youtrack_json",
29469
- label: "YouTrack config",
29470
- ok: fs9.existsSync(ytJson) && youtrackConfig !== null && !("error" in youtrackConfig),
29471
- path: fs9.existsSync(ytJson) ? resolvePath(ytJson) : path21.resolve(ytJson),
29472
- config_edit_path: resolvePath(ytJson),
29473
- fix: "workit_init_apply action=youtrack_scaffold"
29474
- });
29475
- const ytTokenPath = youtrackConfig?.tokenFile ?? path21.join(configDirPath, "youtrack.token");
29476
- const tokenText = fs9.existsSync(ytTokenPath) ? fs9.readFileSync(ytTokenPath, "utf8").trim() : "";
29477
- const placeholder = fs9.existsSync(ytTokenPath) && isPlaceholder(tokenText);
29478
- const tokenOk = fs9.existsSync(ytTokenPath) && modeOk(ytTokenPath) && Boolean(tokenText) && !isPlaceholder(tokenText);
29479
- const youtrackTokenItem = {
29480
- id: "youtrack_token",
29481
- label: "YouTrack API token (mode 600, not placeholder)",
29482
- ok: tokenOk,
29483
- path: fs9.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path21.resolve(ytTokenPath),
29484
- token_edit_path: fs9.existsSync(ytTokenPath) ? resolvePath(ytTokenPath) : path21.resolve(ytTokenPath),
29485
- placeholder,
29486
- fix: `Open ${resolvePath(ytTokenPath)} — replace ${TOKEN_PLACEHOLDER2} with your permanent token, save, then /wk-status`
29487
- };
29488
- if (youtrackTokenCreate) {
29489
- if (youtrackTokenCreate.createUrl)
29490
- youtrackTokenItem.token_create_url = youtrackTokenCreate.createUrl;
29491
- if (youtrackTokenCreate.docsUrl)
29492
- youtrackTokenItem.token_create_docs_url = youtrackTokenCreate.docsUrl;
29493
- if (youtrackTokenCreate.scopes)
29494
- youtrackTokenItem.token_scopes = youtrackTokenCreate.scopes;
29495
- if (youtrackTokenCreate.tokenName)
29496
- youtrackTokenItem.token_name = youtrackTokenCreate.tokenName;
29497
- if (youtrackTokenCreate.steps)
29498
- youtrackTokenItem.token_create_steps = youtrackTokenCreate.steps;
29499
- youtrackTokenItem.token_prefill_supported = youtrackTokenCreate.prefillSupported ?? false;
29500
- }
29501
- items.push(youtrackTokenItem);
29502
- let vcsCfg = null;
29503
- const vcsParsed = readJson(vcsJson);
29504
- if (vcsParsed) {
29505
- const rawProvider = vcsParsed.provider;
29506
- const effective = vcsConfig("resolve", process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd());
29507
- const provider = effective?.ok && typeof effective.provider === "string" ? effective.provider : typeof rawProvider === "string" && rawProvider.trim() ? rawProvider.toLowerCase() : null;
29508
- vcsCfg = {
29509
- config_edit_path: resolvePath(vcsJson),
29510
- provider,
29511
- defaultTargetBranch: effective.ok ? effective.defaultTargetBranch : vcsParsed.defaultTargetBranch ?? "develop",
29512
- pr: vcsParsed.pr ?? {},
29513
- gitlab: vcsParsed.gitlab,
29514
- github: vcsParsed.github,
29515
- skill: "/wk-pr",
29516
- switchHint: 'Set "provider" to "gitlab" or "github" in vcs.json'
29051
+ const resolved = vcsConfig("resolve", cwd);
29052
+ if (resolved.ok === false) {
29053
+ return { ok: false, error: String(resolved.error) };
29054
+ }
29055
+ const base = String(resolved.defaultTargetBranch ?? "");
29056
+ if (!base || runGit(cwd, ["check-ref-format", "--branch", base]).exitCode !== 0) {
29057
+ return { ok: false, error: `invalid configured PR target branch ${base}` };
29058
+ }
29059
+ for (const ref of [`origin/${base}`, base]) {
29060
+ if (runGit(cwd, ["rev-parse", "--verify", ref]).exitCode !== 0)
29061
+ continue;
29062
+ const mb = runGit(cwd, ["merge-base", ref, "HEAD"]);
29063
+ if (mb.exitCode !== 0 || !mb.stdout)
29064
+ continue;
29065
+ return {
29066
+ ok: true,
29067
+ value: {
29068
+ baseRef: ref,
29069
+ mergeBase: mb.stdout,
29070
+ range: `${ref}..HEAD`,
29071
+ diffRange: `${mb.stdout}..HEAD`
29072
+ }
29517
29073
  };
29518
- } else if (fs9.existsSync(vcsJson)) {
29519
- vcsCfg = { config_edit_path: resolvePath(vcsJson), error: "invalid vcs.json" };
29520
29074
  }
29521
- items.push({
29522
- id: "vcs_json",
29523
- label: "VCS config (GitLab / GitHub)",
29524
- ok: fs9.existsSync(vcsJson) && vcsCfg !== null && !("error" in vcsCfg),
29525
- path: fs9.existsSync(vcsJson) ? resolvePath(vcsJson) : path21.resolve(vcsJson),
29526
- config_edit_path: resolvePath(vcsJson),
29527
- fix: "workit_init_apply action=vcs_scaffold"
29528
- });
29529
29075
  return {
29530
- config_dir: configDirPath,
29531
- plugin_root: PLUGIN_ROOT,
29532
- token_placeholder: TOKEN_PLACEHOLDER2,
29533
- youtrack_config: youtrackConfig,
29534
- vcs_config: vcsCfg,
29535
- items,
29536
- ready: false
29537
- };
29538
- }
29539
- async function toolkitStatusData(configDirPath = configDir()) {
29540
- const status = initStatusData(configDirPath);
29541
- const tokenItem = status.items.find((i) => i.id === "youtrack_token") ?? {};
29542
- const placeholder = Boolean(tokenItem.placeholder);
29543
- const vcsCfg = status.vcs_config ?? {};
29544
- const provider = typeof vcsCfg.provider === "string" && vcsCfg.provider.trim() ? vcsCfg.provider.toLowerCase() : null;
29545
- const vcsJsonOk = Boolean(status.items.find((i) => i.id === "vcs_json")?.ok);
29546
- const verify = placeholder ? { ok: false, error: "token still placeholder YOUR_TOKEN_HERE" } : await youTrackVerifyToken();
29547
- const vcsVerify = provider === null ? {
29548
29076
  ok: false,
29549
- error: "vcs provider unconfigured — set provider to gitlab or github in vcs.json"
29550
- } : await vcsVerifyToken();
29551
- const youTrackHealth = "data" in verify ? verify.data : verify;
29552
- status.youtrack_verify = verify;
29553
- status.youtrack_ok = placeholder ? false : Boolean(youTrackHealth.ok);
29554
- status.vcs_verify = vcsVerify;
29555
- status.vcs_ok = vcsJsonOk && Boolean(vcsVerify.ok);
29556
- const fsReady = status.items.every((i) => i.required === false || i.ok);
29557
- status.ready = fsReady && Boolean(status.youtrack_ok) && (!vcsJsonOk || Boolean(status.vcs_ok));
29558
- if (placeholder) {
29559
- const tokenPath = tokenItem.token_edit_path ?? tokenItem.path ?? "";
29560
- const createUrl = tokenItem.token_create_url ?? status.youtrack_config?.tokenCreate?.createUrl;
29561
- status.token_edit_path = tokenPath;
29562
- if (createUrl)
29563
- status.token_create_url = createUrl;
29564
- status.next_step = createUrl ? "Open the YouTrack create-token URL, New token (name workit, scope YouTrack), paste into the token file, save, then run /wk-status" : "Open the YouTrack token file, replace YOUR_TOKEN_HERE, save, then run /wk-status";
29565
- } else if (!status.youtrack_ok) {
29566
- status.next_step = "Fix YouTrack token or re-run /wk-init";
29567
- } else if (vcsJsonOk && !status.vcs_ok) {
29568
- status.next_step = `Run ${provider === "gitlab" ? "glab" : "gh"} auth login/switch, then /wk-status`;
29569
- } else if (status.ready) {
29570
- status.next_step = "All checks passed";
29571
- }
29572
- return status;
29077
+ error: `configured PR target branch ${base} not found — fetch/checkout it or pass an explicit git range`
29078
+ };
29573
29079
  }
29574
- var youtrackJsonContent = (dir) => ({
29575
- baseUrl: process.env.WORKFLOW_YT_BASE_URL ?? "https://enghouseamg.youtrack.cloud",
29576
- tokenFile: process.env.WORKFLOW_YT_TOKEN_FILE ?? path21.join(dir, "youtrack.token"),
29577
- timezone: process.env.WORKFLOW_YT_TIMEZONE ?? "America/Santiago",
29578
- locale: "es-CL",
29579
- defaultMention: process.env.WORKFLOW_YT_MENTION ?? "Alejandra.Flores",
29580
- greetings: { morning: "buenos días", afternoon: "buenas tardes" },
29581
- greetingCutoff: "12:00",
29582
- meetingIssue: process.env.WORKFLOW_YT_MEETING_ISSUE ?? "IRPT-12",
29583
- meetingIssues: {
29584
- general: {
29585
- issue: process.env.WORKFLOW_YT_MEETING_ISSUE ?? "IRPT-12",
29586
- label: "General meetings (Reuniones internas Team IRP)",
29587
- workItemText: "Reuniones"
29588
- },
29589
- web: {
29590
- issue: process.env.WORKFLOW_YT_WEB_MEETING_ISSUE ?? "NSXFT-21",
29591
- label: "Web meetings",
29592
- workItemText: "Reuniones web",
29593
- url: "https://enghouseamg.youtrack.cloud/projects/NSXFT/issues/NSXFT-21"
29080
+ var printSection = (title) => `
29081
+ ## ${title}
29082
+
29083
+ `;
29084
+ var probeCaseInsensitive = (dir, wanted) => {
29085
+ try {
29086
+ for (const entry of readdirSync7(dir)) {
29087
+ if (entry.toLowerCase() === wanted.toLowerCase())
29088
+ return entry;
29594
29089
  }
29595
- },
29596
- commentHeader: "# Actualización",
29597
- attachmentsHeaderImages: "## Adjunto capturas",
29598
- attachmentsHeaderFiles: "## Archivos adjuntos",
29599
- attachmentsHeaderMixed: "## Adjuntos",
29600
- tokenDefaults: {
29601
- name: "workit",
29602
- description: "OpenCode workit — /wk-issue-update and /wk-meetings",
29603
- scopes: ["YouTrack"],
29604
- profileTab: "account-security"
29090
+ } catch {}
29091
+ return null;
29092
+ };
29093
+ var findPrTemplate = (cwd) => {
29094
+ for (const rel of [
29095
+ ".gitlab/merge_request_templates/Default.md",
29096
+ ".gitlab/merge_request_templates/default.md",
29097
+ ".gitlab/merge_request_templates/merge_request_template.md",
29098
+ ".github/PULL_REQUEST_TEMPLATE.md",
29099
+ ".github/pull_request_template.md",
29100
+ ".github/PULL_REQUEST_TEMPLATE/pull_request_template.md",
29101
+ "docs/PULL_REQUEST_TEMPLATE.md",
29102
+ "PULL_REQUEST_TEMPLATE.md"
29103
+ ]) {
29104
+ const parts = rel.split("/");
29105
+ const base = parts[parts.length - 1];
29106
+ const dir = parts.length > 1 ? path22.join(cwd, ...parts.slice(0, -1)) : cwd;
29107
+ const actual = probeCaseInsensitive(dir, base);
29108
+ if (actual)
29109
+ return [...parts.slice(0, -1), actual].join("/");
29605
29110
  }
29606
- });
29607
- var vcsJsonContent = () => {
29608
- const provider = resolveInitProvider();
29609
- return {
29610
- ...provider ? { provider } : {},
29611
- defaultTargetBranch: process.env.WORKFLOW_VCS_TARGET_BRANCH ?? "develop",
29612
- gitlab: {
29613
- host: process.env.WORKFLOW_GITLAB_HOST ?? "gitlab.com",
29614
- apiUrl: process.env.WORKFLOW_GITLAB_API_URL ?? "https://gitlab.com/api/v4"
29615
- },
29616
- github: {
29617
- host: process.env.WORKFLOW_GITHUB_HOST ?? "github.com"
29618
- },
29619
- pr: { squashOnMerge: true, removeSourceBranch: true, pushBranch: true, confirmSkip: true }
29620
- };
29111
+ return null;
29621
29112
  };
29622
- function initApplyData(action, env = process.env) {
29623
- const dir = String(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
29624
- fs9.mkdirSync(dir, { recursive: true });
29625
- switch (action) {
29626
- case "youtrack_json": {
29627
- const out = path21.join(dir, "youtrack.json");
29628
- fs9.writeFileSync(out, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
29629
- `, "utf8");
29630
- return { action, ok: true, path: out };
29631
- }
29632
- case "youtrack_token_placeholder": {
29633
- const p = path21.join(dir, "youtrack.token");
29634
- const preserved = writeFileExclusive(p, TOKEN_PLACEHOLDER2 + `
29635
- `, 384) === "preserved";
29636
- const abs = path21.resolve(p);
29637
- return {
29638
- action,
29639
- ok: true,
29640
- path: abs,
29641
- token_edit_path: abs,
29642
- placeholder: TOKEN_PLACEHOLDER2,
29643
- preserved,
29644
- instruction: `Open ${abs} in your editor, replace YOUR_TOKEN_HERE with your YouTrack permanent token, save, then run /wk-status`
29645
- };
29646
- }
29647
- case "youtrack_scaffold": {
29648
- const jsonOut = path21.join(dir, "youtrack.json");
29649
- const tokenOut = path21.join(dir, "youtrack.token");
29650
- fs9.writeFileSync(jsonOut, JSON.stringify(youtrackJsonContent(dir), null, 2) + `
29651
- `, "utf8");
29652
- const preserved = writeFileExclusive(tokenOut, TOKEN_PLACEHOLDER2 + `
29653
- `, 384) === "preserved";
29654
- const configPath = path21.resolve(jsonOut);
29655
- const tokenPath = path21.resolve(tokenOut);
29656
- const prev = process.env.WORKFLOW_YOUTRACK_CONFIG;
29657
- process.env.WORKFLOW_YOUTRACK_CONFIG = configPath;
29658
- let tokenCreate = {};
29659
- try {
29660
- const cfg = readJson(configPath) ?? {};
29661
- const base = String(cfg.baseUrl ?? "").replace(/\/+$/, "");
29662
- const meeting = String(cfg.meetingIssue ?? "");
29663
- tokenCreate = youTrackTokenCreateUrl().data;
29664
- return {
29665
- action,
29666
- ok: true,
29667
- youtrack_json: configPath,
29668
- youtrack_token: tokenPath,
29669
- token_edit_path: tokenPath,
29670
- token_create_url: tokenCreate.createUrl,
29671
- token_create: tokenCreate,
29672
- config_edit_path: configPath,
29673
- placeholder: TOKEN_PLACEHOLDER2,
29674
- preserved,
29675
- youtrack_config: {
29676
- config_edit_path: configPath,
29677
- baseUrl: base,
29678
- meetingIssue: meeting,
29679
- meetingIssueUrl: base && meeting ? `${base}/issue/${meeting}` : null,
29680
- defaultMention: cfg.defaultMention,
29681
- timezone: cfg.timezone,
29682
- locale: cfg.locale,
29683
- tokenCreate,
29684
- timeLogging: {
29685
- meetings: {
29686
- issue: meeting,
29687
- skill: "/wk-meetings",
29688
- logsTime: true,
29689
- postsComment: false
29690
- },
29691
- taskWork: {
29692
- issueSource: "active spec/plan **YouTrack:** field or --issue",
29693
- skill: "/wk-issue-update",
29694
- logsTime: true,
29695
- postsComment: true
29696
- }
29697
- }
29698
- },
29699
- instruction: `Open the create-token URL, New token → name workit, scope YouTrack, paste into ${tokenPath}, then /wk-status.`
29700
- };
29701
- } finally {
29702
- if (prev === undefined)
29703
- delete process.env.WORKFLOW_YOUTRACK_CONFIG;
29704
- else
29705
- process.env.WORKFLOW_YOUTRACK_CONFIG = prev;
29706
- }
29113
+ var fallbackPrTemplate = () => `## Summary
29114
+ -
29115
+
29116
+ ## Validation
29117
+ - [ ] Not run`;
29118
+ var commitLogForRange = (cwd, range) => {
29119
+ const r = runGit(cwd, ["log", "--oneline", "--decorate", "--no-merges", range, "--"]);
29120
+ if (r.exitCode === 0)
29121
+ return r.stdout;
29122
+ return runGit(cwd, ["log", "--oneline", "--decorate", "-10", "--"]).stdout;
29123
+ };
29124
+ var changedFilesForRange = (cwd, range) => {
29125
+ const r = runGit(cwd, ["diff", "--name-only", range, "--"]);
29126
+ if (r.exitCode === 0)
29127
+ return r.stdout;
29128
+ return runGit(cwd, ["diff", "--name-only", "--"]).stdout;
29129
+ };
29130
+ var diffStatForRange = (cwd, range) => {
29131
+ const r = runGit(cwd, ["diff", "--stat", range, "--"]);
29132
+ if (r.exitCode === 0)
29133
+ return r.stdout;
29134
+ return runGit(cwd, ["diff", "--stat", "--"]).stdout;
29135
+ };
29136
+ var packageScripts = (cwd, keys) => {
29137
+ const pkgPath = path22.join(cwd, "package.json");
29138
+ if (!existsSync11(pkgPath))
29139
+ return [];
29140
+ try {
29141
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
29142
+ return keys.filter((k) => pkg.scripts && typeof pkg.scripts[k] === "string").map((k) => `${k}: ${pkg.scripts?.[k]}`);
29143
+ } catch {
29144
+ return [];
29145
+ }
29146
+ };
29147
+ var documentationFiles = (cwd) => {
29148
+ const excluded = new Set([".git", "node_modules", "target", "dist"]);
29149
+ const matches = [];
29150
+ const walk = (dir, depth) => {
29151
+ if (depth > 3)
29152
+ return;
29153
+ let entries;
29154
+ try {
29155
+ entries = readdirSync7(dir, { withFileTypes: true });
29156
+ } catch {
29157
+ return;
29707
29158
  }
29708
- case "vcs_scaffold": {
29709
- const jsonOut = path21.join(dir, "vcs.json");
29710
- fs9.writeFileSync(jsonOut, JSON.stringify(vcsJsonContent(), null, 2) + `
29711
- `, "utf8");
29712
- const configPath = path21.resolve(jsonOut);
29713
- const prev = process.env.WORKFLOW_VCS_CONFIG;
29714
- process.env.WORKFLOW_VCS_CONFIG = configPath;
29715
- try {
29716
- const cfg = readJson(configPath) ?? {};
29717
- const rawProvider = cfg.provider;
29718
- const provider = typeof rawProvider === "string" && rawProvider.trim() ? rawProvider : null;
29719
- return {
29720
- action,
29721
- ok: true,
29722
- vcs_json: configPath,
29723
- config_edit_path: configPath,
29724
- vcs_config: {
29725
- config_edit_path: configPath,
29726
- provider,
29727
- defaultTargetBranch: cfg.defaultTargetBranch,
29728
- pr: cfg.pr,
29729
- switchHint: 'Change "provider" to "github" when you migrate; authenticate with gh/glab',
29730
- skill: "/wk-pr"
29731
- },
29732
- instruction: provider === null ? `No provider resolved — set provider to gitlab or github in ${configPath}, then /wk-status.` : `Run ${provider === "gitlab" ? "glab" : "gh"} auth login, then /wk-status.`
29733
- };
29734
- } finally {
29735
- if (prev === undefined)
29736
- delete process.env.WORKFLOW_VCS_CONFIG;
29737
- else
29738
- process.env.WORKFLOW_VCS_CONFIG = prev;
29159
+ for (const entry of entries) {
29160
+ if (entry.isDirectory()) {
29161
+ if (excluded.has(entry.name))
29162
+ continue;
29163
+ walk(path22.join(dir, entry.name), depth + 1);
29164
+ } else if (entry.isFile() && (entry.name === "README.md" || entry.name.endsWith(".md"))) {
29165
+ matches.push("./" + path22.relative(cwd, path22.join(dir, entry.name)).replaceAll("\\", "/"));
29739
29166
  }
29740
29167
  }
29741
- case "branch_policy": {
29742
- const root = env.WORKFLOW_WORKSPACE_ROOT?.trim() ? env.WORKFLOW_WORKSPACE_ROOT : process.cwd();
29743
- return applyWorkspaceBranchPolicy({ workspace_root: root, env });
29744
- }
29745
- default: {
29746
- return {
29747
- error: `unknown action ${action} (youtrack_scaffold|youtrack_json|youtrack_token_placeholder|vcs_scaffold)`
29748
- };
29749
- }
29168
+ };
29169
+ walk(cwd, 1);
29170
+ return matches.sort().slice(0, 200);
29171
+ };
29172
+ var readTrimmed = (file, maxLines) => {
29173
+ if (!existsSync11(file))
29174
+ return "";
29175
+ const lines = readFileSync10(file, "utf8").split(`
29176
+ `);
29177
+ return lines.slice(0, maxLines).join(`
29178
+ `);
29179
+ };
29180
+ function prReadyContext(root, range) {
29181
+ const cwd = path22.resolve(repoRoot3(root));
29182
+ if (range !== undefined && !isResolvableContextRange(cwd, range))
29183
+ return invalidRange(cwd);
29184
+ let stdout = "";
29185
+ const autoRange = range === undefined;
29186
+ let ctx = null;
29187
+ if (autoRange) {
29188
+ ctx = resolvePrBranchContext(cwd);
29189
+ if (!ctx.ok)
29190
+ return { stdout: "", stderr: `ERROR: ${ctx.error}
29191
+ `, exitCode: 1, cwd };
29192
+ range = ctx.value.range;
29750
29193
  }
29751
- }
29752
- function initApply({
29753
- action,
29754
- confirmed,
29755
- env
29756
- }) {
29757
- if (!confirmed)
29758
- return { error: "confirmed: true required" };
29759
- return { data: initApplyData(action, env ? { ...process.env, ...env } : process.env) };
29760
- }
29194
+ stdout += printSection("Repository");
29195
+ stdout += `root: ${cwd}
29196
+ `;
29197
+ stdout += `branch: ${currentBranch(cwd)}
29198
+ `;
29199
+ stdout += `range: ${range}
29200
+ `;
29201
+ if (autoRange && ctx?.ok) {
29202
+ stdout += `base_ref: ${ctx.value.baseRef}
29203
+ `;
29204
+ stdout += `merge_base: ${ctx.value.mergeBase}
29205
+ `;
29206
+ stdout += `diff_range: ${ctx.value.diffRange}
29207
+ `;
29208
+ stdout += `range_mode: branch-exclusive
29209
+ `;
29210
+ if (process.env.PR_SYNC_NOTES)
29211
+ stdout += `git_sync: ${process.env.PR_SYNC_NOTES}
29212
+ `;
29213
+ }
29214
+ const diffRange = autoRange && ctx?.ok ? ctx.value.diffRange : range ?? "";
29215
+ stdout += printSection("Working Tree");
29216
+ const status = runGit(cwd, ["status", "--short"]);
29217
+ stdout += status.stdout + (status.stderr ? `
29218
+ ${status.stderr}` : "");
29219
+ stdout += `
29220
+ `;
29221
+ stdout += printSection("Commits");
29222
+ stdout += commitLogForRange(cwd, range ?? "");
29223
+ stdout += `
29224
+ `;
29225
+ stdout += printSection("Diff Stat");
29226
+ stdout += diffStatForRange(cwd, diffRange);
29227
+ stdout += `
29228
+ `;
29229
+ stdout += printSection("Changed Files");
29230
+ stdout += changedFilesForRange(cwd, diffRange);
29231
+ stdout += `
29232
+ `;
29233
+ stdout += printSection("PR Template");
29234
+ const template = findPrTemplate(cwd);
29235
+ if (template) {
29236
+ stdout += `template_path: ${template}
29761
29237
 
29762
- // packages/workit-opencode/src/shared/init-apply.ts
29763
- import path22 from "node:path";
29238
+ `;
29239
+ stdout += readTrimmed(path22.join(cwd, template), 220);
29240
+ } else {
29241
+ stdout += `template_path: none
29764
29242
 
29765
- // packages/workit-opencode/src/shared/repo-result.ts
29766
- var output = (value) => JSON.stringify(value, null, 2);
29767
- var diagnostics = ({ stdout, stderr, exitCode }) => ({
29768
- stdout,
29769
- stderr,
29770
- exitCode
29771
- });
29772
- var requireConfirmed = (confirmed) => {
29773
- if (confirmed === true)
29774
- return null;
29775
- return output(fail("confirmed: true required"));
29776
- };
29777
- function scriptResult(result, parse) {
29778
- if (result.exitCode !== 0) {
29779
- return fail(result.stderr.trim() || result.stdout.trim() || "workflow script failed", diagnostics(result));
29243
+ `;
29244
+ stdout += fallbackPrTemplate();
29780
29245
  }
29781
- try {
29782
- return ok({
29783
- ...parse(result.stdout),
29784
- exitCode: 0,
29785
- ...result.stderr ? { stderr: result.stderr } : {}
29786
- });
29787
- } catch (error) {
29788
- return fail(error instanceof Error ? error.message : "workflow output parse failed", diagnostics(result));
29246
+ stdout += `
29247
+ `;
29248
+ stdout += printSection("Recent Validation Signals");
29249
+ if (existsSync11(path22.join(cwd, "package.json"))) {
29250
+ stdout += `package.json detected. Common scripts:
29251
+ `;
29252
+ const found = packageScripts(cwd, ["lint", "format:check", "test", "build"]);
29253
+ if (found.length)
29254
+ stdout += found.join(`
29255
+ `) + `
29256
+ `;
29789
29257
  }
29790
- }
29791
- var legacyScriptResult = (result) => {
29792
- let parsed = null;
29793
- try {
29794
- parsed = JSON.parse(result.stdout.trim());
29795
- } catch {}
29796
- if (result.exitCode !== 0 || parsed?.error || parsed?.ok === false) {
29797
- const error = parsed?.error ? String(parsed.error) : parsed?.ok === false ? "legacy operation reported failure" : result.stderr.trim() || result.stdout.trim() || "workflow script failed";
29798
- return fail(error, diagnostics(result));
29258
+ if (existsSync11(path22.join(cwd, "Cargo.toml")) || existsSync11(path22.join(cwd, "src-tauri/Cargo.toml"))) {
29259
+ stdout += `Rust project detected.
29260
+ `;
29799
29261
  }
29800
- if (!parsed)
29801
- return fail("workflow output parse failed", diagnostics(result));
29802
- const { ok: _legacyOk, ...data } = parsed;
29803
- return ok({ ...data, exitCode: 0, ...result.stderr ? { stderr: result.stderr } : {} });
29804
- };
29805
-
29806
- // packages/workit-opencode/src/shared/init-apply.ts
29807
- var initApplyRuntime = {
29808
- initApply: (root, action, env) => {
29809
- const out = initApply({ action, confirmed: true, env });
29810
- return {
29811
- exitCode: out.error ? 1 : 0,
29812
- stdout: JSON.stringify(out.data ?? out),
29813
- stderr: "",
29814
- cwd: root
29815
- };
29262
+ stdout += printSection("VCS Config");
29263
+ const vc = vcsConfig("resolve", cwd);
29264
+ if (vc.ok) {
29265
+ stdout += `workspace: ${String(vc.workspace_name ?? "none")}
29266
+ `;
29267
+ stdout += `provider: ${String(vc.provider ?? "unconfigured")}
29268
+ `;
29269
+ } else {
29270
+ stdout += `vcs: unreadable (malformed) — ${String(vc.error ?? "cannot read vcs.json")}
29271
+ `;
29272
+ }
29273
+ if (!vcsConfig("summary", cwd).ok) {
29274
+ stdout += `vcs: not configured — run /wk-init action vcs_scaffold
29275
+ `;
29276
+ }
29277
+ stdout += printSection("Merged PR Style");
29278
+ const style = mergedPrStyle(6, cwd);
29279
+ stdout += JSON.stringify(style, null, 2) + `
29280
+ `;
29281
+ return { stdout, stderr: "", exitCode: 0, cwd };
29282
+ }
29283
+ var CHANGELOG_RULES = `- Use an [Unreleased] section.
29284
+ - Use Added, Changed, Deprecated, Removed, Fixed, Security.
29285
+ - Entries should be human-readable and user-facing.
29286
+ - Do not use raw commit messages as changelog bullets.
29287
+ - MERGE into existing ### Category under [Unreleased] — never append a second ### Added / ### Fixed block.
29288
+ - Apply with the host-native editor, preserving the repository changelog conventions.
29289
+ - If Unreleased already has duplicate category headings, normalize_only first.`;
29290
+ function changelogContext(root, range) {
29291
+ const cwd = path22.resolve(repoRoot3(root));
29292
+ if (range !== undefined && !isResolvableContextRange(cwd, range))
29293
+ return invalidRange(cwd);
29294
+ const resolvedRange = rangeArgOrDefault(range, cwd);
29295
+ if (!isResolvableContextRange(cwd, resolvedRange))
29296
+ return invalidRange(cwd);
29297
+ let stdout = "";
29298
+ stdout += printSection("Repository");
29299
+ stdout += `root: ${cwd}
29300
+ `;
29301
+ stdout += `branch: ${currentBranch(cwd)}
29302
+ `;
29303
+ stdout += `range: ${resolvedRange}
29304
+ `;
29305
+ stdout += printSection("Keep a Changelog Rules");
29306
+ stdout += CHANGELOG_RULES + `
29307
+ `;
29308
+ stdout += printSection("Existing CHANGELOG.md");
29309
+ const changelogPath = path22.join(cwd, "CHANGELOG.md");
29310
+ if (existsSync11(changelogPath)) {
29311
+ stdout += readTrimmed(changelogPath, 260) + `
29312
+ `;
29313
+ } else {
29314
+ stdout += `CHANGELOG.md not found.
29315
+ `;
29816
29316
  }
29817
- };
29818
- var executeInitApply = (args, directory, runtime) => {
29819
- const rejected = requireConfirmed(args.confirmed);
29820
- if (rejected)
29821
- return rejected;
29822
- const action = args.action;
29823
- if (action === "hygiene") {
29824
- const result = ensureHygieneFiles(directory, {
29825
- confirmed: args.confirmed,
29826
- includeOpenSource: args.include_open_source
29827
- });
29828
- return output(result.ok ? ok(result) : fail(result.error));
29317
+ stdout += printSection("Commits");
29318
+ stdout += commitLogForRange(cwd, resolvedRange) + `
29319
+ `;
29320
+ stdout += printSection("Diff Stat");
29321
+ stdout += diffStatForRange(cwd, resolvedRange) + `
29322
+ `;
29323
+ stdout += printSection("Changed Files");
29324
+ stdout += changedFilesForRange(cwd, resolvedRange) + `
29325
+ `;
29326
+ return { stdout, stderr: "", exitCode: 0, cwd };
29327
+ }
29328
+ function docsRefreshContext(root, range) {
29329
+ const cwd = path22.resolve(repoRoot3(root));
29330
+ if (range !== undefined && !isResolvableContextRange(cwd, range))
29331
+ return invalidRange(cwd);
29332
+ const resolvedRange = rangeArgOrDefault(range, cwd);
29333
+ if (!isResolvableContextRange(cwd, resolvedRange))
29334
+ return invalidRange(cwd);
29335
+ let stdout = "";
29336
+ stdout += printSection("Repository");
29337
+ stdout += `root: ${cwd}
29338
+ `;
29339
+ stdout += `branch: ${currentBranch(cwd)}
29340
+ `;
29341
+ stdout += `range: ${resolvedRange}
29342
+ `;
29343
+ stdout += printSection("Changed Files");
29344
+ stdout += changedFilesForRange(cwd, resolvedRange) + `
29345
+ `;
29346
+ stdout += printSection("Documentation Files");
29347
+ stdout += documentationFiles(cwd).join(`
29348
+ `) + `
29349
+ `;
29350
+ stdout += printSection("README Preview");
29351
+ const readme = path22.join(cwd, "README.md");
29352
+ if (existsSync11(readme)) {
29353
+ stdout += readTrimmed(readme, 220) + `
29354
+ `;
29355
+ } else {
29356
+ stdout += `README.md not found.
29357
+ `;
29829
29358
  }
29830
- if (action === "gitignore") {
29831
- const result = ensureProjectGitignore(directory, args.confirmed);
29832
- return output(result.ok ? ok(result) : fail(result.error));
29359
+ stdout += printSection("Package Scripts");
29360
+ const pkgPath = path22.join(cwd, "package.json");
29361
+ if (existsSync11(pkgPath)) {
29362
+ try {
29363
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
29364
+ stdout += JSON.stringify({ name: pkg.name, version: pkg.version, scripts: pkg.scripts }, null, 2) + `
29365
+ `;
29366
+ } catch {}
29367
+ } else {
29368
+ stdout += `package.json not found.
29369
+ `;
29833
29370
  }
29834
- if (action === "config") {
29835
- const LOCALE_RE = /^[a-z]{2,3}(-(?:[A-Z]{2}|[0-9]{3}))?$/;
29836
- if (args.locale !== undefined && !LOCALE_RE.test(args.locale)) {
29837
- return output(fail(`invalid locale: ${JSON.stringify(args.locale)} — expected BCP-47 like en or es-CL`));
29838
- }
29839
- const current = readConfig();
29840
- const next = mergeConfigValues({
29841
- locale: args.locale,
29842
- localeOptions: args.locale_options,
29843
- timezone: args.timezone,
29844
- preset: args.branch_policy_preset,
29845
- allowed: args.branch_policy_allowed,
29846
- protectedNames: args.branch_policy_protected
29847
- }, current);
29848
- writeConfig(next);
29849
- return output(ok({ action: "config", path: path22.join(configDir(), "config.json"), ...next }));
29371
+ return { stdout, stderr: "", exitCode: 0, cwd };
29372
+ }
29373
+ function releaseNotesContext(root, rangeOrTag) {
29374
+ const cwd = path22.resolve(repoRoot3(root));
29375
+ if (!rangeOrTag) {
29376
+ return { stdout: "", stderr: `ERROR: release tag or range required
29377
+ `, exitCode: 1, cwd };
29850
29378
  }
29851
- const env = Object.fromEntries(Object.entries({
29852
- WORKFLOW_YT_BASE_URL: args.base_url,
29853
- WORKFLOW_YT_MENTION: args.default_mention,
29854
- WORKFLOW_YT_MEETING_ISSUE: args.meeting_issue,
29855
- WORKFLOW_VCS_PROVIDER: args.vcs_provider,
29856
- WORKFLOW_VCS_TARGET_BRANCH: args.vcs_target_branch,
29857
- WORKFLOW_WORKSPACE_ROOT: directory,
29858
- WORKFLOW_BP_NAME: args.name,
29859
- WORKFLOW_BP_DEVELOP: args.develop_branch,
29860
- WORKFLOW_BP_INTEGRATION: args.integration
29861
- }).filter((entry) => entry[1] !== undefined));
29862
- return output(legacyScriptResult(runtime.initApply(directory, action, env)));
29863
- };
29864
-
29865
- // packages/workit-opencode/src/tools/repo.ts
29866
- var defaultRuntime = {
29867
- git: (root, args) => run2(root, "git", args),
29868
- verifyProject: (root, dryRun) => runVerifyProject(root, dryRun),
29869
- prContext: (root, range) => prReadyContext(root, range),
29870
- changelogContext: (root, range) => changelogContext(root, range),
29871
- docsContext: (root, range) => docsRefreshContext(root, range),
29872
- releaseContext: (root, range) => releaseNotesContext(root, range),
29873
- ...initApplyRuntime,
29874
- initStatus: (root) => ({
29875
- exitCode: 0,
29876
- stdout: JSON.stringify(initStatusData()),
29877
- stderr: "",
29878
- cwd: root
29879
- }),
29880
- toolkitStatus: async (root) => ({
29881
- exitCode: 0,
29882
- stdout: JSON.stringify(await toolkitStatusData()),
29883
- stderr: "",
29884
- cwd: root
29885
- })
29886
- };
29887
- var json2 = (stdout) => JSON.parse(stdout.trim());
29888
- var optionalJson = (value) => {
29889
- if (!value?.trim())
29890
- return null;
29891
- try {
29892
- return JSON.parse(value);
29893
- } catch {
29894
- return null;
29379
+ if (!isResolvableContextRange(cwd, rangeOrTag))
29380
+ return invalidRange(cwd);
29381
+ const resolvedRange = rangeArgOrDefault(rangeOrTag, cwd);
29382
+ if (!isResolvableContextRange(cwd, resolvedRange))
29383
+ return invalidRange(cwd);
29384
+ let stdout = "";
29385
+ stdout += printSection("Repository");
29386
+ stdout += `root: ${cwd}
29387
+ `;
29388
+ stdout += `branch: ${currentBranch(cwd)}
29389
+ `;
29390
+ stdout += `requested: ${rangeOrTag}
29391
+ `;
29392
+ stdout += `range: ${resolvedRange}
29393
+ `;
29394
+ stdout += printSection("Tags");
29395
+ const tags = runGit(cwd, ["tag", "--sort=-creatordate"]);
29396
+ if (tags.exitCode === 0)
29397
+ stdout += tags.stdout.split(`
29398
+ `).slice(0, 20).join(`
29399
+ `) + `
29400
+ `;
29401
+ stdout += printSection("Commits");
29402
+ stdout += commitLogForRange(cwd, resolvedRange) + `
29403
+ `;
29404
+ stdout += printSection("Diff Stat");
29405
+ stdout += diffStatForRange(cwd, resolvedRange) + `
29406
+ `;
29407
+ stdout += printSection("Changed Files");
29408
+ stdout += changedFilesForRange(cwd, resolvedRange) + `
29409
+ `;
29410
+ stdout += printSection("Existing Release Files");
29411
+ for (const rel of ["CHANGELOG.md", "RELEASE_NOTES.md", ".github/releases.md"]) {
29412
+ if (existsSync11(path22.join(cwd, rel)))
29413
+ stdout += rel + `
29414
+ `;
29895
29415
  }
29896
- };
29897
- var sections = (stdout) => parseSections(stdout);
29898
- var parsePr = (stdout) => {
29899
- const part = sections(stdout);
29900
- const repo = parseKeyValueLines(part.Repository ?? "", [
29901
- "branch",
29902
- "range",
29903
- "base_ref",
29904
- "merge_base",
29905
- "diff_range",
29906
- "range_mode",
29907
- "git_sync"
29908
- ]);
29909
- return {
29910
- ...repo,
29911
- commits: part.Commits ?? "",
29912
- diff_stat: part["Diff Stat"] ?? "",
29913
- files: part["Changed Files"] ?? "",
29914
- pr_template: part["PR Template"] ?? "",
29915
- vcs_config: optionalJson(part["VCS Config"]),
29916
- merged_pr_style: optionalJson(part["Merged PR Style"])
29917
- };
29918
- };
29919
- var parseChangelog = (stdout) => {
29920
- const part = sections(stdout);
29921
- const repo = parseKeyValueLines(part.Repository ?? "", ["branch", "range"]);
29922
- return {
29923
- ...repo,
29924
- changelog_excerpt: part["Existing CHANGELOG.md"] ?? "",
29925
- rules: part["Keep a Changelog Rules"] ?? "",
29926
- commits: part.Commits ?? "",
29927
- diff_stat: part["Diff Stat"] ?? "",
29928
- files: part["Changed Files"] ?? ""
29929
- };
29930
- };
29931
- var parseRelease = (stdout) => {
29932
- const part = sections(stdout);
29933
- const repo = parseKeyValueLines(part.Repository ?? "", ["requested", "range"]);
29934
- return {
29935
- ...repo,
29936
- tags: part.Tags ?? "",
29937
- commits: part.Commits ?? "",
29938
- diff_stat: part["Diff Stat"] ?? "",
29939
- files: part["Changed Files"] ?? "",
29940
- release_files: part["Existing Release Files"] ?? ""
29941
- };
29942
- };
29943
- var parseDocs = (stdout) => {
29944
- const part = sections(stdout);
29945
- const repo = parseKeyValueLines(part.Repository ?? "", ["branch", "range"]);
29946
- return {
29947
- ...repo,
29948
- changed_files: part["Changed Files"] ?? "",
29949
- readme_preview: part["README Preview"] ?? "",
29950
- package_scripts: part["Package Scripts"] ?? "",
29951
- files: part["Documentation Files"] ?? ""
29952
- };
29953
- };
29954
- function createRepoTools(runtime = defaultRuntime) {
29955
- const validateRange = (root, value) => {
29956
- for (const revision of gitRevisionParts(value)) {
29957
- const resolved = runtime.git(root, [
29958
- "rev-parse",
29959
- "--verify",
29960
- "--quiet",
29961
- "--end-of-options",
29962
- `${revision}^{commit}`
29963
- ]);
29964
- if (resolved.exitCode !== 0)
29965
- throw new Error(`invalid Git revision or range: ${value}`);
29966
- }
29967
- };
29968
- const contextWithRange = async (root, runContext, value, parse) => {
29969
- try {
29970
- if (value)
29971
- validateRange(root, value);
29972
- } catch (error) {
29973
- return output(fail(error instanceof Error ? error.message : "invalid Git revision or range"));
29974
- }
29975
- return output(scriptResult(await runContext(root, value), parse));
29976
- };
29977
- return {
29978
- workit_init_status: tool({
29979
- description: "Inspect toolkit initialization",
29980
- args: {},
29981
- execute: async (_input, context) => output(scriptResult(await runtime.initStatus(context.directory), json2))
29982
- }),
29983
- workit_status: tool({
29984
- description: "Inspect toolkit and repository state",
29985
- args: {},
29986
- execute: async (_input, context) => output(scriptResult(await runtime.toolkitStatus(context.directory), json2))
29987
- }),
29988
- workit_git_context: tool({
29989
- description: "Read Git branch and change context",
29990
- args: { paths: tool.schema.array(tool.schema.string()).optional() },
29991
- execute: async ({ paths }, context) => output(ok(gitContext(context.directory, paths ?? [])))
29992
- }),
29993
- workit_verify: tool({
29994
- description: "Discover and run repository verification",
29995
- args: { dry_run: tool.schema.boolean().optional() },
29996
- execute: async ({ dry_run }, context) => output(scriptResult(runtime.verifyProject(context.directory, Boolean(dry_run)), parseVerifyOutput))
29997
- }),
29998
- workit_pr_context: tool({
29999
- description: "Gather branch-exclusive PR context",
30000
- args: { range: tool.schema.string().optional() },
30001
- execute: async ({ range }, context) => contextWithRange(context.directory, runtime.prContext, range, parsePr)
30002
- }),
30003
- workit_changelog_context: tool({
30004
- description: "Gather changelog context",
30005
- args: { range: tool.schema.string().optional() },
30006
- execute: async ({ range }, context) => contextWithRange(context.directory, runtime.changelogContext, range, parseChangelog)
30007
- }),
30008
- workit_release_notes_context: tool({
30009
- description: "Gather release notes for an explicit range",
30010
- args: { range_or_tag: tool.schema.string() },
30011
- execute: async ({ range_or_tag }, context) => !range_or_tag.trim() ? output(fail("release tag or range required")) : contextWithRange(context.directory, (root, value) => runtime.releaseContext(root, value ?? ""), range_or_tag, parseRelease)
30012
- }),
30013
- workit_docs_context: tool({
30014
- description: "Gather documentation refresh context",
30015
- args: { range: tool.schema.string().optional() },
30016
- execute: async ({ range }, context) => output(scriptResult(runtime.docsContext(context.directory, range), parseDocs))
30017
- }),
30018
- workit_init_apply: tool({
30019
- description: "Apply a confirmed toolkit initialization action",
30020
- args: {
30021
- confirmed: tool.schema.boolean(),
30022
- action: tool.schema.enum([
30023
- "youtrack_scaffold",
30024
- "youtrack_json",
30025
- "youtrack_token_placeholder",
30026
- "vcs_scaffold",
30027
- "config",
30028
- "gitignore",
30029
- "hygiene",
30030
- "branch_policy"
30031
- ]),
30032
- base_url: tool.schema.string().optional(),
30033
- default_mention: tool.schema.string().optional(),
30034
- meeting_issue: tool.schema.string().optional(),
30035
- vcs_provider: tool.schema.enum(["gitlab", "github"]).optional(),
30036
- vcs_target_branch: tool.schema.string().optional(),
30037
- name: tool.schema.string().optional(),
30038
- develop_branch: tool.schema.string().optional(),
30039
- integration: tool.schema.enum(["pr", "merge"]).optional(),
30040
- locale: tool.schema.string().optional(),
30041
- locale_options: tool.schema.array(tool.schema.string()).optional(),
30042
- timezone: tool.schema.string().optional(),
30043
- branch_policy_preset: tool.schema.enum(["gitflow", "github-flow", "trunk-based", "custom"]).optional(),
30044
- branch_policy_allowed: tool.schema.array(tool.schema.string()).optional(),
30045
- branch_policy_protected: tool.schema.array(tool.schema.string()).optional(),
30046
- include_open_source: tool.schema.boolean().optional()
30047
- },
30048
- execute: async (args, context) => executeInitApply(args, context.directory, runtime)
30049
- })
30050
- };
30051
- }
29416
+ stdout += printSection("Release Notes Draft");
29417
+ stdout += `## Release notes (${resolvedRange})
30052
29418
 
30053
- // packages/workit-opencode/src/tools/context.ts
30054
- import { realpathSync as realpathSync10 } from "node:fs";
30055
- import path24 from "node:path";
29419
+ `;
29420
+ const commits = commitLogForRange(cwd, resolvedRange).split(`
29421
+ `).map((line) => line.trim()).filter(Boolean);
29422
+ stdout += commits.length ? `${commits.map((line) => `- ${line}`).join(`
29423
+ `)}
29424
+ ` : `- No commits in range.
29425
+ `;
29426
+ stdout += `
29427
+ ### Changed files
30056
29428
 
30057
- // packages/workit-core/src/core/pr-create.ts
30058
- import fs10 from "node:fs";
30059
- import path23 from "node:path";
30060
- import { spawnSync as spawnSync8 } from "node:child_process";
30061
- function parseGhIssue(value) {
30062
- const m = /issues\/(\d+)/.exec(value);
30063
- return m ? m[1] : String(value).trim().replace(/^#/, "");
30064
- }
30065
- function whichOnPath(tool) {
30066
- const names = process.platform === "win32" ? [tool, `${tool}.exe`, `${tool}.cmd`] : [tool];
30067
- for (const dir of (process.env.PATH ?? "").split(path23.delimiter)) {
30068
- if (!dir)
30069
- continue;
30070
- for (const name of names) {
30071
- const candidate = path23.join(dir, name);
30072
- try {
30073
- fs10.accessSync(candidate, fs10.constants.X_OK);
30074
- return candidate;
30075
- } catch {}
30076
- }
30077
- }
30078
- return null;
29429
+ `;
29430
+ const files = changedFilesForRange(cwd, resolvedRange).split(`
29431
+ `).map((line) => line.trim()).filter(Boolean);
29432
+ stdout += files.length ? `${files.map((line) => `- ${line}`).join(`
29433
+ `)}
29434
+ ` : `- None.
29435
+ `;
29436
+ return { stdout, stderr: "", exitCode: 0, cwd };
30079
29437
  }
30080
- var hostingCliAvailable = (provider) => whichOnPath(provider === "gitlab" ? "glab" : "gh") !== null;
30081
- var pushRemote = (cwd) => {
30082
- const result = spawnSync8("git", ["remote", "get-url", "--push", "--all", "origin"], {
30083
- cwd,
30084
- encoding: "utf8"
30085
- });
30086
- if (result.status !== 0)
30087
- return null;
30088
- const urls = (result.stdout ?? "").split(/\r?\n/u).map((url) => url.trim()).filter(Boolean);
30089
- return urls.length === 1 ? urls[0] : null;
30090
- };
30091
29438
 
30092
29439
  // packages/workit-core/src/core/tracker-issues.ts
30093
- import { spawnSync as spawnSync9 } from "node:child_process";
29440
+ import { spawnSync as spawnSync8 } from "node:child_process";
30094
29441
  var repoPathFromRemote = (remote) => {
30095
29442
  let rest = (remote || "").trim().replace(/\/+$/, "");
30096
29443
  if (!rest)
@@ -30111,7 +29458,7 @@ var defaultCli = (bin, available) => {
30111
29458
  if (!available)
30112
29459
  return null;
30113
29460
  return (args, root) => {
30114
- const out = spawnSync9(bin, args, {
29461
+ const out = spawnSync8(bin, args, {
30115
29462
  cwd: root,
30116
29463
  encoding: "utf8",
30117
29464
  env: { ...process.env, PATH: process.env.PATH ?? "" }
@@ -30278,7 +29625,7 @@ var createContextTool = () => tool({
30278
29625
  try {
30279
29626
  const payload = parsed.data.payload;
30280
29627
  const cwd = payload.cwd;
30281
- root = realpathSync10(cwd ? path24.resolve(context.directory, cwd) : context.directory);
29628
+ root = realpathSync10(cwd ? path23.resolve(context.directory, cwd) : context.directory);
30282
29629
  } catch {
30283
29630
  return JSON.stringify(failure("invalid_input", "action target directory cannot be resolved", {
30284
29631
  outcome: "not_started"
@@ -30290,14 +29637,14 @@ var createContextTool = () => tool({
30290
29637
 
30291
29638
  // packages/workit-opencode/src/shared/session.ts
30292
29639
  import { realpathSync as realpathSync11 } from "node:fs";
30293
- import path25 from "node:path";
29640
+ import path24 from "node:path";
30294
29641
  var sameWorkspace = (expected, observed) => {
30295
29642
  if (typeof observed !== "string" || !observed)
30296
29643
  return false;
30297
29644
  try {
30298
29645
  return realpathSync11(expected) === realpathSync11(observed);
30299
29646
  } catch {
30300
- return path25.resolve(expected) === path25.resolve(observed);
29647
+ return path24.resolve(expected) === path24.resolve(observed);
30301
29648
  }
30302
29649
  };
30303
29650
 
@@ -30773,7 +30120,7 @@ var observeQuestionEvent = (receipts, event) => {
30773
30120
  };
30774
30121
 
30775
30122
  // packages/workit-opencode/src/runtime.ts
30776
- import { readFileSync as readFileSync12 } from "node:fs";
30123
+ import { readFileSync as readFileSync11 } from "node:fs";
30777
30124
  var compactContextFor = (root, sessionID) => {
30778
30125
  try {
30779
30126
  const store = new TaskStore(root);
@@ -30839,7 +30186,7 @@ var workerContextFor = (root, sessionID, parentID, directChildren) => {
30839
30186
  };
30840
30187
  var loadProvenance = (logger, pkgUrl) => {
30841
30188
  try {
30842
- const pkg = JSON.parse(readFileSync12(pkgUrl, "utf8"));
30189
+ const pkg = JSON.parse(readFileSync11(pkgUrl, "utf8"));
30843
30190
  return {
30844
30191
  name: String(pkg.name ?? "workit-opencode"),
30845
30192
  version: String(pkg.version ?? "unknown")
@@ -30852,7 +30199,7 @@ var loadProvenance = (logger, pkgUrl) => {
30852
30199
 
30853
30200
  // packages/workit-opencode/src/v1/server.ts
30854
30201
  var root = assetsRoot();
30855
- var skillsPath = path26.join(root, "skills");
30202
+ var skillsPath = path25.join(root, "skills");
30856
30203
  var logger = createLogger({
30857
30204
  appLog: () => {
30858
30205
  return;
@@ -30871,7 +30218,7 @@ var warnStaleSources = () => {
30871
30218
  logger.warn(EVENT.hooks, {
30872
30219
  boundary: "stale_sources",
30873
30220
  reason: "workit sources changed after plugin load; restart the session for latest behavior",
30874
- files: changed.map((file) => path26.basename(file))
30221
+ files: changed.map((file) => path25.basename(file))
30875
30222
  });
30876
30223
  } catch {}
30877
30224
  };
@@ -30901,7 +30248,7 @@ var plugin = async ({ client, directory }) => {
30901
30248
  const dispatches = new Map;
30902
30249
  try {
30903
30250
  logger.info(EVENT.initialization, { host: "opencode", plugin_root: root });
30904
- logger.info(EVENT.provenance, loadProvenance(logger, path26.join(packageRoot2(), "package.json")));
30251
+ logger.info(EVENT.provenance, loadProvenance(logger, path25.join(packageRoot2(), "package.json")));
30905
30252
  } catch (error) {
30906
30253
  logger.warn(EVENT.hooks, { boundary: "initialization", ...errorDetail(error) });
30907
30254
  }
@@ -31194,7 +30541,7 @@ var plugin = async ({ client, directory }) => {
31194
30541
  config: async (config) => {
31195
30542
  const mutable = config;
31196
30543
  const paths = [...mutable.skills?.paths ?? []];
31197
- if (existsSync13(skillsPath) && !paths.includes(skillsPath))
30544
+ if (existsSync12(skillsPath) && !paths.includes(skillsPath))
31198
30545
  paths.push(skillsPath);
31199
30546
  mutable.skills ??= {};
31200
30547
  mutable.skills.paths = paths;
@@ -31836,8 +31183,8 @@ var normalizeQuestionAnswers = (answers) => {
31836
31183
  };
31837
31184
 
31838
31185
  // packages/workit-opencode/src/v2/registry.ts
31839
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
31840
- import path27 from "node:path";
31186
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "node:fs";
31187
+ import path26 from "node:path";
31841
31188
  var parseFrontmatter = (markdown) => {
31842
31189
  const match = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
31843
31190
  if (!match)
@@ -31857,8 +31204,8 @@ var parseFrontmatter = (markdown) => {
31857
31204
  };
31858
31205
  };
31859
31206
  var registerSkills = async (ctx) => {
31860
- const skillsDir = path27.join(assetsRoot(), "skills");
31861
- if (!existsSync14(skillsDir))
31207
+ const skillsDir = path26.join(assetsRoot(), "skills");
31208
+ if (!existsSync13(skillsDir))
31862
31209
  return new Set;
31863
31210
  const registered = new Set;
31864
31211
  await ctx.skill.transform((editor) => {
@@ -31866,10 +31213,10 @@ var registerSkills = async (ctx) => {
31866
31213
  for (const id of WORKIT_METHOD_SKILLS) {
31867
31214
  if (existing.has(id))
31868
31215
  continue;
31869
- const file = path27.join(skillsDir, id, "SKILL.md");
31870
- if (!existsSync14(file))
31216
+ const file = path26.join(skillsDir, id, "SKILL.md");
31217
+ if (!existsSync13(file))
31871
31218
  continue;
31872
- const parsed = parseFrontmatter(readFileSync13(file, "utf8"));
31219
+ const parsed = parseFrontmatter(readFileSync12(file, "utf8"));
31873
31220
  editor.add({
31874
31221
  id,
31875
31222
  name: parsed.name || id,