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