@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 +1 -1
- package/src/core/doctor.ts +71 -8
- package/src/core/registration.ts +3 -0
- package/src/core/setup.ts +38 -9
package/package.json
CHANGED
package/src/core/doctor.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
672
|
-
//
|
|
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:
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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;
|
package/src/core/registration.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
894
|
+
cpSync(realSrc, stage, {
|
|
869
895
|
recursive: true,
|
|
870
|
-
filter: (entry) => !path.relative(
|
|
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"),
|
|
877
|
-
if (!samePluginContent(
|
|
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);
|