@brainervirus/workit-core 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainervirus/workit-core",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "private": false,
5
5
  "description": "Workit shared core — task, policy, evidence, review, decision, worker, and writer state for agentic coding workflows",
6
6
  "keywords": [
@@ -9,7 +9,16 @@
9
9
  // header for a login lookup — token bytes never enter the report, any fix
10
10
  // text, or any log event.
11
11
  import { spawnSync } from "node:child_process";
12
- import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
+ import {
13
+ existsSync,
14
+ lstatSync,
15
+ mkdirSync,
16
+ readFileSync,
17
+ readlinkSync,
18
+ statSync,
19
+ unlinkSync,
20
+ writeFileSync,
21
+ } from "node:fs";
13
22
  import os from "node:os";
14
23
  import path from "node:path";
15
24
  import { SUPPORT_MATRIX } from "./support-matrix";
@@ -661,15 +670,28 @@ const checkUtility = (res: Resolved): DoctorCheck => {
661
670
  };
662
671
  };
663
672
 
664
- const staleEntry = (entry: string): "ok" | "stale" | "missing-file" => {
673
+ /** file:// pins into package-manager ephemeral caches (pnpm dlx, npx). */
674
+ const isFragileCachePin = (entry: string): boolean => {
675
+ const n = entry.replaceAll("\\", "/").toLowerCase();
676
+ if (!n.startsWith("file:")) return false;
677
+ return (
678
+ n.includes("/caches/pnpm/dlx/") ||
679
+ n.includes("/_npx/") ||
680
+ n.includes("/.pacquet/") ||
681
+ n.includes("/library/caches/pnpm/")
682
+ );
683
+ };
684
+
685
+ const staleEntry = (entry: string): "ok" | "stale" | "missing-file" | "fragile-cache" => {
665
686
  if (!entry) return "stale";
666
687
  if (entry.includes("git+file")) return "stale";
688
+ if (isFragileCachePin(entry)) return "fragile-cache";
667
689
  if (entry.startsWith("file:")) {
668
690
  const target = entry.replace(/^file:\/\//, "").replace(/^file:/, "");
669
691
  return existsSync(target) ? "ok" : "missing-file";
670
692
  }
671
- // registry/ssh/git pins are not the canonical file pin, but are left to the
672
- // duplicate/versions checks rather than being labelled stale.
693
+ // Registry package names (e.g. @brainervirus/workit-opencode) are the
694
+ // published pin shape — OpenCode resolves them; not labelled stale.
673
695
  return "ok";
674
696
  };
675
697
 
@@ -695,7 +717,7 @@ const checkStalePin = (res: Resolved): DoctorCheck => {
695
717
  id: "stale_pin",
696
718
  status: "fail",
697
719
  detail: "no workit plugin registered in the opencode config",
698
- fix: "Run install-opencode-plugin.sh to pin the workit plugin entry",
720
+ fix: 'Run `workit init` and select OpenCode — pin "@brainervirus/workit-opencode"',
699
721
  };
700
722
  }
701
723
  const first = staleEntry(entries[0]);
@@ -704,7 +726,15 @@ const checkStalePin = (res: Resolved): DoctorCheck => {
704
726
  id: "stale_pin",
705
727
  status: "fail",
706
728
  detail: `stale workit pin: ${entries[0]}`,
707
- fix: "Re-run install-opencode-plugin.sh — the pin points at a git+file or non-file source",
729
+ fix: "Run `workit init` (or install-opencode-plugin.sh for a checkout) — replace git+file pins",
730
+ };
731
+ }
732
+ if (first === "fragile-cache") {
733
+ return {
734
+ id: "stale_pin",
735
+ status: "fail",
736
+ detail: `workit pin points at a package-manager cache path: ${entries[0]}`,
737
+ fix: 'Run `workit init` and select OpenCode — pin should be "@brainervirus/workit-opencode"',
708
738
  };
709
739
  }
710
740
  if (first === "missing-file") {
@@ -712,7 +742,7 @@ const checkStalePin = (res: Resolved): DoctorCheck => {
712
742
  id: "stale_pin",
713
743
  status: "fail",
714
744
  detail: `workit pin points at a missing file: ${entries[0]}`,
715
- fix: "Re-run install-opencode-plugin.sh after restoring the checkout the pin references",
745
+ fix: "Run `workit init` (or restore the checkout and re-run install-opencode-plugin.sh)",
716
746
  };
717
747
  }
