jobforge 0.5.2 → 0.6.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 CHANGED
@@ -9,7 +9,9 @@ JobForge package (`jobforge install ...`); the Node package only resolves the
9
9
  runtime, collects choices, and calls the Python engine.
10
10
 
11
11
  ```bash
12
- npx jobforge
12
+ npx jobforge # install or update the runtime, then install into agents
13
+ npx jobforge --update # force a runtime update to this installer version
14
+ npx jobforge --no-update
13
15
  ```
14
16
 
15
17
  Optional browser automation (Chromium for filling applications) is offered
package/bin/jobforge.js CHANGED
@@ -29,14 +29,18 @@ function usage() {
29
29
 
30
30
  Usage:
31
31
  npx jobforge
32
+ npx jobforge --update
33
+ npx jobforge --no-update
32
34
 
33
35
  No arguments are required. The wizard detects your local agents, shows the
34
- exact files it will write, and installs the MCP server and Agent Skill.
36
+ exact files it will write, and installs the MCP server and Agent Skill. It
37
+ also updates a stale uv-managed JobForge runtime to this installer's version.
35
38
 
36
39
  Environment:
37
- JOBFORGE_BIN JobForge command to use (default: jobforge on PATH)
38
- JOBFORGE_PYTHON Python executable to use with \`-m jobforge\`
39
- JOBFORGE_CONFIG Path to config.yml
40
+ JOBFORGE_BIN JobForge command to use (default: jobforge on PATH)
41
+ JOBFORGE_PYTHON Python executable to use with \`-m jobforge\`
42
+ JOBFORGE_CONFIG Path to config.yml
43
+ JOBFORGE_INSTALL_SPEC Override the runtime install source
40
44
  `);
41
45
  }
42
46
 
@@ -48,11 +52,17 @@ function guard(value) {
48
52
  return value;
49
53
  }
50
54
 
51
- function commandExists(name) {
52
- if (!name || name.includes(path.sep)) return existsSync(name);
55
+ function whichPath(name) {
56
+ if (!name) return "";
57
+ if (name.includes(path.sep)) return existsSync(name) ? name : "";
53
58
  const finder = process.platform === "win32" ? "where" : "which";
54
- const result = spawnSync(finder, [name], { stdio: "ignore" });
55
- return result.status === 0;
59
+ const result = spawnSync(finder, [name], { encoding: "utf8" });
60
+ if (result.status !== 0) return "";
61
+ return String(result.stdout || "").split("\n")[0].trim();
62
+ }
63
+
64
+ function commandExists(name) {
65
+ return Boolean(whichPath(name));
56
66
  }
57
67
 
58
68
  function isPythonJobForge(cmd, prefix) {
@@ -83,14 +93,21 @@ function repoRoot(start) {
83
93
  function resolveJobForge() {
84
94
  const envBin = (process.env.JOBFORGE_BIN || "").trim();
85
95
  if (envBin && commandExists(envBin) && isPythonJobForge(envBin, [])) {
86
- return { cmd: envBin, prefix: [], config: process.env.JOBFORGE_CONFIG || "" };
96
+ return {
97
+ cmd: envBin,
98
+ prefix: [],
99
+ config: process.env.JOBFORGE_CONFIG || "",
100
+ source: "env-bin",
101
+ };
87
102
  }
88
103
 
89
- if (commandExists("jobforge") && isPythonJobForge("jobforge", [])) {
104
+ const pathJobforge = whichPath("jobforge");
105
+ if (pathJobforge && isPythonJobForge(pathJobforge, [])) {
90
106
  return {
91
- cmd: "jobforge",
107
+ cmd: pathJobforge,
92
108
  prefix: [],
93
109
  config: process.env.JOBFORGE_CONFIG || "",
110
+ source: "path",
94
111
  };
95
112
  }
96
113
 
@@ -100,6 +117,7 @@ function resolveJobForge() {
100
117
  cmd: localShim,
101
118
  prefix: [],
102
119
  config: process.env.JOBFORGE_CONFIG || "",
120
+ source: "local-shim",
103
121
  };
104
122
  }
105
123
 
@@ -109,6 +127,7 @@ function resolveJobForge() {
109
127
  cmd: envPython,
110
128
  prefix: ["-m", "jobforge"],
111
129
  config: process.env.JOBFORGE_CONFIG || "",
130
+ source: "env-python",
112
131
  };
113
132
  }
114
133
 
@@ -118,6 +137,7 @@ function resolveJobForge() {
118
137
  cmd: python,
119
138
  prefix: ["-m", "jobforge"],
120
139
  config: process.env.JOBFORGE_CONFIG || "",
140
+ source: "system-python",
121
141
  };
122
142
  }
123
143
  }
@@ -130,6 +150,7 @@ function resolveJobForge() {
130
150
  cmd: python,
131
151
  prefix: ["-m", "jobforge"],
132
152
  config: process.env.JOBFORGE_CONFIG || (existsSync(config) ? config : ""),
153
+ source: "repo",
133
154
  };
134
155
  }
