@brainervirus/workit-cli 0.7.1 → 0.8.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 (3) hide show
  1. package/README.md +28 -3
  2. package/dist/index.js +162 -97
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # @brainervirus/workit-cli
2
2
 
3
- The workit CLI — an interactive wizard that configures workit for OpenCode and/or Cursor.
3
+ The workit CLI — an interactive Ink wizard that configures workit for OpenCode and/or Cursor, plus an offline installation doctor.
4
+
5
+ ## Requirements
6
+
7
+ - **Node.js ≥ 22** — the published CLI is a self-contained Node bundle (no Bun runtime). Node 21 and below fail (`ERR_MODULE_NOT_FOUND`/ESM syntax or the `>=22` engine gate).
4
8
 
5
9
  ## Install
6
10
 
@@ -13,10 +17,31 @@ npx @brainervirus/workit-cli init
13
17
  ## Usage
14
18
 
15
19
  ```bash
16
- workit init
20
+ workit init # interactive setup wizard
21
+ workit doctor # offline installation health report
22
+ workit doctor --json # machine-readable report
23
+ workit # help
24
+ ```
25
+
26
+ `workit init` guides you through: platform selection (OpenCode/Cursor), global config (locale, timezone, branch policy), YouTrack, VCS, workspaces (path globs → provider), and project hygiene files. The wizard is a TTY application — `workit init` requires an interactive terminal and prints guidance (exiting nonzero) when stdin is not a TTY.
27
+
28
+ `workit doctor` checks the offline installation health and exits nonzero when problems are found; `--json` prints the full report as JSON instead of the human-readable table.
29
+
30
+ ## Behavior
31
+
32
+ - **Safe apply semantics** — a malformed `config.json` is detected before the wizard renders and reported as a friendly blocked output instead of crashing; the same guard runs after the Apply preview.
33
+ - **Stable interaction** — unchanged wizard inputs are no-ops; they settle without React render warnings and never discard draft state.
34
+ - **Clean terminal** — only `warn`/`error` diagnostics print to stderr; routine structured `info` records stay in the JSONL journal. Nonzero failures and human-readable errors remain visible.
35
+ - **Node support** — the packed CLI runs on Node 22+; installation on Node 22.19 emits no engine warning from workit's dependency tree.
36
+
37
+ ## Package scripts
38
+
39
+ ```bash
40
+ bun run build # bundle dist/index.js (self-contained, Node shebang) + assets
41
+ bun run typecheck # tsc --noEmit
17
42
  ```
18
43
 
19
- Guides you through: platform selection (OpenCode/Cursor), global config (locale, timezone, branch policy), YouTrack, VCS, workspaces (path globs → provider), and project hygiene files.
44
+ The build produces a nonsplitting `dist/index.js` with a portable `#!/usr/bin/env node` shebang and copies the deterministic `assets/` (templates) from core.
20
45
 
21
46
  ## Docs
22
47
 