718
748
  return { id: "stale_pin", status: "pass", detail: "opencode pin resolves" };
@@ -790,6 +820,30 @@ const checkStaleInstall = (res: Resolved): DoctorCheck & { registryProbed?: bool
790
820
  detail: "cursor plugin not inspected on the opencode host",
791
821
  };
792
822
  }
823
+ // Symlink installs into pnpm dlx / _npx caches break when the cache is cleared.
824
+ // MCP launchers may still be canonical; the plugin dir itself must be real.
825
+ if (existsSync(res.cursorPluginDir) && lstatSync(res.cursorPluginDir).isSymbolicLink()) {
826
+ let target = "";
827
+ try {
828
+ target = readlinkSync(res.cursorPluginDir);
829
+ } catch {
830
+ target = "(unreadable)";
831
+ }
832
+ const n = target.replaceAll("\\", "/").toLowerCase();
833
+ const fragile =
834
+ n.includes("/caches/pnpm/dlx/") ||
835
+ n.includes("/_npx/") ||
836
+ n.includes("/.pacquet/") ||
837
+ n.includes("/library/caches/pnpm/");
838
+ return {
839
+ id: "stale_install",
840
+ status: "fail",
841
+ detail: fragile
842
+ ? `stale_install: cursor plugin dir is a symlink into a package-manager cache (${target})`
843
+ : `stale_install: cursor plugin dir is a symlink (${target}) — expected a real directory copy`,
844
+ fix: "Run `workit init` and select Cursor — install must copy into ~/.cursor/plugins/local/workit",
845
+ };
846
+ }
793
847
  const canonicalMcp = cursorMcpServerEntry("").args.find((a) => a.startsWith("--package="));
794
848
  const mcpSelectors = pluginMcpSelectors(res);