135
156
 
@@ -174,9 +195,122 @@ function runJson(target, args, options = {}) {
174
195
  }
175
196
  }
176
197
 
177
- async function ensureRuntime() {
178
- const existing = resolveJobForge();
179
- if (existing) return existing;
198
+ function installSpec() {
199
+ return (
200
+ process.env.JOBFORGE_INSTALL_SPEC ||
201
+ `git+https://github.com/Taf0711/jobforge@v${VERSION}`
202
+ );
203
+ }
204
+
205
+ function versionParts(value) {
206
+ const base = String(value || "").trim().split(/[-+]/)[0];
207
+ return base.split(".").map((part) => Number.parseInt(part, 10));
208
+ }
209
+
210
+ function isOlderVersion(installed, expected) {
211
+ const left = versionParts(installed);
212
+ const right = versionParts(expected);
213
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
214
+ const a = Number.isFinite(left[index]) ? left[index] : 0;
215
+ const b = Number.isFinite(right[index]) ? right[index] : 0;
216
+ if (a < b) return true;
217
+ if (a > b) return false;
218
+ }
219
+ return false;
220
+ }
221
+
222
+ function runtimeVersion(target) {
223
+ const direct = runCommand(target, ["--version"], { timeout: 15_000 });
224
+ const output = `${direct.stdout || ""}${direct.stderr || ""}`.trim();
225
+ const match = output.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
226
+ if (direct.status === 0 && match) return match[1];
227
+ try {
228
+ const fallback = runCommand(target, ["doctor"], { timeout: 60_000 });
229
+ const report = JSON.parse(String(fallback.stdout || "").trim());
230
+ if (report.jobforge) return String(report.jobforge);
231
+ } catch {
232
+ // Older runtimes may not expose a cheap version command.
233
+ }
234
+ return "";
235
+ }
236
+
237
+ function uvToolInstalled() {
238
+ if (!commandExists("uv")) return false;
239
+ const result = runRaw("uv", ["tool", "list"], { timeout: 30_000 });
240
+ if (result.status !== 0) return false;
241
+ return String(result.stdout || "")
242
+ .split("\n")
243
+ .some((line) => /^jobforge\s+v?\d/.test(line.trim()));
244
+ }
245
+
246
+ function uvToolJobforgePath() {
247
+ if (!commandExists("uv")) return "";
248
+ const result = runRaw("uv", ["tool", "dir", "--bin"], { timeout: 15_000 });
249
+ if (result.status !== 0) return "";
250
+ const base = String(result.stdout || "").trim().split("\n").pop() || "";
251
+ if (!base) return "";
252
+ const candidate = path.join(base, process.platform === "win32" ? "jobforge.exe" : "jobforge");
253
+ return existsSync(candidate) ? candidate : "";
254
+ }
255
+
256
+ async function updateRuntimeIfNeeded(target, { force = false } = {}) {
257
+ const source = target.source || "";
258
+ if (["env-bin", "env-python", "repo"].includes(source)) {
259
+ if (force) {
260
+ log.info("This JobForge runtime is a local or custom build; update it with its own installer.");
261
+ }
262
+ return target;
263
+ }
264
+ if (process.env.JOBFORGE_INSTALL_SPEC && !force) return target;
265
+ if (!uvToolInstalled()) {
266
+ if (force) {
267
+ log.warn("The installed JobForge runtime is not managed by uv; update it with its installer.");
268
+ }
269
+ return target;
270
+ }
271
+
272
+ const installed = runtimeVersion(target);
273
+ if (!force && installed && !isOlderVersion(installed, VERSION)) return target;
274
+
275
+ const message = installed
276
+ ? `Update the JobForge runtime ${installed} → ${VERSION}?`
277
+ : `Update the JobForge runtime to ${VERSION}?`;
278
+ const proceed = force || guard(await confirm({ message, initialValue: true }));
279
+ if (!proceed) return target;
280
+
281
+ const progress = spinner();
282
+ progress.start(`Updating JobForge runtime to ${VERSION}...`);
283
+ const result = runRaw(
284
+ "uv",
285
+ ["tool", "install", "--force", "--from", installSpec(), "jobforge"],
286
+ { timeout: 10 * 60_000 },
287
+ );
288
+ if (result.status !== 0) {
289
+ progress.stop("Update failed");
290
+ log.error("The JobForge runtime update failed.");
291
+ log.message((result.stderr || result.stdout || "").trim().slice(0, 1200));
292
+ return target;
293
+ }
294
+ progress.stop("Runtime updated");
295
+
296
+ const updated = uvToolJobforgePath();
297
+ if (updated && isPythonJobForge(updated, [])) {
298
+ return {
299
+ cmd: updated,
300
+ prefix: [],
301
+ config: process.env.JOBFORGE_CONFIG || "",
302
+ source: "uv-tool",
303
+ };
304
+ }
305
+ return resolveJobForge() || target;
306
+ }
307
+
308
+ async function ensureRuntime({ forceUpdate = false, skipUpdate = false } = {}) {
309
+ let existing = resolveJobForge();
310
+ if (existing) {
311
+ if (!skipUpdate) existing = await updateRuntimeIfNeeded(existing, { force: forceUpdate });
312
+ return existing;
313
+ }
180
314
 
181
315
  log.warn("JobForge runtime not found.");
182
316
  if (!commandExists("uv")) {
@@ -197,9 +331,7 @@ async function ensureRuntime() {
197
331
  );
198
332
  if (!proceed) return null;
199
333
 
200
- const spec =
201
- process.env.JOBFORGE_INSTALL_SPEC ||
202
- "git+https://github.com/Taf0711/jobforge@main";
334
+ const spec = installSpec();
203
335
  const result = runRaw(
204
336
  "uv",
205
337
  ["tool", "install", "--from", spec, "jobforge"],
@@ -210,6 +342,15 @@ async function ensureRuntime() {
210
342
  log.message((result.stderr || result.stdout || "").trim().slice(0, 1200));
211
343
  return null;
212
344
  }
345
+ const installed = uvToolJobforgePath();
346
+ if (installed && isPythonJobForge(installed, [])) {
347
+ return {
348
+ cmd: installed,
349
+ prefix: [],
350
+ config: process.env.JOBFORGE_CONFIG || "",
351
+ source: "uv-tool",
352
+ };
353
+ }
213
354
  return resolveJobForge();
214
355
  }
215
356
 
@@ -327,12 +468,21 @@ async function main() {
327
468
  return;
328
469
  }
329
470
  if (process.argv.includes("--check-runtime")) {
330
- console.log(JSON.stringify(resolveJobForge(), null, 2));
471
+ const resolved = resolveJobForge();
472
+ console.log(
473
+ JSON.stringify(
474
+ resolved ? { ...resolved, version: runtimeVersion(resolved) } : null,
475
+ null,
476
+ 2,
477
+ ),
478
+ );
331
479
  return;
332
480
  }
333
481
 
334
482
  intro(`JobForge ${VERSION}`);
335
- const target = await ensureRuntime();
483
+ const forceUpdate = process.argv.includes("--update");
484
+ const skipUpdate = process.argv.includes("--no-update") && !forceUpdate;
485
+ const target = await ensureRuntime({ forceUpdate, skipUpdate });
336
486
  if (!target) {
337
487
  log.error("JobForge is not installed or its Python runtime is not available.");
338
488
  note(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobforge",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Install the JobForge local MCP server and Agent Skill into your coding agent.",
5
5
  "type": "module",
6
6
  "bin": {