package/dist/index.js CHANGED
@@ -42763,9 +42763,9 @@ function writeFileExclusive(file, content, mode) {
42763
42763
 
42764
42764
  // packages/workit-core/src/core/setup.ts
42765
42765
  import {
42766
- chmodSync,
42766
+ copyFileSync as copyFileSync2,
42767
42767
  cpSync as cpSync2,
42768
- existsSync as existsSync9,
42768
+ existsSync as existsSync8,
42769
42769
  mkdirSync as mkdirSync3,
42770
42770
  mkdtempSync,
42771
42771
  readFileSync as readFileSync8,
@@ -42872,7 +42872,6 @@ var readSetupState = (dir = resolveConfigDir()) => ({
42872
42872
  });
42873
42873
 
42874
42874
  // packages/workit-core/src/core/registration.ts
42875
- import { existsSync as existsSync6 } from "node:fs";
42876
42875
  import path6 from "node:path";
42877
42876
  var isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
42878
42877
  var named = (s, name) => s === name || s.startsWith(`${name}@`);
@@ -42880,7 +42879,7 @@ function isWorkitPlugin(value) {
42880
42879
  const s = String(value).replaceAll("\\", "/");
42881
42880
  const url = s.startsWith("file://") || s.startsWith("git+file://");
42882
42881
  const pkgPath = s.includes("/packages/workit-opencode/") || s.includes("/packages/workit-cursor/") || s.includes("/node_modules/@brainervirus/workit-opencode/") || s.includes("/node_modules/@brainervirus/workit-cursor/");
42883
- return named(s, "workflow-toolkit") || named(s, "workflow-toolkit-opencode") || named(s, "local/workflow-toolkit") || named(s, "@brainervirus/workit-opencode") || named(s, "@brainervirus/workit-cursor") || url && pkgPath || s.startsWith("git+file://") && s.includes("workflow-toolkit");
42882
+ return named(s, "workflow-toolkit") || named(s, "workflow-toolkit-opencode") || named(s, "local/workflow-toolkit") || named(s, "workit") || named(s, "local/workit") || named(s, "@brainervirus/workit-opencode") || named(s, "@brainervirus/workit-cursor") || url && pkgPath || s.startsWith("git+file://") && s.includes("workflow-toolkit");
42884
42883
  }
42885
42884
  function mergeOpenCodePlugins(plugin, pin) {
42886
42885
  const existing = Array.isArray(plugin) ? plugin : typeof plugin === "string" ? [plugin] : [];
@@ -42911,7 +42910,8 @@ function mergeOpenCodeConfig(config, pin) {
42911
42910
  }
42912
42911
  function mergeCursorEnabledPlugins(enabled) {
42913
42912
  const prev = isRecord(enabled) ? { ...enabled } : {};
42914
- const next = { ...prev, "workflow-toolkit": true };
42913
+ const next = { ...prev, workit: true };
42914
+ delete next["workflow-toolkit"];
42915
42915
  delete next["local/workflow-toolkit"];
42916
42916
  const changed = JSON.stringify(next) !== JSON.stringify(prev) ? ["enabled_plugins"] : [];
42917
42917
  return { config: next, changed };
@@ -42922,10 +42922,12 @@ function mergeCursorPluginDirs(pluginDirs, pluginDir) {
42922
42922
  return path6.dirname(j) === j ? j : j.replace(/[\\/]+$/, "");
42923
42923
  };
42924
42924
  const normalized = strip(pluginDir);
42925
+ const legacy = strip(path6.join(path6.dirname(pluginDir), "workflow-toolkit"));
42925
42926
  const prev = Array.isArray(pluginDirs) ? pluginDirs.map(String) : [];
42926
- const exists = prev.some((d) => strip(d) === normalized);
42927
- const next = exists ? prev : [...prev, normalized];
42928
- const changed = next.length !== prev.length ? ["plugin_dirs"] : [];
42927
+ const kept = prev.filter((d) => strip(d) !== legacy);
42928
+ const exists = kept.some((d) => strip(d) === normalized);
42929
+ const next = exists ? kept : [...kept, normalized];
42930
+ const changed = JSON.stringify(next) !== JSON.stringify(prev) ? ["plugin_dirs"] : [];
42929
42931
  return { config: next, changed };
42930
42932
  }
42931
42933
  function mergeCursorSettings(settings, pluginDir) {
@@ -42952,35 +42954,36 @@ function mergeCursorMcp(mcp, serverName, server) {
42952
42954
  base2.mcpServers = servers;
42953
42955
  return { config: base2, changed };
42954
42956
  }
42955
- function cursorMcpServerEntry(packageDir) {
42956
- if (existsSync6(path6.join(packageDir, "dist", "mcp-server.js"))) {
42957
- return {
42958
- command: "node",
42959
- args: [path6.join(packageDir, "dist", "mcp-server.js"), "${workspaceFolder}"]
42960
- };
42961
- }
42957
+ var CURSOR_RUNTIME_PACKAGE = "@brainervirus/workit-cursor@0.8.0";
42958
+ function cursorMcpServerEntry(_packageDir) {
42959
+ return {
42960
+ command: "npx",
42961
+ args: ["-y", `--package=${CURSOR_RUNTIME_PACKAGE}`, "workit-cursor-mcp", "${workspaceFolder}"]
42962
+ };
42963
+ }
42964
+ function cursorHooksEntry(_packageDir) {
42962
42965
  return {
42963
- command: "bash",
42964
- args: [path6.join(packageDir, "mcp", "run-server.sh"), "${workspaceFolder}"]
42966
+ command: `npx -y --package=${CURSOR_RUNTIME_PACKAGE} workit-cursor-session-start`,
42967
+ args: []
42965
42968
  };
42966
42969
  }
42967
42970
 
42968
42971
  // packages/workit-core/src/core/doctor.ts
42969
42972
  import { spawnSync as spawnSync2 } from "node:child_process";
42970
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync7, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
42973
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync7, statSync as statSync2, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
42971
42974
  import os4 from "node:os";
42972
42975
  import path8 from "node:path";
42973
42976
 
42974
42977
  // packages/workit-core/src/core/support-matrix.ts
42975
42978
  var SUPPORT_MATRIX = {
42976
42979
  bun: "1.3.14",
42977
- node: { minimum: "20", current: "22" },
42980
+ node: { minimum: "22", current: "22" },
42978
42981
  opencode: { minimum: "1.15.0", current: "1.17.7" },
42979
42982
  os: ["ubuntu-latest", "macos-latest", "windows-latest"]
42980
42983
  };
42981
42984
 
42982
42985
  // packages/workit-core/src/core/skill-manifests.ts
42983
- import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync } from "node:fs";
42986
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync } from "node:fs";
42984
42987
  import path7 from "node:path";
42985
42988
  var CANONICAL_SKILLS = {
42986
42989
  superpowers: [
@@ -43014,7 +43017,7 @@ var CANONICAL_SKILLS = {
43014
43017
  "wk-verify"
43015
43018
  ]
43016
43019
  };
43017
- var skillManifestNames = (root) => existsSync7(root) ? readdirSync2(root).filter((name) => existsSync7(path7.join(root, name, "SKILL.md"))).sort() : [];
43020
+ var skillManifestNames = (root) => existsSync6(root) ? readdirSync2(root).filter((name) => existsSync6(path7.join(root, name, "SKILL.md"))).sort() : [];
43018
43021
  var validateSkillManifests = (root, expected, label) => {
43019
43022
  const actual = skillManifestNames(root);
43020
43023
  const missing = expected.filter((name) => !actual.includes(name));
@@ -43050,7 +43053,7 @@ var TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
43050
43053
  var findDevFromCwd = (cwd2) => {
43051
43054
  let dir = path8.resolve(cwd2);
43052
43055
  while (true) {
43053
- if (existsSync8(path8.join(dir, "packages", "workit-core", "package.json")))
43056
+ if (existsSync7(path8.join(dir, "packages", "workit-core", "package.json")))
43054
43057
  return dir;
43055
43058
  const parent = path8.dirname(dir);
43056
43059
  if (parent === dir)
@@ -43075,7 +43078,7 @@ var resolve = (options) => {
43075
43078
  opencodeConfig: options.opencodeConfig ?? path8.join(home, ".config", "opencode", "opencode.json"),
43076
43079
  cursorSettings: options.cursorSettings ?? path8.join(home, ".cursor", "settings.json"),
43077
43080
  cursorMcp: options.cursorMcp ?? path8.join(home, ".cursor", "mcp.json"),
43078
- cursorPluginDir: options.cursorPluginDir ?? path8.join(home, ".cursor", "plugins", "local", "workflow-toolkit"),
43081
+ cursorPluginDir: options.cursorPluginDir ?? path8.join(home, ".cursor", "plugins", "local", "workit"),
43079
43082
  env: env3,
43080
43083
  installer: options.installer ?? false
43081
43084
  };
@@ -43136,14 +43139,14 @@ var semverAtLeast = (version, min2) => {
43136
43139
  return true;
43137
43140
  };
43138
43141
  var resolveBun = (env3) => {
43139
- if (env3.BUN && existsSync8(env3.BUN))
43142
+ if (env3.BUN && existsSync7(env3.BUN))
43140
43143
  return env3.BUN;
43141
43144
  for (const candidate of [
43142
43145
  path8.join(os4.homedir(), ".bun/bin/bun"),
43143
43146
  "/usr/local/bin/bun",
43144
43147
  "/usr/bin/bun"
43145
43148
  ]) {
43146
- if (existsSync8(candidate))
43149
+ if (existsSync7(candidate))
43147
43150
  return candidate;
43148
43151
  }
43149
43152
  return commandOnPath("bun", env3) ? "bun" : null;
@@ -43253,7 +43256,6 @@ var assetPathsFor = (host, dev) => {
43253
43256
  return [
43254
43257
  path8.join(pkg, "assets", "templates", "spec-template.md"),
43255
43258
  path8.join(pkg, "mcp.json"),
43256
- path8.join(pkg, "marketplace.json"),
43257
43259
  path8.join(pkg, ".cursor-plugin")
43258
43260
  ];
43259
43261
  case "cli":
@@ -43263,7 +43265,7 @@ var assetPathsFor = (host, dev) => {
43263
43265
  var hostsFor = (host) => host === "cli" ? ["opencode", "cursor", "cli"] : [host];
43264
43266
  var checkAssets = (res) => {
43265
43267
  const dev = res.dev;
43266
- const missing = dev ? hostsFor(res.host).flatMap((h) => assetPathsFor(h, dev).filter((p) => !existsSync8(p)).map((p) => `${h}: ${p}`)) : [];
43268
+ const missing = dev ? hostsFor(res.host).flatMap((h) => assetPathsFor(h, dev).filter((p) => !existsSync7(p)).map((p) => `${h}: ${p}`)) : [];
43267
43269
  if (res.host === "cursor" || res.host === "cli") {
43268
43270
  const cursorError = validateCursorSkills(res.cursorPluginDir);
43269
43271
  if (cursorError)
@@ -43297,11 +43299,8 @@ var launcherSlotsFor = (host, dev) => {
43297
43299
  return [[path8.join(pkg, "src", "plugin.ts"), path8.join(pkg, "dist", "plugin.js")]];
43298
43300
  case "cursor":
43299
43301
  return [
43300
- [path8.join(pkg, "dist", "mcp-server.js"), path8.join(pkg, "mcp", "run-server.sh")],
43301
- [
43302
- path8.join(pkg, "dist", "cursor-session-start.js"),
43303
- path8.join(pkg, "hooks", "session-start")
43304
- ]
43302
+ [path8.join(pkg, "dist", "mcp-server.js")],
43303
+ [path8.join(pkg, "dist", "cursor-session-start.js")]
43305
43304
  ];
43306
43305
  case "cli":
43307
43306
  return [[path8.join(pkg, "src", "index.tsx"), path8.join(pkg, "dist", "index.js")]];
@@ -43320,7 +43319,7 @@ var validNodeEntry = (entry, runtime, env3) => {
43320
43319
  }
43321
43320
  };
43322
43321
  var registeredCursorLauncher = (res) => {
43323
- if (!existsSync8(res.cursorMcp))
43322
+ if (!existsSync7(res.cursorMcp))
43324
43323
  return "invalid";
43325
43324
  const config = readJson(res.cursorMcp);
43326
43325
  if (!config)
@@ -43334,28 +43333,60 @@ var registeredCursorLauncher = (res) => {
43334
43333
  return "invalid";
43335
43334
  }
43336
43335
  const executable = path8.basename(command).toLowerCase();
43336
+ if (executable === "npx" || executable === "npx.exe" || executable === "npx.cmd") {
43337
+ if (args[0] !== "-y")
43338
+ return "invalid";
43339
+ if (args[1] !== `--package=${CURSOR_RUNTIME_PACKAGE}`)
43340
+ return "invalid";
43341
+ if (args[2] !== "workit-cursor-mcp")
43342
+ return "invalid";
43343
+ return { kind: "npx" };
43344
+ }
43337
43345
  if (executable !== "node" && executable !== "node.exe")
43338
43346
  return "invalid";
43339
43347
  return {
43348
+ kind: "node",
43340
43349
  runtime: command,
43341
43350
  entry: path8.isAbsolute(args[0]) ? args[0] : path8.resolve(path8.dirname(res.cursorMcp), args[0])
43342
43351
  };
43343
43352
  };
43353
+ var canonicalCursorHook = cursorHooksEntry("").command;
43354
+ var registeredCursorHook = (res) => {
43355
+ const hooksFile = path8.join(res.cursorPluginDir, "hooks", "hooks-cursor.json");
43356
+ if (!existsSync7(hooksFile))
43357
+ return "invalid";
43358
+ const config = readJson(hooksFile);
43359
+ if (!config)
43360
+ return "invalid";
43361
+ const sessionStart = config.hooks?.sessionStart;
43362
+ if (!Array.isArray(sessionStart) || sessionStart.length !== 1)
43363
+ return "invalid";
43364
+ const entry = sessionStart[0];
43365
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
43366
+ return "invalid";
43367
+ return typeof entry.command === "string" ? entry.command : "invalid";
43368
+ };
43344
43369
  var checkLauncher = (res) => {
43345
43370
  const dev = res.dev;
43346
43371
  const hosts = hostsFor(res.host);
43347
43372
  const registered = hosts.includes("cursor") ? registeredCursorLauncher(res) : null;
43348
- const runtime = registered && registered !== "invalid" ? registered.runtime : "node";
43373
+ const runtime = registered && registered !== "invalid" && registered.kind === "node" ? registered.runtime : "node";
43349
43374
  const missing = hosts.includes("cursor") ? ["dist/mcp-server.js", "dist/cursor-session-start.js"].map((rel) => path8.join(res.cursorPluginDir, rel)).filter((p) => !validNodeEntry(p, runtime, res.env)).map((p) => `cursor: ${p}`) : [];
43350
43375
  if (hosts.includes("cursor")) {
43351
43376
  if (registered === "invalid") {
43352
43377
  missing.push(`cursor: canonical workit MCP launcher in ${res.cursorMcp}`);
43353
- } else if (registered && !validNodeEntry(registered.entry, registered.runtime, res.env)) {
43378
+ } else if (registered && registered.kind === "node" && !validNodeEntry(registered.entry, registered.runtime, res.env)) {
43354
43379
  missing.push(`cursor: registered ${registered.entry}`);
43355
43380
  }
43381
+ const hook = registeredCursorHook(res);
43382
+ if (hook === "invalid") {
43383
+ missing.push(`cursor: canonical session-start hook in ${path8.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")}`);
43384
+ } else if (hook !== null && hook !== canonicalCursorHook) {
43385
+ missing.push(`cursor: canonical session-start hook in ${path8.join(res.cursorPluginDir, "hooks", "hooks-cursor.json")} (registered ${hook})`);
43386
+ }
43356
43387
  }
43357
43388
  if (dev) {
43358
- missing.push(...hosts.filter((h) => h !== "cursor").flatMap((h) => launcherSlotsFor(h, dev).filter((slot) => !slot.some((p) => existsSync8(p))).map((slot) => `${h}: ${slot.join(" or ")}`)));
43389
+ missing.push(...hosts.filter((h) => h !== "cursor").flatMap((h) => launcherSlotsFor(h, dev).filter((slot) => !slot.some((p) => existsSync7(p))).map((slot) => `${h}: ${slot.join(" or ")}`)));
43359
43390
  }
43360
43391
  if (missing.length > 0) {
43361
43392
  return {
@@ -43401,7 +43432,7 @@ var staleEntry = (entry) => {
43401
43432
  return "stale";
43402
43433
  if (entry.startsWith("file:")) {
43403
43434
  const target = entry.replace(/^file:\/\//, "").replace(/^file:/, "");
43404
- return existsSync8(target) ? "ok" : "missing-file";
43435
+ return existsSync7(target) ? "ok" : "missing-file";
43405
43436
  }
43406
43437
  return "ok";
43407
43438
  };
@@ -43413,7 +43444,7 @@ var checkStalePin = (res) => {
43413
43444
  detail: "opencode pin not inspected on the cursor host"
43414
43445
  };
43415
43446
  }
43416
- if (!existsSync8(res.opencodeConfig)) {
43447
+ if (!existsSync7(res.opencodeConfig)) {
43417
43448
  return { id: "stale_pin", status: "pass", detail: "no opencode config — not registered" };
43418
43449
  }
43419
43450
  const cfg = readJson(res.opencodeConfig);
@@ -43449,13 +43480,13 @@ var checkDuplicateRegistration = (res) => {
43449
43480
  const problems = [];
43450
43481
  const opencodeHost = res.host !== "cursor";
43451
43482
  const cursorHost = res.host !== "opencode";
43452
- if (opencodeHost && existsSync8(res.opencodeConfig)) {
43483
+ if (opencodeHost && existsSync7(res.opencodeConfig)) {
43453
43484
  const cfg = readJson(res.opencodeConfig);
43454
43485
  const entries = pluginEntries(cfg);
43455
43486
  if (entries.length > 1)
43456
43487
  problems.push(`opencode registers ${entries.length} workit plugin entries`);
43457
43488
  }
43458
- if (cursorHost && existsSync8(res.cursorSettings)) {
43489
+ if (cursorHost && existsSync7(res.cursorSettings)) {
43459
43490
  const settings = readJson(res.cursorSettings);
43460
43491
  const enabled = settings?.enabled_plugins;
43461
43492
  if (enabled && typeof enabled === "object") {
@@ -43463,11 +43494,14 @@ var checkDuplicateRegistration = (res) => {
43463
43494
  if (keys2.length > 1)
43464
43495
  problems.push(`cursor enables ${keys2.length} workit plugin identities (${keys2.join(", ")})`);
43465
43496
  }
43466
- const dirs = Array.isArray(settings?.plugin_dirs) ? settings.plugin_dirs.map(String).filter((d) => isWorkitPlugin(d) || d.includes("workflow-toolkit")) : [];
43497
+ const dirs = Array.isArray(settings?.plugin_dirs) ? settings.plugin_dirs.map(String).filter((d) => {
43498
+ const n = d.replaceAll("\\", "/").replace(/\/+$/, "");
43499
+ return isWorkitPlugin(d) || n.endsWith("local/workit") || n.endsWith("local/workflow-toolkit");
43500
+ }) : [];
43467
43501
  if (dirs.length > 1)
43468
43502
  problems.push(`cursor plugin_dirs has ${dirs.length} workit entries`);
43469
43503
  }
43470
- if (cursorHost && existsSync8(res.cursorMcp)) {
43504
+ if (cursorHost && existsSync7(res.cursorMcp)) {
43471
43505
  const mcp = readJson(res.cursorMcp);
43472
43506
  const servers = mcp?.mcpServers ?? {};
43473
43507
  if (servers && typeof servers === "object") {
@@ -43490,16 +43524,16 @@ var checkMalformedConfig = (res) => {
43490
43524
  const files = [];
43491
43525
  for (const name of ["config.json", "youtrack.json", "vcs.json", "workspaces.json"]) {
43492
43526
  const p = path8.join(res.configDir, name);
43493
- if (existsSync8(p))
43527
+ if (existsSync7(p))
43494
43528
  files.push(p);
43495
43529
  }
43496
43530
  const opencodeHost = res.host !== "cursor";
43497
43531
  const cursorHost = res.host !== "opencode";
43498
- if (opencodeHost && existsSync8(res.opencodeConfig))
43532
+ if (opencodeHost && existsSync7(res.opencodeConfig))
43499
43533
  files.push(res.opencodeConfig);
43500
- if (cursorHost && existsSync8(res.cursorSettings))
43534
+ if (cursorHost && existsSync7(res.cursorSettings))
43501
43535
  files.push(res.cursorSettings);
43502
- if (cursorHost && existsSync8(res.cursorMcp))
43536
+ if (cursorHost && existsSync7(res.cursorMcp))
43503
43537
  files.push(res.cursorMcp);
43504
43538
  const bad = files.filter((p) => !parsesAsConfigObject(p));
43505
43539
  if (bad.length === 0)
@@ -43513,7 +43547,7 @@ var checkMalformedConfig = (res) => {
43513
43547
  };
43514
43548
  var checkWorkspaceMismatch = (res) => {
43515
43549
  const file = path8.join(res.configDir, "workspaces.json");
43516
- if (!existsSync8(file))
43550
+ if (!existsSync7(file))
43517
43551
  return { id: "workspace_mismatch", status: "pass", detail: "no workspaces configured" };
43518
43552
  const ws = readJson(file);
43519
43553
  if (!Array.isArray(ws?.workspaces)) {
@@ -43545,7 +43579,7 @@ var checkCredentialMetadata = (res) => {
43545
43579
  const youtrackJson = readJson(path8.join(res.configDir, "youtrack.json"));
43546
43580
  if (youtrackJson) {
43547
43581
  tokenPaths.push(typeof youtrackJson.tokenFile === "string" ? youtrackJson.tokenFile : path8.join(res.configDir, "youtrack.token"));
43548
- } else if (existsSync8(path8.join(res.configDir, "youtrack.token"))) {
43582
+ } else if (existsSync7(path8.join(res.configDir, "youtrack.token"))) {
43549
43583
  tokenPaths.push(path8.join(res.configDir, "youtrack.token"));
43550
43584
  }
43551
43585
  const vcsJson = readJson(path8.join(res.configDir, "vcs.json"));
@@ -43554,7 +43588,7 @@ var checkCredentialMetadata = (res) => {
43554
43588
  if (provider && typeof provider === "object") {
43555
43589
  const tf = provider.tokenFile;
43556
43590
  tokenPaths.push(typeof tf === "string" ? tf : path8.join(res.configDir, `${key}.token`));
43557
- } else if (!vcsJson && existsSync8(path8.join(res.configDir, `${key}.token`))) {
43591
+ } else if (!vcsJson && existsSync7(path8.join(res.configDir, `${key}.token`))) {
43558
43592
  tokenPaths.push(path8.join(res.configDir, `${key}.token`));
43559
43593
  }
43560
43594
  }
@@ -43564,7 +43598,7 @@ var checkCredentialMetadata = (res) => {
43564
43598
  const problems = [];
43565
43599
  for (const raw of tokenPaths) {
43566
43600
  const p = path8.isAbsolute(raw) ? raw : path8.resolve(res.configDir, raw);
43567
- if (!existsSync8(p)) {
43601
+ if (!existsSync7(p)) {
43568
43602
  problems.push(`${p} is missing`);
43569
43603
  continue;
43570
43604
  }
@@ -43830,7 +43864,7 @@ function buildSetupPreview(values2, opts = {}) {
43830
43864
  path: ytPath,
43831
43865
  value: draft
43832
43866
  });
43833
- if (existsSync9(tokenPath))
43867
+ if (existsSync8(tokenPath))
43834
43868
  preserved.push(tokenPath);
43835
43869
  else
43836
43870
  mutations.push({
@@ -43869,7 +43903,7 @@ function buildSetupPreview(values2, opts = {}) {
43869
43903
  value: draft
43870
43904
  });
43871
43905
  const activeToken = values2.vcsProvider === "gitlab" ? gitlabToken : githubToken;
43872
- if (existsSync9(activeToken))
43906
+ if (existsSync8(activeToken))
43873
43907
  preserved.push(activeToken);
43874
43908
  else
43875
43909
  mutations.push({
@@ -43896,7 +43930,7 @@ function buildSetupPreview(values2, opts = {}) {
43896
43930
  if (values2.applyProject) {
43897
43931
  const root = path9.resolve(opts.cwd ?? process.cwd());
43898
43932
  const gitignorePath = path9.join(root, ".gitignore");
43899
- const existing = existsSync9(gitignorePath) ? readFileSync8(gitignorePath, "utf8") : "";
43933
+ const existing = existsSync8(gitignorePath) ? readFileSync8(gitignorePath, "utf8") : "";
43900
43934
  const existingLines = new Set(existing.split(`
43901
43935
  `).map((l) => l.trim()).filter(Boolean));
43902
43936
  const entries = GITIGNORE_ENTRIES.filter((e) => e.trim() !== "" && !existingLines.has(e.trim()));
@@ -43927,7 +43961,7 @@ var resolveSetupPaths = (options, configDir2) => {
43927
43961
  opencodeConfig: options.opencodeConfig ?? path9.join(home, ".config", "opencode", "opencode.json"),
43928
43962
  cursorSettings: options.cursorSettings ?? path9.join(home, ".cursor", "settings.json"),
43929
43963
  cursorMcp: options.cursorMcp ?? path9.join(home, ".cursor", "mcp.json"),
43930
- cursorPluginDir: options.cursorPluginDir ?? path9.join(home, ".cursor", "plugins", "local", "workflow-toolkit")
43964
+ cursorPluginDir: options.cursorPluginDir ?? path9.join(home, ".cursor", "plugins", "local", "workit")
43931
43965
  };
43932
43966
  };
43933
43967
  function resolveApply(preview, options) {
@@ -43943,7 +43977,7 @@ var readExisting = (p) => {
43943
43977
  try {
43944
43978
  raw = readFileSync8(p, "utf8");
43945
43979
  } catch {
43946
- if (existsSync9(p))
43980
+ if (existsSync8(p))
43947
43981
  return { kind: "malformed", error: `${p} is not readable` };
43948
43982
  return { kind: "missing" };
43949
43983
  }
@@ -44117,7 +44151,7 @@ function adapterRoot(platform2, res) {
44117
44151
  var opencodePin = (root) => {
44118
44152
  for (const rel of ["src/plugin.ts", "dist/plugin.js"]) {
44119
44153
  const entry = path9.join(root, rel);
44120
- if (existsSync9(entry))
44154
+ if (existsSync8(entry))
44121
44155
  return `file://${entry}`;
44122
44156
  }
44123
44157
  return null;
@@ -44165,15 +44199,11 @@ function applyOpenCode(root, res) {
44165
44199
  detail: `pinned ${pin}`
44166
44200
  };
44167
44201
  }
44168
- var cursorMcpManifest = (dir) => JSON.stringify({ mcpServers: { workit: cursorMcpServerEntry(dir) } }, null, 2) + `
44169
- `;
44170
44202
  var samePluginContent = (src, dest, relative = "") => {
44171
44203
  try {
44172
44204
  for (const entry of readdirSync3(src, { withFileTypes: true })) {
44173
44205
  if (entry.name === "node_modules")
44174
44206
  continue;
44175
- if (relative === "" && entry.name === "mcp.json")
44176
- continue;
44177
44207
  const source = path9.join(src, entry.name);
44178
44208
  const installed = path9.join(dest, entry.name);
44179
44209
  if (entry.isDirectory()) {
@@ -44184,15 +44214,13 @@ var samePluginContent = (src, dest, relative = "") => {
44184
44214
  }
44185
44215
  }
44186
44216
  for (const entry of readdirSync3(dest, { withFileTypes: true })) {
44187
- if (existsSync9(path9.join(src, entry.name)))
44217
+ if (existsSync8(path9.join(src, entry.name)))
44188
44218
  continue;
44189
44219
  const rel = path9.join(relative, entry.name);
44190
44220
  if (rel === ".workflow-toolkit-root")
44191
44221
  continue;
44192
44222
  if (relative === "rules" && entry.isFile() && entry.name.endsWith(".mdc"))
44193
44223
  continue;
44194
- if (relative === "" && entry.name === "mcp.json")
44195
- continue;
44196
44224
  return false;
44197
44225
  }
44198
44226
  return true;
@@ -44208,7 +44236,7 @@ var preservedCursorRules = (src, dest) => {
44208
44236
  for (const entry of readdirSync3(installedRules, { withFileTypes: true })) {
44209
44237
  if (!entry.isFile() || !entry.name.endsWith(".mdc"))
44210
44238
  continue;
44211
- if (existsSync9(path9.join(sourceRules, entry.name)))
44239
+ if (existsSync8(path9.join(sourceRules, entry.name)))
44212
44240
  continue;
44213
44241
  preserved.set(entry.name, readFileSync8(path9.join(installedRules, entry.name)));
44214
44242
  }
@@ -44217,10 +44245,10 @@ var preservedCursorRules = (src, dest) => {
44217
44245
  };
44218
44246
  function copyPluginDir(src, dest) {
44219
44247
  const marker = path9.join(dest, ".workflow-toolkit-root");
44220
- const synced = readFileSafe(marker)?.trim() === src && samePluginContent(src, dest) && readFileSafe(path9.join(dest, "mcp.json")) === cursorMcpManifest(dest);
44248
+ const synced = readFileSafe(marker)?.trim() === src && samePluginContent(src, dest);
44221
44249
  if (synced)
44222
44250
  return "Skipped";
44223
- const hadDir = existsSync9(dest);
44251
+ const hadDir = existsSync8(dest);
44224
44252
  const rules = preservedCursorRules(src, dest);
44225
44253
  const parent = path9.dirname(dest);
44226
44254
  mkdirSync3(parent, { recursive: true });
@@ -44238,11 +44266,6 @@ function copyPluginDir(src, dest) {
44238
44266
  }
44239
44267
  writeFileSync5(path9.join(stage, ".workflow-toolkit-root"), src + `
44240
44268
  `, "utf8");
44241
- for (const rel of ["hooks/session-start", "mcp/run-server.sh"]) {
44242
- try {
44243
- chmodSync(path9.join(stage, rel), 493);
44244
- } catch {}
44245
- }
44246
44269
  if (!samePluginContent(src, stage))
44247
44270
  throw new Error("staged adapter content is incomplete");
44248
44271
  if (hadDir)
@@ -44250,17 +44273,35 @@ function copyPluginDir(src, dest) {
44250
44273
  try {
44251
44274
  renameSync(stage, dest);
44252
44275
  } catch (error) {
44253
- if (hadDir && !existsSync9(dest))
44276
+ if (hadDir && !existsSync8(dest))
44254
44277
  renameSync(backup, dest);
44255
44278
  throw error;
44256
44279
  }
44257
- writeFileSync5(path9.join(dest, "mcp.json"), cursorMcpManifest(dest), "utf8");
44258
44280
  rmSync(backup, { recursive: true, force: true });
44259
44281
  } finally {
44260
44282
  rmSync(swap, { recursive: true, force: true });
44261
44283
  }
44262
44284
  return hadDir ? "Configured" : "Installed";
44263
44285
  }
44286
+ var removeLegacyCursorDir = (res) => {
44287
+ const legacy = path9.join(res.home, ".cursor", "plugins", "local", "workflow-toolkit");
44288
+ if (legacy === res.cursorPluginDir || !existsSync8(legacy))
44289
+ return;
44290
+ const legacyRules = path9.join(legacy, "rules");
44291
+ if (existsSync8(legacyRules)) {
44292
+ try {
44293
+ mkdirSync3(path9.join(res.cursorPluginDir, "rules"), { recursive: true });
44294
+ for (const entry of readdirSync3(legacyRules, { withFileTypes: true })) {
44295
+ if (!entry.isFile() || !entry.name.endsWith(".mdc"))
44296
+ continue;
44297
+ const target = path9.join(res.cursorPluginDir, "rules", entry.name);
44298
+ if (!existsSync8(target))
44299
+ copyFileSync2(path9.join(legacyRules, entry.name), target);
44300
+ }
44301
+ } catch {}
44302
+ }
44303
+ rmSync(legacy, { recursive: true, force: true });
44304
+ };
44264
44305
  function applyCursorSettings(root, res) {
44265
44306
  mkdirSync3(path9.dirname(res.cursorSettings), { recursive: true });
44266
44307
  const settingsExisting = readExisting(res.cursorSettings);
@@ -44318,9 +44359,9 @@ function applyCursorMcp(root, res) {
44318
44359
  };
44319
44360
  }
44320
44361
  function verifyPlatform(platform2, res) {
44321
- if (platform2 === "opencode" && !existsSync9(res.opencodeConfig))
44362
+ if (platform2 === "opencode" && !existsSync8(res.opencodeConfig))
44322
44363
  return null;
44323
- if (platform2 === "cursor" && !existsSync9(res.cursorSettings) && !existsSync9(res.cursorMcp)) {
44364
+ if (platform2 === "cursor" && !existsSync8(res.cursorSettings) && !existsSync8(res.cursorMcp)) {
44324
44365
  return null;
44325
44366
  }
44326
44367
  return runDoctor({
@@ -44364,6 +44405,7 @@ function applySetupPreview(preview, options = {}) {
44364
44405
  const res = resolveApply(preview, options);
44365
44406
  const entries = [];
44366
44407
  const rootFor = new Map;
44408
+ let cursorCopyOk = false;
44367
44409
  for (const mutation of preview.mutations) {
44368
44410
  if (mutation.type !== "register-platform" && mutation.type !== "install-adapter") {
44369
44411
  entries.push(applyMutation(mutation));
@@ -44396,7 +44438,9 @@ function applySetupPreview(preview, options = {}) {
44396
44438
  }
44397
44439
  if (mutation.type === "install-adapter") {
44398
44440
  try {
44399
- entries.push({ platform: platform2, file: mutation.path, status: copyPluginDir(root, mutation.path) });
44441
+ const status = copyPluginDir(root, mutation.path);
44442
+ cursorCopyOk = true;
44443
+ entries.push({ platform: platform2, file: mutation.path, status });
44400
44444
  } catch (error) {
44401
44445
  entries.push({
44402
44446
  platform: platform2,
@@ -44407,10 +44451,15 @@ function applySetupPreview(preview, options = {}) {
44407
44451
  }
44408
44452
  } else if (platform2 === "opencode") {
44409
44453
  entries.push(applyOpenCode(root, res));
44454
+ } else if (!cursorCopyOk) {
44455
+ continue;
44410
44456
  } else {
44411
44457
  entries.push(mutation.path === res.cursorSettings ? applyCursorSettings(root, res) : applyCursorMcp(root, res));
44412
44458
  }
44413
44459
  }
44460
+ if (preview.platforms.includes("cursor") && cursorCopyOk && !entries.some((e) => e.platform === "cursor" && e.status === "Failed")) {
44461
+ removeLegacyCursorDir(res);
44462
+ }
44414
44463
  for (const preserved of preview.preserved) {
44415
44464
  entries.push({
44416
44465
  platform: "core",
@@ -44558,8 +44607,9 @@ function applyWizardBranchPolicy(branchPolicy, workspace_root, env3 = process.en
44558
44607
  }
44559
44608
 
44560
44609
  // packages/workit-cli/src/wizard-state.ts
44561
- import { existsSync as existsSync10 } from "node:fs";
44610
+ import { existsSync as existsSync9 } from "node:fs";
44562
44611
  import path10 from "node:path";
44612
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
44563
44613
  var NEXT = {
44564
44614
  platforms: "locale",
44565
44615
  locale: "timezone",
@@ -44603,7 +44653,7 @@ var PREV = {
44603
44653
  exit: null
44604
44654
  };
44605
44655
  var skipsCustomBranch = (screen, preset) => (screen === "branchAllowed" || screen === "branchProtected") && preset !== "custom";
44606
- var skipsBranchPolicy = (screen) => (screen === "branchPolicy" || screen === "branchPolicyDevelop") && !existsSync10(path10.join(process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), ".git"));
44656
+ var skipsBranchPolicy = (screen) => (screen === "branchPolicy" || screen === "branchPolicyDevelop") && !existsSync9(path10.join(process.env.WORKFLOW_WORKSPACE_ROOT ?? process.cwd(), ".git"));
44607
44657
  function nextScreen(screen, preset) {
44608
44658
  let next = NEXT[screen];
44609
44659
  while (next && (skipsCustomBranch(next, preset) || skipsBranchPolicy(next)))
@@ -44660,6 +44710,9 @@ var decodeBranchPreset = (value, fallback) => value === "gitflow" || value === "
44660
44710
  var decodeVcsProvider = (value, fallback) => value === "gitlab" || value === "github" || value === "skip" ? value : fallback;
44661
44711
  function setTextValue(draft, field, value) {
44662
44712
  const message = field === "locale" ? validateLocale(value) : field === "timezone" ? validateTimezone(value) : field === "branchAllowed" ? parseList(value).length > 0 ? null : "at least one allowed branch pattern is required" : field === "branchProtected" ? parseList(value).length > 0 ? null : "at least one protected branch name is required" : value.trim() === "" ? null : validateBaseUrl(value);
44713
+ if (draft.values[field] === value && (draft.errors[field] ?? undefined) === (message ?? undefined)) {
44714
+ return draft;
44715
+ }
44663
44716
  const errors = { ...draft.errors };
44664
44717
  if (message)
44665
44718
  errors[field] = message;
@@ -44694,24 +44747,24 @@ function reducer3(draft, action) {
44694
44747
  case "set":
44695
44748
  switch (action.field) {
44696
44749
  case "platforms":
44750
+ if (isDeepStrictEqual3(action.value, draft.values.platforms))
44751
+ return draft;
44697
44752
  return { ...draft, values: { ...draft.values, platforms: action.value } };
44698
- case "branchPreset":
44699
- return {
44700
- ...draft,
44701
- values: {
44702
- ...draft.values,
44703
- branchPreset: decodeBranchPreset(action.value, draft.values.branchPreset)
44704
- }
44705
- };
44706
- case "vcsProvider":
44707
- return {
44708
- ...draft,
44709
- values: {
44710
- ...draft.values,
44711
- vcsProvider: decodeVcsProvider(action.value, draft.values.vcsProvider)
44712
- }
44713
- };
44753
+ case "branchPreset": {
44754
+ const next = decodeBranchPreset(action.value, draft.values.branchPreset);
44755
+ if (next === draft.values.branchPreset)
44756
+ return draft;
44757
+ return { ...draft, values: { ...draft.values, branchPreset: next } };
44758
+ }
44759
+ case "vcsProvider": {
44760
+ const next = decodeVcsProvider(action.value, draft.values.vcsProvider);
44761
+ if (next === draft.values.vcsProvider)
44762
+ return draft;
44763
+ return { ...draft, values: { ...draft.values, vcsProvider: next } };
44764
+ }
44714
44765
  case "applyProject":
44766
+ if (action.value === draft.values.applyProject)
44767
+ return draft;
44715
44768
  return { ...draft, values: { ...draft.values, applyProject: action.value } };
44716
44769
  case "branchPolicyDetected":
44717
44770
  return { ...draft, values: { ...draft.values, branchPolicyDetected: action.value } };
@@ -44732,6 +44785,8 @@ function reducer3(draft, action) {
44732
44785
  const base2 = draft.values.branchPolicy ?? draft.values.branchPolicyDetected;
44733
44786
  if (!base2)
44734
44787
  return draft;
44788
+ if (action.value === base2.integration)
44789
+ return draft;
44735
44790
  return {
44736
44791
  ...draft,
44737
44792
  values: {
@@ -44748,11 +44803,14 @@ function reducer3(draft, action) {
44748
44803
  const base2 = draft.values.branchPolicy ?? draft.values.branchPolicyDetected;
44749
44804
  if (!base2)
44750
44805
  return draft;
44806
+ const next = action.value || undefined;
44807
+ if ((base2.developBranch ?? undefined) === next)
44808
+ return draft;
44751
44809
  return {
44752
44810
  ...draft,
44753
44811
  values: {
44754
44812
  ...draft.values,
44755
- branchPolicy: { ...base2, developBranch: action.value || undefined }
44813
+ branchPolicy: { ...base2, developBranch: next }
44756
44814
  }
44757
44815
  };
44758
44816
  }
@@ -44874,6 +44932,9 @@ function defaultWorkspaceProvider(vcs) {
44874
44932
  function workspaceDraftText(draft, field, value) {
44875
44933
  const errorKey = field === "name" ? "workspaceName" : "workspaceGlob";
44876
44934
  const message = field === "name" ? value.trim() ? null : "workspace name is required" : value.trim() ? null : "workspace pattern is required";
44935
+ if ((draft.workspaceDraft?.[field] ?? "") === value && (draft.errors[errorKey] ?? undefined) === (message ?? undefined)) {
44936
+ return draft;
44937
+ }
44877
44938
  const errors = { ...draft.errors };
44878
44939
  if (message)
44879
44940
  errors[errorKey] = message;
@@ -46124,8 +46185,12 @@ var createLogger = (options = {}) => {
46124
46185
  // packages/workit-cli/src/index.tsx
46125
46186
  var jsx_dev_runtime2 = __toESM(require_jsx_dev_runtime(), 1);
46126
46187
  var logger = createLogger({
46127
- stderr: (event) => process.stderr.write(`${JSON.stringify(event)}
46128
- `)
46188
+ stderr: (event) => {
46189
+ if (event.level === "debug" || event.level === "info")
46190
+ return;
46191
+ process.stderr.write(`${JSON.stringify(event)}
46192
+ `);
46193
+ }
46129
46194
  });
46130
46195
  var HELP = `workit — workflow rails for agentic coding
46131
46196
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "description": "Workit interactive setup wizard (Ink TUI)",
6
6
  "keywords": [
@@ -34,14 +34,14 @@
34
34
  "typecheck": "tsc --noEmit"
35
35
  },
36
36
  "dependencies": {
37
- "@brainervirus/workit-core": "^0.7.1",
38
- "@brainervirus/workit-cursor": "^0.7.1",
39
- "@brainervirus/workit-opencode": "^0.7.1",
37
+ "@brainervirus/workit-core": "^0.8.1",
38
+ "@brainervirus/workit-cursor": "^0.8.1",
39
+ "@brainervirus/workit-opencode": "^0.8.1",
40
40
  "@inkjs/ui": "2.0.0",
41
41
  "ink": "7.1.1",
42
42
  "react": "19.2.8"
43
43
  },
44
44
  "engines": {
45
- "node": ">=20"
45
+ "node": ">=22"
46
46
  }
47
47
  }