795
849
  const legacyPin = (mcpSelectors ?? []).find(
@@ -1035,7 +1089,16 @@ const checkCredentialMetadata = (res: Resolved): DoctorCheck => {
1035
1089
  }
1036
1090
 
1037
1091
  const vcsJson = readJson(path.join(res.configDir, "vcs.json"));
1038
- for (const key of ["gitlab", "github"] as const) {
1092
+ // Only the active VCS provider's token is required. vcs.json scaffolds both
1093
+ // github and gitlab blocks for switching later — inactive tokenFile paths
1094
+ // must not fail the doctor when provider is set to the other host.
1095
+ const active =
1096
+ typeof vcsJson?.provider === "string" ? vcsJson.provider.trim().toLowerCase() : null;
1097
+ const vcsKeys =
1098
+ active === "gitlab" || active === "github"
1099
+ ? ([active] as const)
1100
+ : (["gitlab", "github"] as const);
1101
+ for (const key of vcsKeys) {
1039
1102
  const provider = vcsJson?.[key];
1040
1103
  if (provider && typeof provider === "object") {
1041
1104
  const tf = provider.tokenFile;
@@ -6,6 +6,9 @@
6
6
  // there is exactly one source of truth for registration merging.
7
7
  import path from "node:path";
8
8
 
9
+ /** Canonical published OpenCode plugin identity (OpenCode resolves from npm). */
10
+ export const OPENCODE_NPM_PIN = "@brainervirus/workit-opencode";
11
+
9
12
  export interface MergeResult<T> {
10
13
  config: T;
11
14
  changed: string[];
package/src/core/setup.ts CHANGED
@@ -2,10 +2,12 @@ import {
2
2
  copyFileSync,
3
3
  cpSync,
4
4
  existsSync,
5
+ lstatSync,
5
6
  mkdirSync,
6
7
  mkdtempSync,
7
8
  readFileSync,
8
9
  readdirSync,
10
+ realpathSync,
9
11
  renameSync,
10
12
  rmSync,
11
13
  statSync,
@@ -38,6 +40,7 @@ import {
38
40
  mergeCursorMcp,
39
41
  mergeCursorSettings,
40
42
  mergeOpenCodeConfig,
43
+ OPENCODE_NPM_PIN,
41
44
  } from "./registration";
42
45
  import { runDoctor, type DoctorReport } from "./doctor";
43
46
  import { writeFileExclusive } from "./safe-write";
@@ -750,7 +753,19 @@ function adapterRoot(platform: Platform, res: ResolvedApply): string | null {
750
753
  return null;
751
754
  }
752
755
 
753
- const opencodePin = (root: string): string | null => {
756
+ /** True when the adapter root is a monorepo checkout (dev pin), not a packed node_modules install. */
757
+ export const isDevOpenCodeAdapterRoot = (root: string, dev: string | null | undefined): boolean => {
758
+ if (dev) return true;
759
+ const norm = root.replaceAll("\\", "/");
760
+ return /\/packages\/workit-opencode\/?$/.test(norm);
761
+ };
762
+
763
+ /** Published installs pin the npm package; checkout installs pin file:// entry. */
764
+ export const resolveOpenCodePin = (
765
+ root: string,
766
+ opts: { dev?: string | null } = {},
767
+ ): string | null => {
768
+ if (!isDevOpenCodeAdapterRoot(root, opts.dev)) return OPENCODE_NPM_PIN;
754
769
  for (const rel of ["src/plugin.ts", "dist/plugin.js"]) {
755
770
  const entry = path.join(root, rel);
756
771
  if (existsSync(entry)) return `file://${entry}`;
@@ -758,8 +773,11 @@ const opencodePin = (root: string): string | null => {
758
773
  return null;
759
774
  };
760
775
 
776
+ const opencodePin = (root: string, res: ResolvedApply): string | null =>
777
+ resolveOpenCodePin(root, { dev: res.dev });
778
+
761
779
  function applyOpenCode(root: string, res: ResolvedApply): SetupResultEntry {
762
- const pin = opencodePin(root);
780
+ const pin = opencodePin(root, res);
763
781
  if (!pin) {
764
782
  return {
765
783
  platform: "opencode",
@@ -853,28 +871,39 @@ const preservedCursorRules = (src: string, dest: string): Map<string, Buffer> =>
853
871
  return preserved;
854
872
  };
855
873
 
856
- function copyPluginDir(src: string, dest: string): SetupResultStatus {
874
+ /** Install Cursor plugin files as a real directory (never a symlink into a package cache). */
875
+ export function copyPluginDir(src: string, dest: string): SetupResultStatus {
876
+ // pnpm dlx/pacquet (and similar) expose packages as symlinks into a content
877
+ // store. Node's cpSync copies a symlink *root* as a symlink, so the installed
878
+ // plugin would point into an ephemeral cache. Always materialize from the
879
+ // realpath so ~/.cursor/plugins/local/workit is a real directory.
880
+ const realSrc = realpathSync(src);
857
881
  const marker = path.join(dest, ".workit-root");
858
- const synced = readFileSafe(marker)?.trim() === src && samePluginContent(src, dest);
882
+ const destIsLink = existsSync(dest) && lstatSync(dest).isSymbolicLink();
883
+ const synced =
884
+ !destIsLink && readFileSafe(marker)?.trim() === realSrc && samePluginContent(realSrc, dest);
859
885
  if (synced) return "Skipped";
860
886
  const hadDir = existsSync(dest);
861
- const rules = preservedCursorRules(src, dest);
887
+ const rules = preservedCursorRules(realSrc, dest);
862
888
  const parent = path.dirname(dest);
863
889
  mkdirSync(parent, { recursive: true });
864
890
  const swap = mkdtempSync(path.join(parent, `.${path.basename(dest)}.swap-`));
865
891
  const stage = path.join(swap, "stage");
866
892
  const backup = path.join(swap, "backup");
867
893
  try {
868
- cpSync(src, stage, {
894
+ cpSync(realSrc, stage, {
869
895
  recursive: true,
870
- filter: (entry) => !path.relative(src, entry).split(path.sep).includes("node_modules"),
896
+ filter: (entry) => !path.relative(realSrc, entry).split(path.sep).includes("node_modules"),
871
897
  });
872
898
  for (const [name, content] of rules) {
873
899
  mkdirSync(path.join(stage, "rules"), { recursive: true });
874
900
  writeFileSync(path.join(stage, "rules", name), content);
875
901
  }
876
- writeFileSync(path.join(stage, ".workit-root"), src + "\n", "utf8");
877
- if (!samePluginContent(src, stage)) throw new Error("staged adapter content is incomplete");
902
+ writeFileSync(path.join(stage, ".workit-root"), realSrc + "\n", "utf8");
903
+ if (!samePluginContent(realSrc, stage)) throw new Error("staged adapter content is incomplete");
904
+ if (lstatSync(stage).isSymbolicLink()) {
905
+ throw new Error("staged adapter resolved to a symlink — refusing fragile cache install");
906
+ }
878
907
  if (hadDir) renameSync(dest, backup);
879
908
  try {
880
909
  renameSync(stage, dest);