@brainervirus/workit-core 2.9.0 → 3.0.0
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/README.md +2 -1
- package/package.json +1 -1
- package/scripts/bundled-deps.json +1 -7
- package/scripts/install-codex-plugin.sh +1 -2
- package/scripts/install-cursor-plugin.sh +3 -4
- package/scripts/install-opencode-plugin.sh +2 -3
- package/scripts/install-pi-package.sh +2 -3
- package/scripts/sync-runtime.sh +0 -13
- package/src/core/init.ts +1 -1
- package/src/core/support-matrix.ts +4 -4
- package/src/core/workspace-branch-policy.ts +123 -0
- package/src/hooks/hosts/opencode.ts +2 -2
- package/scripts/doctor-check.ts +0 -37
- package/src/core/config-conversion.ts +0 -180
- package/src/core/cutover.ts +0 -1789
- package/src/core/detect-hosts.ts +0 -115
- package/src/core/doctor.ts +0 -1882
- package/src/core/host-install.ts +0 -617
- package/src/core/legacy-ownership.ts +0 -47
- package/src/core/registration.ts +0 -306
- package/src/core/setup-state.ts +0 -61
- package/src/core/setup.ts +0 -1582
- package/src/core/uninstall.ts +0 -544
package/src/core/host-install.ts
DELETED
|
@@ -1,617 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
accessSync,
|
|
4
|
-
constants,
|
|
5
|
-
existsSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
readdirSync,
|
|
8
|
-
realpathSync,
|
|
9
|
-
statSync,
|
|
10
|
-
} from "node:fs";
|
|
11
|
-
import os from "node:os";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
|
|
14
|
-
export type HostId = "opencode" | "cursor" | "codex" | "pi" | "claude-code";
|
|
15
|
-
|
|
16
|
-
/** A reviewed argv vector. Arguments are never composed into a shell string. */
|
|
17
|
-
export type HostInstallCommand = {
|
|
18
|
-
command: string;
|
|
19
|
-
args: string[];
|
|
20
|
-
cwd?: string;
|
|
21
|
-
purpose: string;
|
|
22
|
-
/** Optional managed package tree to stage, for Cursor's local plugin copy. */
|
|
23
|
-
packageRoot?: string;
|
|
24
|
-
packageTarget?: string;
|
|
25
|
-
skipWhenOutputIncludes?: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type HostCommandResult = {
|
|
29
|
-
exitCode: number;
|
|
30
|
-
stdout: string;
|
|
31
|
-
stderr: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type HostCommandRunner = (step: HostInstallCommand) => HostCommandResult;
|
|
35
|
-
export type HostCommandContext = { home?: string; env?: NodeJS.ProcessEnv };
|
|
36
|
-
|
|
37
|
-
export function hostCommand(
|
|
38
|
-
name: string,
|
|
39
|
-
args: string[],
|
|
40
|
-
options: HostInstallOptions = {},
|
|
41
|
-
): HostInstallCommand {
|
|
42
|
-
const executable = findHostExecutable(name, options);
|
|
43
|
-
if (!executable)
|
|
44
|
-
throw new Error(`${name} executable was not found on PATH or supported user bin directories`);
|
|
45
|
-
if (process.platform === "win32" && /\.(?:cmd|bat)$/i.test(executable)) {
|
|
46
|
-
const node = findHostExecutable("node", options);
|
|
47
|
-
if (!node) throw new Error(`Node.js is required to run the ${name} Windows shim`);
|
|
48
|
-
const script = resolveWindowsShimScript(executable);
|
|
49
|
-
if (!script)
|
|
50
|
-
throw new Error(`could not resolve the ${name} Windows shim to a JavaScript entrypoint`);
|
|
51
|
-
return { command: node, args: [script, ...args], cwd: options.cwd, purpose: `${name} CLI` };
|
|
52
|
-
}
|
|
53
|
-
return { command: executable, args, cwd: options.cwd, purpose: `${name} CLI` };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function resolveWindowsShimEntry(
|
|
57
|
-
contents: string,
|
|
58
|
-
shim: string,
|
|
59
|
-
exists: (candidate: string) => boolean = existsSync,
|
|
60
|
-
): string | null {
|
|
61
|
-
const root = path.win32.dirname(shim);
|
|
62
|
-
const matches = contents.matchAll(/(["']?)([^\r\n"']*node_modules[\\/][^\r\n"']+?\.js)\1/gi);
|
|
63
|
-
for (const match of matches) {
|
|
64
|
-
const raw = match[2].replace(/%~?dp0%?/gi, root);
|
|
65
|
-
const candidate = path.win32.isAbsolute(raw) ? raw : path.win32.resolve(root, raw);
|
|
66
|
-
if (exists(candidate)) return candidate;
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const resolveWindowsShimScript = (shim: string): string | null => {
|
|
72
|
-
try {
|
|
73
|
-
const contents = readFileSync(shim, "utf8");
|
|
74
|
-
return resolveWindowsShimEntry(contents, shim);
|
|
75
|
-
} catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type HostInstallOptions = {
|
|
81
|
-
home?: string;
|
|
82
|
-
cwd?: string;
|
|
83
|
-
env?: NodeJS.ProcessEnv;
|
|
84
|
-
packageVersion?: string;
|
|
85
|
-
alreadyInstalled?: boolean;
|
|
86
|
-
targetDir?: string;
|
|
87
|
-
mode?: "install" | "upgrade";
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const MARKETPLACE = "https://github.com/BrainerVirus/workit.git";
|
|
91
|
-
const CODEX_MARKETPLACE_NAME = "workflow-toolkit";
|
|
92
|
-
/** Claude Code reads the git-hosted marketplace at the repo root
|
|
93
|
-
* (`.claude-plugin/marketplace.json`, name `workit`); its entry installs the
|
|
94
|
-
* published npm package. */
|
|
95
|
-
export const CLAUDE_MARKETPLACE_REPO = "BrainerVirus/workit";
|
|
96
|
-
export const CLAUDE_MARKETPLACE_NAME = "workit";
|
|
97
|
-
export const CLAUDE_PLUGIN_ID = `workit@${CLAUDE_MARKETPLACE_NAME}`;
|
|
98
|
-
const validPackageVersion = (value: string): boolean =>
|
|
99
|
-
value === "latest" || /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
|
|
100
|
-
|
|
101
|
-
const nodePrerequisite = (options: HostInstallOptions): HostInstallCommand => {
|
|
102
|
-
const node = findHostExecutable("node", options);
|
|
103
|
-
if (!node) throw new Error("Workit host packages require Node.js 24+; install Node.js and retry");
|
|
104
|
-
return {
|
|
105
|
-
command: node,
|
|
106
|
-
args: [
|
|
107
|
-
"-e",
|
|
108
|
-
'const v=Number(process.versions.node.split(".")[0]);if(v<24){console.error(`Workit host packages require Node.js 24+ (found ${process.versions.node})`);process.exit(1)}',
|
|
109
|
-
],
|
|
110
|
-
cwd: options.cwd,
|
|
111
|
-
purpose: "Check the Node.js 24+ package runtime requirement",
|
|
112
|
-
};
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/** Resolve host executables through PATH plus common user-level Node managers. */
|
|
116
|
-
export function findHostExecutable(
|
|
117
|
-
name: string,
|
|
118
|
-
options: { home?: string; env?: NodeJS.ProcessEnv } = {},
|
|
119
|
-
): string | null {
|
|
120
|
-
const env = options.env ?? process.env;
|
|
121
|
-
const home = options.home ?? env.HOME ?? os.homedir();
|
|
122
|
-
const suffixes = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
123
|
-
const dirs = (env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
124
|
-
dirs.push(path.join(home, ".local", "bin"), path.join(home, ".asdf", "shims"));
|
|
125
|
-
for (const [base, tail] of [
|
|
126
|
-
[path.join(home, ".local", "share", "fnm", "node-versions"), path.join("installation", "bin")],
|
|
127
|
-
[path.join(home, ".nvm", "versions", "node"), "bin"],
|
|
128
|
-
] as const) {
|
|
129
|
-
try {
|
|
130
|
-
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
131
|
-
if (entry.isDirectory()) dirs.push(path.join(base, entry.name, tail));
|
|
132
|
-
}
|
|
133
|
-
} catch {
|
|
134
|
-
// A missing version-manager directory is normal.
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
for (const dir of dirs) {
|
|
138
|
-
for (const suffix of suffixes) {
|
|
139
|
-
const candidate = path.join(dir, `${name}${suffix}`);
|
|
140
|
-
try {
|
|
141
|
-
if (statSync(candidate).isFile()) {
|
|
142
|
-
if (process.platform === "win32") return candidate;
|
|
143
|
-
accessSync(candidate, constants.X_OK);
|
|
144
|
-
return candidate;
|
|
145
|
-
}
|
|
146
|
-
} catch {
|
|
147
|
-
// Continue through PATH entries and platform shims.
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export const isPiWorkitInstalled = (
|
|
155
|
-
home: string,
|
|
156
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
157
|
-
): boolean => {
|
|
158
|
-
const settings = path.join(
|
|
159
|
-
env.PI_CODING_AGENT_DIR ?? path.join(home, ".pi", "agent"),
|
|
160
|
-
"settings.json",
|
|
161
|
-
);
|
|
162
|
-
try {
|
|
163
|
-
const document = JSON.parse(readFileSync(settings, "utf8")) as { packages?: unknown[] };
|
|
164
|
-
return (document.packages ?? []).some((entry) => {
|
|
165
|
-
const source =
|
|
166
|
-
entry && typeof entry === "object" && !Array.isArray(entry)
|
|
167
|
-
? (entry as { source?: unknown }).source
|
|
168
|
-
: entry;
|
|
169
|
-
const value = String(source).replaceAll("\\", "/").replace(/\/+$/, "");
|
|
170
|
-
return (
|
|
171
|
-
/^npm:@brainervirus\/workit-pi(?:@[^/]+)?$/.test(value) ||
|
|
172
|
-
value === "@brainervirus/workit-pi" ||
|
|
173
|
-
/(?:^|\/)packages\/workit-pi$/.test(value)
|
|
174
|
-
);
|
|
175
|
-
});
|
|
176
|
-
} catch {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const isCanonicalCodexPackage = (root: string): boolean => {
|
|
182
|
-
for (const file of [
|
|
183
|
-
path.join(root, ".codex-plugin", "plugin.json"),
|
|
184
|
-
path.join(root, "package.json"),
|
|
185
|
-
]) {
|
|
186
|
-
try {
|
|
187
|
-
const manifest = JSON.parse(readFileSync(file, "utf8")) as {
|
|
188
|
-
name?: string;
|
|
189
|
-
repository?: string | { url?: string };
|
|
190
|
-
};
|
|
191
|
-
const repository =
|
|
192
|
-
typeof manifest.repository === "string" ? manifest.repository : manifest.repository?.url;
|
|
193
|
-
if (
|
|
194
|
-
(manifest.name === "workit" || manifest.name === "@brainervirus/workit-codex") &&
|
|
195
|
-
repository?.replace(/\.git$/, "").toLowerCase() === "https://github.com/brainervirus/workit"
|
|
196
|
-
)
|
|
197
|
-
return true;
|
|
198
|
-
} catch {
|
|
199
|
-
// Try the other manifest path.
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return false;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
export const isCodexWorkitInstalled = (
|
|
206
|
-
home: string,
|
|
207
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
208
|
-
): boolean => {
|
|
209
|
-
const codexHome = env.CODEX_HOME ?? path.join(home, ".codex");
|
|
210
|
-
const isPackageOrVersionDirectory = (root: string): boolean => {
|
|
211
|
-
if (isCanonicalCodexPackage(root)) return true;
|
|
212
|
-
try {
|
|
213
|
-
return readdirSync(root, { withFileTypes: true }).some(
|
|
214
|
-
(entry) => entry.isDirectory() && isCanonicalCodexPackage(path.join(root, entry.name)),
|
|
215
|
-
);
|
|
216
|
-
} catch {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
if (isPackageOrVersionDirectory(path.join(codexHome, "plugins", "workit"))) return true;
|
|
221
|
-
const root = path.join(codexHome, "plugins", "cache");
|
|
222
|
-
try {
|
|
223
|
-
for (const marketplace of readdirSync(root, { withFileTypes: true })) {
|
|
224
|
-
if (!marketplace.isDirectory()) continue;
|
|
225
|
-
if (isPackageOrVersionDirectory(path.join(root, marketplace.name, "workit"))) return true;
|
|
226
|
-
}
|
|
227
|
-
} catch {
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
return false;
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/** Claude Code's config root (`CLAUDE_CONFIG_DIR`, default `~/.claude`). */
|
|
234
|
-
export const claudeConfigDir = (home: string, env: NodeJS.ProcessEnv = process.env): string =>
|
|
235
|
-
env.CLAUDE_CONFIG_DIR ?? path.join(home, ".claude");
|
|
236
|
-
|
|
237
|
-
export type ClaudeInstallScope = "user" | "project" | "local";
|
|
238
|
-
|
|
239
|
-
export type ClaudeWorkitInstall = {
|
|
240
|
-
id: string;
|
|
241
|
-
version: string | null;
|
|
242
|
-
installPath: string;
|
|
243
|
-
scope: ClaudeInstallScope;
|
|
244
|
-
/** The project a project/local-scope install belongs to; null for user scope. */
|
|
245
|
-
projectPath: string | null;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const CLAUDE_SCOPES = new Set<string>(["user", "project", "local"]);
|
|
249
|
-
|
|
250
|
-
/** A project/local install applies to `cwd` when cwd is inside its project. */
|
|
251
|
-
const appliesTo = (install: ClaudeWorkitInstall, cwd: string | null): boolean => {
|
|
252
|
-
if (install.scope === "user") return true;
|
|
253
|
-
if (cwd === null || install.projectPath === null) return false;
|
|
254
|
-
const relative = path.relative(path.resolve(install.projectPath), path.resolve(cwd));
|
|
255
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const isCanonicalClaudePlugin = (root: string): boolean => {
|
|
259
|
-
try {
|
|
260
|
-
const manifest = JSON.parse(
|
|
261
|
-
readFileSync(path.join(root, ".claude-plugin", "plugin.json"), "utf8"),
|
|
262
|
-
) as { name?: string; repository?: string | { url?: string } };
|
|
263
|
-
const repository =
|
|
264
|
-
typeof manifest.repository === "string" ? manifest.repository : manifest.repository?.url;
|
|
265
|
-
return (
|
|
266
|
-
manifest.name === "workit" &&
|
|
267
|
-
repository?.replace(/\.git$/, "").toLowerCase() === "https://github.com/brainervirus/workit"
|
|
268
|
-
);
|
|
269
|
-
} catch {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Workit installs recorded in Claude Code's plugin registry
|
|
276
|
-
* (`<config>/plugins/installed_plugins.json`, v2: `{plugins: {"name@market":
|
|
277
|
-
* [{installPath, version}]}}`). Only entries whose install directory carries
|
|
278
|
-
* the canonical Workit manifest count, whatever marketplace they came from.
|
|
279
|
-
* Only installs that apply here count: user scope always, project/local
|
|
280
|
-
* scope only when `cwd` is inside the recorded project (`cwd: null` keeps
|
|
281
|
-
* user scope only). A `--plugin-dir` pin is per-session and never recorded.
|
|
282
|
-
*/
|
|
283
|
-
export const claudeWorkitInstalls = (
|
|
284
|
-
home: string,
|
|
285
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
286
|
-
cwd: string | null = process.cwd(),
|
|
287
|
-
): ClaudeWorkitInstall[] => {
|
|
288
|
-
const file = path.join(claudeConfigDir(home, env), "plugins", "installed_plugins.json");
|
|
289
|
-
let registry: unknown;
|
|
290
|
-
try {
|
|
291
|
-
registry = JSON.parse(readFileSync(file, "utf8"));
|
|
292
|
-
} catch {
|
|
293
|
-
return [];
|
|
294
|
-
}
|
|
295
|
-
const plugins =
|
|
296
|
-
registry && typeof registry === "object" && "plugins" in registry ? registry.plugins : null;
|
|
297
|
-
if (!plugins || typeof plugins !== "object") return [];
|
|
298
|
-
const found: ClaudeWorkitInstall[] = [];
|
|
299
|
-
for (const [id, entries] of Object.entries(plugins as Record<string, unknown>)) {
|
|
300
|
-
if (!id.startsWith("workit@") || !Array.isArray(entries)) continue;
|
|
301
|
-
for (const entry of entries as Array<{
|
|
302
|
-
installPath?: unknown;
|
|
303
|
-
version?: unknown;
|
|
304
|
-
scope?: unknown;
|
|
305
|
-
projectPath?: unknown;
|
|
306
|
-
}>) {
|
|
307
|
-
if (typeof entry?.installPath !== "string" || !isCanonicalClaudePlugin(entry.installPath))
|
|
308
|
-
continue;
|
|
309
|
-
// Older registries omit scope; Claude's default install scope is user.
|
|
310
|
-
const scope = entry.scope === undefined ? "user" : String(entry.scope);
|
|
311
|
-
if (!CLAUDE_SCOPES.has(scope)) continue; // e.g. managed: not ours to change
|
|
312
|
-
const install: ClaudeWorkitInstall = {
|
|
313
|
-
id,
|
|
314
|
-
installPath: entry.installPath,
|
|
315
|
-
version:
|
|
316
|
-
typeof entry.version === "string" && /^\d+\.\d+\.\d+/.test(entry.version)
|
|
317
|
-
? entry.version
|
|
318
|
-
: null,
|
|
319
|
-
scope: scope as ClaudeInstallScope,
|
|
320
|
-
projectPath:
|
|
321
|
-
scope !== "user" && typeof entry.projectPath === "string" ? entry.projectPath : null,
|
|
322
|
-
};
|
|
323
|
-
if (appliesTo(install, cwd)) found.push(install);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return found;
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
/** A Workit install Claude Code loads for this user (any project). */
|
|
330
|
-
export const isClaudeWorkitInstalled = (
|
|
331
|
-
home: string,
|
|
332
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
333
|
-
cwd: string | null = process.cwd(),
|
|
334
|
-
): boolean => claudeWorkitInstalls(home, env, cwd).length > 0;
|
|
335
|
-
|
|
336
|
-
export function installedHostApp(
|
|
337
|
-
host: HostId,
|
|
338
|
-
home: string,
|
|
339
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
340
|
-
platform: NodeJS.Platform = process.platform,
|
|
341
|
-
exists: (candidate: string) => boolean = existsSync,
|
|
342
|
-
): boolean {
|
|
343
|
-
const candidates: string[] = [];
|
|
344
|
-
const pathApi = platform === "win32" ? path.win32 : path;
|
|
345
|
-
if (platform === "darwin") {
|
|
346
|
-
const app =
|
|
347
|
-
host === "cursor"
|
|
348
|
-
? "Cursor.app"
|
|
349
|
-
: host === "codex"
|
|
350
|
-
? "Codex.app"
|
|
351
|
-
: host === "opencode"
|
|
352
|
-
? "OpenCode.app"
|
|
353
|
-
: "";
|
|
354
|
-
if (app) candidates.push(`/Applications/${app}`, pathApi.join(home, "Applications", app));
|
|
355
|
-
} else if (platform === "win32") {
|
|
356
|
-
const local = env.LOCALAPPDATA ?? pathApi.join(home, "AppData", "Local");
|
|
357
|
-
if (host === "cursor") candidates.push(pathApi.join(local, "Programs", "Cursor", "Cursor.exe"));
|
|
358
|
-
if (host === "codex") candidates.push(pathApi.join(local, "Programs", "Codex", "Codex.exe"));
|
|
359
|
-
if (host === "opencode")
|
|
360
|
-
candidates.push(pathApi.join(local, "Programs", "OpenCode", "OpenCode.exe"));
|
|
361
|
-
}
|
|
362
|
-
return candidates.some(exists);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/** Plan the host-native registration commands used by setup and upgrade. */
|
|
366
|
-
export function planHostInstall(
|
|
367
|
-
host: HostId,
|
|
368
|
-
options: HostInstallOptions = {},
|
|
369
|
-
): HostInstallCommand[] {
|
|
370
|
-
const env = options.env ?? process.env;
|
|
371
|
-
const home = options.home ?? env.HOME ?? os.homedir();
|
|
372
|
-
const cwd = options.cwd ?? process.cwd();
|
|
373
|
-
const version = options.packageVersion ?? "latest";
|
|
374
|
-
if (!validPackageVersion(version))
|
|
375
|
-
throw new Error(`unsupported package version selector: ${version}`);
|
|
376
|
-
if (options.alreadyInstalled && options.mode !== "upgrade") return [];
|
|
377
|
-
const upgrading = options.mode === "upgrade";
|
|
378
|
-
if (host === "codex") {
|
|
379
|
-
if (!upgrading && isCodexWorkitInstalled(home, env)) return [];
|
|
380
|
-
const base = hostCommand("codex", [], { home, cwd, env });
|
|
381
|
-
return [
|
|
382
|
-
nodePrerequisite({ home, cwd, env }),
|
|
383
|
-
{
|
|
384
|
-
...base,
|
|
385
|
-
args: [...base.args, "plugin", "marketplace", "list"],
|
|
386
|
-
purpose: "Check whether the Workit marketplace is already registered",
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
...base,
|
|
390
|
-
args: [
|
|
391
|
-
...base.args,
|
|
392
|
-
"plugin",
|
|
393
|
-
"marketplace",
|
|
394
|
-
upgrading ? "upgrade" : "add",
|
|
395
|
-
...(upgrading ? [CODEX_MARKETPLACE_NAME] : [MARKETPLACE]),
|
|
396
|
-
],
|
|
397
|
-
purpose: upgrading
|
|
398
|
-
? "Refresh the Workit Codex marketplace"
|
|
399
|
-
: "Register the Workit Codex marketplace",
|
|
400
|
-
...(upgrading ? {} : { skipWhenOutputIncludes: CODEX_MARKETPLACE_NAME }),
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
...base,
|
|
404
|
-
args: [...base.args, "plugin", "add", `workit@${CODEX_MARKETPLACE_NAME}`],
|
|
405
|
-
purpose: upgrading ? "Refresh the Workit Codex plugin" : "Install the Workit Codex plugin",
|
|
406
|
-
},
|
|
407
|
-
];
|
|
408
|
-
}
|
|
409
|
-
if (host === "claude-code") {
|
|
410
|
-
if (!upgrading && isClaudeWorkitInstalled(home, env, null)) return [];
|
|
411
|
-
const base = hostCommand("claude", [], { home, cwd, env });
|
|
412
|
-
return [
|
|
413
|
-
nodePrerequisite({ home, cwd, env }),
|
|
414
|
-
{
|
|
415
|
-
...base,
|
|
416
|
-
args: [...base.args, "plugin", "marketplace", "list", "--json"],
|
|
417
|
-
purpose: "Check whether the Workit Claude Code marketplace is already registered",
|
|
418
|
-
},
|
|
419
|
-
upgrading
|
|
420
|
-
? {
|
|
421
|
-
...base,
|
|
422
|
-
args: [...base.args, "plugin", "marketplace", "update", CLAUDE_MARKETPLACE_NAME],
|
|
423
|
-
purpose: "Refresh the Workit Claude Code marketplace",
|
|
424
|
-
}
|
|
425
|
-
: {
|
|
426
|
-
...base,
|
|
427
|
-
args: [...base.args, "plugin", "marketplace", "add", CLAUDE_MARKETPLACE_REPO],
|
|
428
|
-
purpose: "Register the Workit Claude Code marketplace",
|
|
429
|
-
skipWhenOutputIncludes: `"name": "${CLAUDE_MARKETPLACE_NAME}"`,
|
|
430
|
-
},
|
|
431
|
-
{
|
|
432
|
-
...base,
|
|
433
|
-
args: upgrading
|
|
434
|
-
? [...base.args, "plugin", "update", CLAUDE_PLUGIN_ID]
|
|
435
|
-
: [...base.args, "plugin", "install", CLAUDE_PLUGIN_ID, "--scope", "user"],
|
|
436
|
-
purpose: upgrading
|
|
437
|
-
? "Update the Workit Claude Code plugin"
|
|
438
|
-
: "Install the Workit Claude Code plugin",
|
|
439
|
-
},
|
|
440
|
-
];
|
|
441
|
-
}
|
|
442
|
-
if (host === "pi") {
|
|
443
|
-
if (!upgrading && isPiWorkitInstalled(home, env)) return [];
|
|
444
|
-
const base = hostCommand("pi", [], { home, cwd, env });
|
|
445
|
-
return [
|
|
446
|
-
nodePrerequisite({ home, cwd, env }),
|
|
447
|
-
{
|
|
448
|
-
...base,
|
|
449
|
-
args: upgrading
|
|
450
|
-
? [...base.args, "update", "--extension", "npm:@brainervirus/workit-pi"]
|
|
451
|
-
: [...base.args, "install", `npm:@brainervirus/workit-pi@${version}`],
|
|
452
|
-
purpose: upgrading
|
|
453
|
-
? "Update the Workit Pi package"
|
|
454
|
-
: "Install the Workit Pi package in personal Pi settings",
|
|
455
|
-
},
|
|
456
|
-
];
|
|
457
|
-
}
|
|
458
|
-
if (host === "cursor") {
|
|
459
|
-
const node = findHostExecutable("node", { home, env });
|
|
460
|
-
if (!node)
|
|
461
|
-
throw new Error("Workit host packages require Node.js 24+; install Node.js and retry");
|
|
462
|
-
const npm = findHostExecutable("npm", { home, env });
|
|
463
|
-
const npmCli = npm ? findNpmCli(npm) : null;
|
|
464
|
-
if (!npmCli)
|
|
465
|
-
throw new Error("npm CLI could not be resolved; install Node.js 24+ and npm, then retry");
|
|
466
|
-
const stage = path.join(home, ".cursor", "plugins", "local", ".workit-package-bootstrap");
|
|
467
|
-
const target = options.targetDir ?? path.join(home, ".cursor", "plugins", "local", "workit");
|
|
468
|
-
return [
|
|
469
|
-
nodePrerequisite({ home, cwd, env }),
|
|
470
|
-
{
|
|
471
|
-
command: node,
|
|
472
|
-
args: [
|
|
473
|
-
npmCli,
|
|
474
|
-
"install",
|
|
475
|
-
"--prefix",
|
|
476
|
-
stage,
|
|
477
|
-
"--no-save",
|
|
478
|
-
"--package-lock=false",
|
|
479
|
-
`@brainervirus/workit-cursor@${version}`,
|
|
480
|
-
],
|
|
481
|
-
cwd,
|
|
482
|
-
purpose: upgrading
|
|
483
|
-
? "Download the current Workit Cursor adapter for the managed local plugin directory"
|
|
484
|
-
: "Download the Workit Cursor adapter package for the managed local plugin directory",
|
|
485
|
-
packageRoot: path.join(stage, "node_modules", "@brainervirus", "workit-cursor"),
|
|
486
|
-
packageTarget: target,
|
|
487
|
-
},
|
|
488
|
-
];
|
|
489
|
-
}
|
|
490
|
-
if (host === "opencode") {
|
|
491
|
-
if (upgrading)
|
|
492
|
-
throw new Error(
|
|
493
|
-
"OpenCode CLI cannot target the Workit server plugin for a scoped update; upgrade is unsupported and the current registration was preserved",
|
|
494
|
-
);
|
|
495
|
-
// The normal setup path merges this package pin into OpenCode's config;
|
|
496
|
-
// OpenCode's native resolver installs and refreshes npm plugin packages.
|
|
497
|
-
return [];
|
|
498
|
-
}
|
|
499
|
-
return [];
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/** Execute a reviewed plan serially and stop immediately on the first failure. */
|
|
503
|
-
export function runHostInstall(
|
|
504
|
-
commands: HostInstallCommand[],
|
|
505
|
-
runner?: HostCommandRunner,
|
|
506
|
-
context: HostCommandContext = {},
|
|
507
|
-
): {
|
|
508
|
-
ok: boolean;
|
|
509
|
-
results: Array<{ command: HostInstallCommand; result: HostCommandResult }>;
|
|
510
|
-
packageRoot?: string;
|
|
511
|
-
} {
|
|
512
|
-
const results: Array<{ command: HostInstallCommand; result: HostCommandResult }> = [];
|
|
513
|
-
let packageRoot: string | undefined;
|
|
514
|
-
let outputHistory = "";
|
|
515
|
-
for (const command of commands) {
|
|
516
|
-
if (command.skipWhenOutputIncludes && outputHistory.includes(command.skipWhenOutputIncludes)) {
|
|
517
|
-
results.push({
|
|
518
|
-
command,
|
|
519
|
-
result: { exitCode: 0, stdout: "", stderr: "already registered; skipped" },
|
|
520
|
-
});
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
const result = runner ? runner(command) : runHostCommand(command, context);
|
|
524
|
-
results.push({ command, result });
|
|
525
|
-
if (result.exitCode !== 0) return { ok: false, results };
|
|
526
|
-
outputHistory += `\n${result.stdout}\n${result.stderr}`;
|
|
527
|
-
if (command.packageRoot && command.packageTarget) {
|
|
528
|
-
try {
|
|
529
|
-
if (!existsSync(command.packageRoot)) {
|
|
530
|
-
return {
|
|
531
|
-
ok: false,
|
|
532
|
-
results: [
|
|
533
|
-
{
|
|
534
|
-
command,
|
|
535
|
-
result: {
|
|
536
|
-
...result,
|
|
537
|
-
exitCode: 1,
|
|
538
|
-
stderr: "npm completed without producing the expected Cursor package",
|
|
539
|
-
},
|
|
540
|
-
},
|
|
541
|
-
],
|
|
542
|
-
};
|
|
543
|
-
} else {
|
|
544
|
-
packageRoot = command.packageRoot;
|
|
545
|
-
}
|
|
546
|
-
} catch (error) {
|
|
547
|
-
return {
|
|
548
|
-
ok: false,
|
|
549
|
-
results: [
|
|
550
|
-
{
|
|
551
|
-
command,
|
|
552
|
-
result: {
|
|
553
|
-
...result,
|
|
554
|
-
exitCode: 1,
|
|
555
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
556
|
-
},
|
|
557
|
-
},
|
|
558
|
-
],
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
return { ok: true, results, packageRoot };
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/** Synchronous, bounded command runner for the synchronous setup Apply API. */
|
|
567
|
-
export function runHostCommand(
|
|
568
|
-
step: HostInstallCommand,
|
|
569
|
-
context: HostCommandContext = {},
|
|
570
|
-
): HostCommandResult {
|
|
571
|
-
if (process.platform === "win32" && /\.(?:cmd|bat)$/i.test(step.command)) {
|
|
572
|
-
return {
|
|
573
|
-
exitCode: 1,
|
|
574
|
-
stdout: "",
|
|
575
|
-
stderr: `Cannot safely execute batch shim ${step.command}; resolve its Node.js CLI entrypoint and retry`,
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
const result = spawnSync(step.command, step.args, {
|
|
579
|
-
cwd: step.cwd,
|
|
580
|
-
env: (() => {
|
|
581
|
-
const home = context.home ?? context.env?.HOME ?? os.homedir();
|
|
582
|
-
const env: NodeJS.ProcessEnv = { ...process.env, ...context.env, HOME: home };
|
|
583
|
-
env.CODEX_HOME = context.env?.CODEX_HOME ?? path.join(home, ".codex");
|
|
584
|
-
env.PI_CODING_AGENT_DIR = context.env?.PI_CODING_AGENT_DIR ?? path.join(home, ".pi", "agent");
|
|
585
|
-
env.CLAUDE_CONFIG_DIR = context.env?.CLAUDE_CONFIG_DIR ?? path.join(home, ".claude");
|
|
586
|
-
env.XDG_CONFIG_HOME = context.env?.XDG_CONFIG_HOME ?? path.join(home, ".config");
|
|
587
|
-
return env;
|
|
588
|
-
})(),
|
|
589
|
-
encoding: "utf8",
|
|
590
|
-
timeout: 60_000,
|
|
591
|
-
windowsHide: true,
|
|
592
|
-
shell: false,
|
|
593
|
-
});
|
|
594
|
-
return {
|
|
595
|
-
exitCode: result.status ?? (result.error ? 1 : 1),
|
|
596
|
-
stdout: result.stdout ?? "",
|
|
597
|
-
stderr: result.stderr ?? result.error?.message ?? "",
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const findNpmCli = (npm: string): string | null => {
|
|
602
|
-
// `npm` is usually a symlink to npm-cli.js (Node's bin/npm, a package's
|
|
603
|
-
// node_modules/.bin/npm); resolve it before guessing install layouts.
|
|
604
|
-
try {
|
|
605
|
-
const resolved = realpathSync(npm);
|
|
606
|
-
if (path.basename(resolved) === "npm-cli.js") return resolved;
|
|
607
|
-
} catch {
|
|
608
|
-
// Fall through to the layout candidates.
|
|
609
|
-
}
|
|
610
|
-
const dir = path.dirname(npm);
|
|
611
|
-
const candidates = [
|
|
612
|
-
path.join(dir, "node_modules", "npm", "bin", "npm-cli.js"),
|
|
613
|
-
path.join(dir, "..", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
614
|
-
path.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
615
|
-
];
|
|
616
|
-
return candidates.find((candidate) => existsSync(candidate)) ?? null;
|
|
617
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// SHA-256 of generated Cursor assets in the published 1.2.1 package. These
|
|
2
|
-
// hashes prove ownership for a narrow, known upgrade source; filenames alone
|
|
3
|
-
// never authorize replacing an installed file.
|
|
4
|
-
export const LEGACY_CURSOR_ASSET_DIGESTS: Readonly<Record<string, string>> = {
|
|
5
|
-
"commands/wk-babysit.md": "002b00bba340ace6bf64a605f7e26c56b6224010529dbf5fd8966102f0065553",
|
|
6
|
-
"commands/wk-blast-radius.md": "474030101d592fe61931baffd50158bf58987eb65902a594df26e53bd62e1d57",
|
|
7
|
-
"commands/wk-challenge.md": "65f3e3bf5ce2d2ec0b77d2fe061894a53c3de98ee5d00cba8cf6a8259b4c6f46",
|
|
8
|
-
"commands/wk-debug.md": "8a67a77da27df3e6416ec685f360c79537feac10c324baf4a2d6f3a0329d7623",
|
|
9
|
-
"commands/wk-deslop.md": "8d31b708225fcc8b6de12a8ce8b3e42c74a20821f1b86d347d969e0906bc1397",
|
|
10
|
-
"commands/wk-diagram.md": "d83a8b4b08f2e9ae30e7ee372b89ed400193ca6cc5fc918087b47283efe2b4a6",
|
|
11
|
-
"commands/wk-green-run.md": "0de645228cedb650ef24ade84d1800891a83ce39a568bc5f04f036542e1c0a4a",
|
|
12
|
-
"commands/wk-handoff.md": "525e3ea4717593826d6a2daf72bfdd35602b33dd77c6d03166e865ad29c65592",
|
|
13
|
-
"commands/wk-implement.md": "9e74766ba1829dbf04cc95e0c4d96fbeaafef57708ce6ff0d027a9ffad6de4c5",
|
|
14
|
-
"commands/wk-mockup.md": "9c441ada81c2b90510dd57804d25fc6b9ccc4f28870d98d1a3f2327e2b6564a4",
|
|
15
|
-
"commands/wk-plan.md": "8b3014450cdb1099f13628e983ce3b3d1566d8594c4bde8b31d74e8587b918ea",
|
|
16
|
-
"commands/wk-review.md": "4260e80e556a1b3042a3990cf1a53a4cb8ea74078667e5c24269f10c58f25f0e",
|
|
17
|
-
"commands/wk-steer.md": "251795b24df1395a4786d1565a2ab32a1f1d5321d33fbc3175bbf917a9c4ea1a",
|
|
18
|
-
"commands/wk-tdd.md": "b3e00a37d9ea1c0c133fc86aaf3c56b8d38bb443aded2ad05a06a9cc762c86f5",
|
|
19
|
-
"rules/workit-contract.mdc": "a7425e888f19c8984f784cc6395800f33ac094525863a894e5fba516ff7ff450",
|
|
20
|
-
"skills/workit-babysit/SKILL.md":
|
|
21
|
-
"a8958f7042be13048756a0c6ad7415feaf356132431b1d8b6e0f6617e34281a3",
|
|
22
|
-
"skills/workit-behavioral-tdd/SKILL.md":
|
|
23
|
-
"78207d684f9f50fa9c6d3ab68e425f71aa12975414c874975ce2daf427b9cf9d",
|
|
24
|
-
"skills/workit-blast-radius/SKILL.md":
|
|
25
|
-
"e829d3106a2b65c3f8aeceb53a1dbd3fadacbf1b1e1380a432e5e5f997e8a5a5",
|
|
26
|
-
"skills/workit-challenge/SKILL.md":
|
|
27
|
-
"60273fc7bdf7920e88e19c9d448f8d74a3e881dcce5f726f6aa947e4fc3881cf",
|
|
28
|
-
"skills/workit-debug/SKILL.md":
|
|
29
|
-
"e2776294e5ed3dc3c5c9d6c0d6321e53204cc4f0a7206424d32dca862d47d748",
|
|
30
|
-
"skills/workit-deslop/SKILL.md":
|
|
31
|
-
"e917dabad0fade7ee29bb4de36fd0bfeb9235d063951c3adad2f501c2d880f15",
|
|
32
|
-
"skills/workit-diagram/SKILL.md":
|
|
33
|
-
"d4a46c95d38b77e05f2246a3ab20b6ce8a5c5a2125df7c4ed5d4b79398307f9e",
|
|
34
|
-
"skills/workit-green-run/SKILL.md":
|
|
35
|
-
"f1bf1af81acd33c72f23a3f803a40c737bdd9e1a1889fb2c2b41ab9336679c4f",
|
|
36
|
-
"skills/workit-handoff/SKILL.md":
|
|
37
|
-
"855fc35166bb8886a1adca17f638ddb85508f36671bb034e00e1e0e2b403614a",
|
|
38
|
-
"skills/workit-implement/SKILL.md":
|
|
39
|
-
"7ba5adb7d6cf2acc34d25b10164e2af6b9f99d3bc5ef20ff5409b75e03e98bd4",
|
|
40
|
-
"skills/workit-mockup/SKILL.md":
|
|
41
|
-
"56fc4046599150874b498b269873d1522e671e72b17257fe312a4786205e15c3",
|
|
42
|
-
"skills/workit-plan/SKILL.md": "6283fdcc6c11c0267e679ce4eefbdd67e7fb5b819521dd86d9908a93bf78482e",
|
|
43
|
-
"skills/workit-review/SKILL.md":
|
|
44
|
-
"0f7c662abedd167589d3974013ff805df650dfc881d9709dfcfa4124c0dbe58d",
|
|
45
|
-
"skills/workit-steer/SKILL.md":
|
|
46
|
-
"5e7142010675d55c21ef84fc2b077f06163c27f95ff4d9ae22a7fda13c7ca543",
|
|
47
|
-
};
|