@workit-dev/claude-code 0.0.1 → 1.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/.claude-plugin/plugin.json +19 -0
- package/README.md +35 -31
- package/agents/implementer.md +56 -0
- package/agents/reviewer.md +33 -0
- package/agents/verifier.md +40 -0
- package/assets/templates/headers.md +3 -0
- package/assets/templates/hygiene/.editorconfig +8 -0
- package/assets/templates/hygiene/.gitattributes +3 -0
- package/assets/templates/hygiene/CHANGELOG.md +14 -0
- package/assets/templates/hygiene/CONTRIBUTING.md +3 -0
- package/assets/templates/hygiene/LICENSE +21 -0
- package/assets/templates/hygiene/README.md +3 -0
- package/assets/templates/issue-update.md +5 -0
- package/assets/templates/plan-template.md +26 -0
- package/assets/templates/spec-template.md +64 -0
- package/assets/templates/workit-contract.md +13 -0
- package/bin/node-resolve.mjs +249 -0
- package/bin/workit +82 -0
- package/bin/workit-hook.mjs +552 -0
- package/bin/workit-paths.mjs +242 -0
- package/dist/workit-hook.js +248 -0
- package/dist/workit.js +122606 -0
- package/hooks/hooks.json +147 -0
- package/package.json +45 -6
- package/skills/architecture/SKILL.md +72 -0
- package/skills/architecture/agents/openai.yaml +3 -0
- package/skills/architecture/references/instruction-files.md +52 -0
- package/skills/architecture/references/vocabulary.md +53 -0
- package/skills/bdd/SKILL.md +53 -0
- package/skills/continue/SKILL.md +54 -0
- package/skills/debug/SKILL.md +45 -0
- package/skills/deslop/SKILL.md +58 -0
- package/skills/fanout/SKILL.md +71 -0
- package/skills/fanout/references/brief.md +141 -0
- package/skills/fanout/references/orchestration.md +169 -0
- package/skills/implement/SKILL.md +71 -0
- package/skills/retro/SKILL.md +72 -0
- package/skills/retro/agents/openai.yaml +3 -0
- package/skills/retro/references/sources.md +35 -0
- package/skills/retro/references/steering.md +39 -0
- package/skills/review/SKILL.md +66 -0
- package/skills/review/references/impact.md +24 -0
- package/skills/shape/SKILL.md +71 -0
- package/skills/shape/references/diagrams.md +17 -0
- package/skills/shape/references/knowledge.md +61 -0
- package/skills/shape/references/mockups.md +15 -0
- package/skills/shape/references/slicing.md +44 -0
- package/skills/ship/SKILL.md +71 -0
- package/skills/ship/references/changelog.md +44 -0
- package/skills/ship/references/pr-body.md +32 -0
- package/skills/ship/references/release.md +39 -0
- package/skills/test-audit/SKILL.md +54 -0
- package/skills/verify-app/SKILL.md +68 -0
- package/skills/verify-app/references/template.md +51 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Finds a Node that can run a Workit hook when the PATH node is below the
|
|
2
|
+
// package's engines floor (a repo pinned to Node 16/18/22 by a version
|
|
3
|
+
// manager). Plain JavaScript with node: builtins only and no syntax newer than
|
|
4
|
+
// Node 16 supports: the host launchers import it (or carry a byte-equal copy,
|
|
5
|
+
// a test holds them equal) and must start on the very Node they are replacing.
|
|
6
|
+
// Side-effect free: importing it runs nothing; no process is spawned here.
|
|
7
|
+
//
|
|
8
|
+
// Order: (1) $WORKIT_NODE; (2) the node `workit setup`/`upgrade` recorded in
|
|
9
|
+
// <state dir>/node.json (workit-paths.mjs); (3) a scan of version-manager installs (fnm, nvm,
|
|
10
|
+
// volta, mise, asdf and the Windows layouts), highest version >= the floor.
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { stateDir } from "./workit-paths.mjs";
|
|
16
|
+
|
|
17
|
+
/** The Node major the hooks need; a test holds it equal to every package's engines.node. */
|
|
18
|
+
export const HOOK_NODE_FLOOR = [24, 0, 0];
|
|
19
|
+
|
|
20
|
+
/** Set on the re-executed child so a launcher never re-executes twice. */
|
|
21
|
+
export const HOOK_NODE_REEXEC_ENV = "WORKIT_HOOK_REEXEC";
|
|
22
|
+
|
|
23
|
+
const MAX_ENTRIES_PER_ROOT = 200;
|
|
24
|
+
|
|
25
|
+
/** @param {unknown} text @returns {number[] | null} */
|
|
26
|
+
export const parseNodeVersion = (text) => {
|
|
27
|
+
const found = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(String(text).trim());
|
|
28
|
+
return found ? [Number(found[1]), Number(found[2] || 0), Number(found[3] || 0)] : null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** @param {number[]} a @param {number[]} b negative when a < b */
|
|
32
|
+
export const compareVersions = (a, b) => {
|
|
33
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i];
|
|
34
|
+
return 0;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The `>=X[.Y[.Z]]` minimum of an engines.node range, or null. @param {unknown} range */
|
|
38
|
+
export const floorOfRange = (range) => {
|
|
39
|
+
const min = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(String(range === undefined ? "" : range));
|
|
40
|
+
return min ? [Number(min[1]), Number(min[2] || 0), Number(min[3] || 0)] : null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Workit's state dir (workit-paths.mjs, the one resolver). @param {NodeJS.ProcessEnv} env */
|
|
44
|
+
export const hookStateDir = (env) => stateDir(env);
|
|
45
|
+
|
|
46
|
+
/** @param {string} file */
|
|
47
|
+
const isFile = (file) => {
|
|
48
|
+
try {
|
|
49
|
+
return statSync(file).isFile();
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** @param {string} file */
|
|
56
|
+
const readJson = (file) => {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The version-manager directories that hold one node per child directory:
|
|
66
|
+
* `{ dir, bin }` where the node is `<dir>/<child>/<bin>`.
|
|
67
|
+
* @param {NodeJS.ProcessEnv} env @param {string} platform
|
|
68
|
+
*/
|
|
69
|
+
const scanRoots = (env, platform) => {
|
|
70
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
71
|
+
const win = platform === "win32";
|
|
72
|
+
const exe = win ? "node.exe" : "node";
|
|
73
|
+
const bin = win ? exe : path.join("bin", exe);
|
|
74
|
+
/** @type {Array<{ dir: string, bin: string }>} */
|
|
75
|
+
const roots = [];
|
|
76
|
+
const add = (dir, rel) => {
|
|
77
|
+
if (dir) roots.push({ dir, bin: rel });
|
|
78
|
+
};
|
|
79
|
+
// fnm: <dir>/node-versions/<v>/installation/bin/node
|
|
80
|
+
const fnmDirs = [
|
|
81
|
+
env.FNM_DIR,
|
|
82
|
+
win ? path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "fnm") : "",
|
|
83
|
+
win ? path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "fnm") : "",
|
|
84
|
+
env.XDG_DATA_HOME ? path.join(env.XDG_DATA_HOME, "fnm") : "",
|
|
85
|
+
path.join(home, ".local", "share", "fnm"),
|
|
86
|
+
path.join(home, "Library", "Application Support", "fnm"),
|
|
87
|
+
path.join(home, ".fnm"),
|
|
88
|
+
];
|
|
89
|
+
for (const dir of fnmDirs)
|
|
90
|
+
if (dir) add(path.join(dir, "node-versions"), path.join("installation", bin));
|
|
91
|
+
// nvm: <dir>/versions/node/<v>/bin/node; nvm-windows: <NVM_HOME>/<v>/node.exe
|
|
92
|
+
for (const dir of [env.NVM_DIR, path.join(home, ".nvm")])
|
|
93
|
+
if (dir) add(path.join(dir, "versions", "node"), bin);
|
|
94
|
+
if (env.NVM_HOME) add(env.NVM_HOME, exe);
|
|
95
|
+
// volta: <dir>/tools/image/node/<v>/bin/node
|
|
96
|
+
const volta = env.VOLTA_HOME || (win ? "" : path.join(home, ".volta"));
|
|
97
|
+
if (volta) add(path.join(volta, "tools", "image", "node"), bin);
|
|
98
|
+
if (win)
|
|
99
|
+
add(
|
|
100
|
+
path.join(
|
|
101
|
+
env.LOCALAPPDATA || path.join(home, "AppData", "Local"),
|
|
102
|
+
"Volta",
|
|
103
|
+
"tools",
|
|
104
|
+
"image",
|
|
105
|
+
"node",
|
|
106
|
+
),
|
|
107
|
+
bin,
|
|
108
|
+
);
|
|
109
|
+
// mise and asdf: <dir>/installs/node[js]/<v>/bin/node
|
|
110
|
+
const mise = env.MISE_DATA_DIR || path.join(home, ".local", "share", "mise");
|
|
111
|
+
add(path.join(mise, "installs", "node"), bin);
|
|
112
|
+
const asdf = env.ASDF_DATA_DIR || path.join(home, ".asdf");
|
|
113
|
+
add(path.join(asdf, "installs", "nodejs"), bin);
|
|
114
|
+
return roots;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Every installed node a version manager lists, with its version: directory
|
|
119
|
+
* listings only, at most MAX_ENTRIES_PER_ROOT per root.
|
|
120
|
+
* @param {NodeJS.ProcessEnv} env @param {string} [platform]
|
|
121
|
+
* @returns {Array<{ path: string, version: number[], source: string }>}
|
|
122
|
+
*/
|
|
123
|
+
export const scanInstalledNodes = (env, platform = process.platform) => {
|
|
124
|
+
/** @type {Array<{ path: string, version: number[], source: string }>} */
|
|
125
|
+
const found = [];
|
|
126
|
+
for (const root of scanRoots(env, platform)) {
|
|
127
|
+
let names;
|
|
128
|
+
try {
|
|
129
|
+
names = readdirSync(root.dir).slice(0, MAX_ENTRIES_PER_ROOT);
|
|
130
|
+
} catch {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
const version = parseNodeVersion(name);
|
|
135
|
+
if (!version || !/^v?\d/.test(name)) continue;
|
|
136
|
+
// volta's layout nests nothing; fnm's, nvm's, mise's and asdf's differ only in `bin`.
|
|
137
|
+
const candidate = path.join(root.dir, name, root.bin);
|
|
138
|
+
if (isFile(candidate)) found.push({ path: candidate, version, source: root.dir });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return found;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The node the hooks should run on, or null. `floor` is [major, minor, patch].
|
|
146
|
+
* @param {{ env: NodeJS.ProcessEnv, floor: number[], platform?: string }} options
|
|
147
|
+
* @returns {{ path: string, version: string | null, source: "env" | "recorded" | "scan" } | null}
|
|
148
|
+
*/
|
|
149
|
+
export const resolveHookNode = ({ env, floor, platform = process.platform }) => {
|
|
150
|
+
if (env.WORKIT_NODE && isFile(env.WORKIT_NODE))
|
|
151
|
+
return { path: env.WORKIT_NODE, version: null, source: "env" };
|
|
152
|
+
const recorded = readJson(path.join(hookStateDir(env), "node.json"));
|
|
153
|
+
if (recorded && typeof recorded.path === "string" && isFile(recorded.path)) {
|
|
154
|
+
const version = parseNodeVersion(recorded.version);
|
|
155
|
+
if (version && compareVersions(version, floor) >= 0)
|
|
156
|
+
return { path: recorded.path, version: String(recorded.version), source: "recorded" };
|
|
157
|
+
}
|
|
158
|
+
let best = null;
|
|
159
|
+
for (const entry of scanInstalledNodes(env, platform))
|
|
160
|
+
if (
|
|
161
|
+
compareVersions(entry.version, floor) >= 0 &&
|
|
162
|
+
(!best || compareVersions(entry.version, best.version) > 0)
|
|
163
|
+
)
|
|
164
|
+
best = entry;
|
|
165
|
+
return best ? { path: best.path, version: best.version.join("."), source: "scan" } : null;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The fail-open line for a node below the floor: null when `found` is at or
|
|
170
|
+
* above `floor`, or unparseable (an unreadable version never blocks).
|
|
171
|
+
* @param {string} found @param {number[] | null} floor @param {string} range
|
|
172
|
+
*/
|
|
173
|
+
export const floorMessage = (found, floor, range) => {
|
|
174
|
+
if (!floor) return null;
|
|
175
|
+
const have = parseNodeVersion(found);
|
|
176
|
+
if (!have || compareVersions(have, floor) >= 0) return null;
|
|
177
|
+
return `needs Node ${range}, found ${found}`;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** What the agent is told once, at SessionStart, when no Node >= the floor resolves. */
|
|
181
|
+
export const nodeFloorNotice = (found, range) =>
|
|
182
|
+
`Workit hooks are off: the Node on PATH is ${String(found).replace(/^v/, "")}, below the required ${range}, and no Node 24+ was found. ` +
|
|
183
|
+
"Install Node 24 (fnm install 24, nvm install 24) or run `workit setup` from a Node 24 shell so Workit can record it; " +
|
|
184
|
+
"`workit doctor` (check hook_node) shows what the hooks run on.";
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Where setup and upgrade record the Node they run on. A no-op for a non-node
|
|
188
|
+
* binary (bun) or a Node below the floor.
|
|
189
|
+
* @param {{ env: NodeJS.ProcessEnv, execPath: string, version: string, floor: number[] }} options
|
|
190
|
+
* @returns {string | null} the file written
|
|
191
|
+
*/
|
|
192
|
+
export const nodeRecord = ({ env, execPath, version, floor }) => {
|
|
193
|
+
const parsed = parseNodeVersion(version);
|
|
194
|
+
if (!parsed || compareVersions(parsed, floor) < 0) return null;
|
|
195
|
+
if (!/^node(?:\.exe)?$/i.test(path.basename(execPath))) return null;
|
|
196
|
+
return path.join(hookStateDir(env), "node.json");
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run `script args…` on `nodePath` with this process's stdin, stdout and
|
|
201
|
+
* stderr passed through. The child carries the re-exec guard and a real
|
|
202
|
+
* version (WORKIT_HOOK_NODE_VERSION, a test seam, is dropped).
|
|
203
|
+
* @returns {number | null} the child's exit code, or null when it could not start
|
|
204
|
+
*/
|
|
205
|
+
export const reexecOnNode = (nodePath, script, args, env) => {
|
|
206
|
+
const child = Object.assign({}, env, { [HOOK_NODE_REEXEC_ENV]: "1" });
|
|
207
|
+
delete child.WORKIT_HOOK_NODE_VERSION;
|
|
208
|
+
const run = spawnSync(nodePath, [script].concat(args), {
|
|
209
|
+
stdio: "inherit",
|
|
210
|
+
env: child,
|
|
211
|
+
windowsHide: true,
|
|
212
|
+
});
|
|
213
|
+
if (run.error) return null;
|
|
214
|
+
return run.status === null ? 1 : run.status;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* What a launcher does on a Node below its floor: re-run itself on a resolved
|
|
219
|
+
* Node (the child's exit code is returned), else fail open: one stderr line,
|
|
220
|
+
* and `{}` on stdout, except that a start event answers with `notice(text)` so
|
|
221
|
+
* the agent is told the hooks are off. Resolves to the exit code.
|
|
222
|
+
* @param {{ host: string, range: string, floor: number[], found: string, script: string,
|
|
223
|
+
* argv: string[], env: NodeJS.ProcessEnv, isStart: (event: unknown) => boolean,
|
|
224
|
+
* notice: (text: string) => unknown }} options
|
|
225
|
+
*/
|
|
226
|
+
export const handleBelowFloor = async (options) => {
|
|
227
|
+
if (!options.env[HOOK_NODE_REEXEC_ENV]) {
|
|
228
|
+
const hit = resolveHookNode({ env: options.env, floor: options.floor });
|
|
229
|
+
const code = hit ? reexecOnNode(hit.path, options.script, options.argv, options.env) : null;
|
|
230
|
+
if (code !== null) return code;
|
|
231
|
+
}
|
|
232
|
+
process.stderr.write(
|
|
233
|
+
`[workit] ${options.host} hook unavailable: needs Node ${options.range}, found ${options.found}\n`,
|
|
234
|
+
);
|
|
235
|
+
let early = "";
|
|
236
|
+
for await (const chunk of process.stdin) early += String(chunk);
|
|
237
|
+
let event;
|
|
238
|
+
try {
|
|
239
|
+
event = JSON.parse(early).hook_event_name;
|
|
240
|
+
} catch {
|
|
241
|
+
event = undefined;
|
|
242
|
+
}
|
|
243
|
+
process.stdout.write(
|
|
244
|
+
options.isStart(event)
|
|
245
|
+
? `${JSON.stringify(options.notice(nodeFloorNotice(options.found, options.range)))}\n`
|
|
246
|
+
: "{}\n",
|
|
247
|
+
);
|
|
248
|
+
return 0;
|
|
249
|
+
};
|
package/bin/workit
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# `workit` on the Claude Code Bash tool's PATH (Claude adds a plugin's bin/).
|
|
3
|
+
# Local pin: run the CLI from the monorepo sources with bun. Installed
|
|
4
|
+
# package: run the bundled dist/workit.js with node.
|
|
5
|
+
# WORKIT_CLAUDE_RUNTIME=source|dist forces one; WORKIT_SHIM_TRACE=1 prints
|
|
6
|
+
# the resolved entry on stderr.
|
|
7
|
+
# When the PATH node is below the floor (24), the dist runs on a Node that is:
|
|
8
|
+
# $WORKIT_NODE, the node `workit setup` recorded in <state dir>/node.json, then
|
|
9
|
+
# the highest fnm/nvm/volta/mise/asdf install (same order as hooks, see
|
|
10
|
+
# bin/node-resolve.mjs; a test holds the floor equal).
|
|
11
|
+
set -e
|
|
12
|
+
# The state dir as core/workit-paths.mjs resolves it (STATE_DIR_SH; a test holds it verbatim).
|
|
13
|
+
workit_state_dir() {
|
|
14
|
+
if [ -n "${WORKIT_STATE_DIR:-}" ]; then printf '%s\n' "$WORKIT_STATE_DIR"; return; fi
|
|
15
|
+
case "${XDG_STATE_HOME:-}" in /*) printf '%s\n' "$XDG_STATE_HOME/workit"; return ;; esac
|
|
16
|
+
case "$(uname -s 2>/dev/null)" in
|
|
17
|
+
MINGW*|MSYS*|CYGWIN*) printf '%s\n' "${LOCALAPPDATA:-$HOME/AppData/Local}/workit" ;;
|
|
18
|
+
*) printf '%s\n' "$HOME/.local/state/workit" ;;
|
|
19
|
+
esac
|
|
20
|
+
}
|
|
21
|
+
here=$(dirname "$0")
|
|
22
|
+
root=$(cd "$here/.." && pwd)
|
|
23
|
+
main="$root/../workit-cli/src/main.ts"
|
|
24
|
+
entry=""
|
|
25
|
+
if [ "${WORKIT_CLAUDE_RUNTIME:-}" != "dist" ] && [ -f "$root/../workit-core/src/core.ts" ] && [ -f "$main" ]; then
|
|
26
|
+
entry="$main"
|
|
27
|
+
fi
|
|
28
|
+
if [ "${WORKIT_CLAUDE_RUNTIME:-}" = "source" ] && [ -z "$entry" ]; then
|
|
29
|
+
echo "workit: WORKIT_CLAUDE_RUNTIME=source but no monorepo CLI source next to $root" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
if [ -n "$entry" ]; then
|
|
33
|
+
[ "${WORKIT_SHIM_TRACE:-}" = "1" ] && echo "workit-shim: source $entry" >&2
|
|
34
|
+
exec bun "$entry" "$@"
|
|
35
|
+
fi
|
|
36
|
+
node_floor=24
|
|
37
|
+
node_bin=node
|
|
38
|
+
node_major=$(node -v 2>/dev/null | sed 's/^v//; s/\..*$//')
|
|
39
|
+
case "$node_major" in
|
|
40
|
+
''|*[!0-9]*) node_major=0 ;;
|
|
41
|
+
esac
|
|
42
|
+
if [ "$node_major" -lt "$node_floor" ]; then
|
|
43
|
+
found=""
|
|
44
|
+
if [ -n "${WORKIT_NODE:-}" ] && [ -x "$WORKIT_NODE" ]; then
|
|
45
|
+
found="$WORKIT_NODE"
|
|
46
|
+
fi
|
|
47
|
+
if [ -z "$found" ]; then
|
|
48
|
+
state=$(workit_state_dir)
|
|
49
|
+
recorded=$(sed -n 's/.*"path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$state/node.json" 2>/dev/null | head -n 1)
|
|
50
|
+
rv=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"v\{0,1\}\([0-9]*\).*/\1/p' "$state/node.json" 2>/dev/null | head -n 1)
|
|
51
|
+
if [ -n "$recorded" ] && [ -x "$recorded" ] && [ "${rv:-0}" -ge "$node_floor" ]; then
|
|
52
|
+
found="$recorded"
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
if [ -z "$found" ]; then
|
|
56
|
+
candidates=""
|
|
57
|
+
for spec in \
|
|
58
|
+
"${FNM_DIR:-$HOME/.local/share/fnm}/node-versions|installation/bin/node" \
|
|
59
|
+
"$HOME/Library/Application Support/fnm/node-versions|installation/bin/node" \
|
|
60
|
+
"${NVM_DIR:-$HOME/.nvm}/versions/node|bin/node" \
|
|
61
|
+
"${VOLTA_HOME:-$HOME/.volta}/tools/image/node|bin/node" \
|
|
62
|
+
"${MISE_DATA_DIR:-$HOME/.local/share/mise}/installs/node|bin/node" \
|
|
63
|
+
"${ASDF_DATA_DIR:-$HOME/.asdf}/installs/nodejs|bin/node"; do
|
|
64
|
+
base=${spec%%|*}
|
|
65
|
+
rel=${spec#*|}
|
|
66
|
+
for dir in "$base"/*; do
|
|
67
|
+
name=${dir##*/}
|
|
68
|
+
v=${name#v}
|
|
69
|
+
major=${v%%.*}
|
|
70
|
+
case "$major" in ''|*[!0-9]*) continue ;; esac
|
|
71
|
+
if [ "$major" -ge "$node_floor" ] && [ -x "$dir/$rel" ]; then
|
|
72
|
+
candidates="$candidates
|
|
73
|
+
$v $dir/$rel"
|
|
74
|
+
fi
|
|
75
|
+
done
|
|
76
|
+
done
|
|
77
|
+
found=$(printf '%s\n' "$candidates" | sed '/^$/d' | sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1 | sed 's/^[^ ]* //')
|
|
78
|
+
fi
|
|
79
|
+
if [ -n "$found" ]; then node_bin="$found"; fi
|
|
80
|
+
fi
|
|
81
|
+
[ "${WORKIT_SHIM_TRACE:-}" = "1" ] && echo "workit-shim: dist $root/dist/workit.js (node $node_bin)" >&2
|
|
82
|
+
exec "$node_bin" "$root/dist/workit.js" "$@"
|