job-apply-plugin 0.2.0 → 0.5.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/marketplace.json +1 -1
- package/README.md +65 -9
- package/bin/job-apply.js +277 -28
- package/package.json +9 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -74,24 +74,80 @@ npm install -g job-apply-plugin && job-apply-plugin install
|
|
|
74
74
|
The installer never overwrites a folder it didn't create unless you pass `--force`.
|
|
75
75
|
When you add Job Apply to an existing `marketplace.json`, the other entries in that file are kept.
|
|
76
76
|
|
|
77
|
-
##
|
|
77
|
+
## Updating
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx job-apply-plugin@latest update
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This updates Job Apply to the newest version in every app where it's installed, without the
|
|
84
|
+
menu. `npx job-apply-plugin status` tells you when a newer version is out.
|
|
85
|
+
|
|
86
|
+
To switch to a specific version, including going back to an older one:
|
|
78
87
|
|
|
79
88
|
```bash
|
|
80
|
-
npx job-apply-plugin
|
|
81
|
-
npx job-apply-plugin install # run again to update
|
|
82
|
-
npx job-apply-plugin uninstall # removes it from both apps and keeps your profile and history
|
|
83
|
-
npx job-apply-plugin uninstall --chatgpt # or from just one app (--claude / --chatgpt)
|
|
89
|
+
npx job-apply-plugin@latest update 0.3.0
|
|
84
90
|
```
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
or
|
|
92
|
+
List the available versions with `npm view job-apply-plugin versions`. Updating never touches
|
|
93
|
+
your résumé, profile or history. `--claude` or `--chatgpt` limits the update to one app.
|
|
94
|
+
|
|
95
|
+
- **ChatGPT:** after updating, open **Plugins → Job Apply** and reinstall it, then restart the
|
|
96
|
+
app. ChatGPT keeps its own copy and doesn't pick up new files until you do.
|
|
97
|
+
- **Claude Code:** start a new session to use the new version.
|
|
98
|
+
|
|
99
|
+
> Keep `@latest` in the command. Without it, npx may run a copy it downloaded earlier.
|
|
100
|
+
> Versions before 0.5.0 have no `update` command; for those, `npx job-apply-plugin@latest update`
|
|
101
|
+
> still works, because it always runs the newest installer.
|
|
102
|
+
|
|
103
|
+
## Uninstall
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx job-apply-plugin uninstall
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
It asks which app to remove Job Apply from, and shows where it's currently installed:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Remove Job Apply from which app?
|
|
113
|
+
1) Claude Code (installed v0.5.0)
|
|
114
|
+
2) ChatGPT desktop / Codex (not installed)
|
|
115
|
+
3) Both
|
|
116
|
+
Choose 1, 2 or 3 [1]:
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
To skip the menu, use `--claude`, `--chatgpt` or `--all`. `remove` works as another name for `uninstall`.
|
|
120
|
+
|
|
121
|
+
What it removes:
|
|
122
|
+
|
|
123
|
+
| App | Removed |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| Claude Code | The two skill folders in `~/.claude/skills`, plus a project install (`./.claude/skills`) in the folder you run it from |
|
|
126
|
+
| ChatGPT / Codex | `~/.codex/plugins/job-apply`, its entry in `~/.agents/plugins/marketplace.json`, ChatGPT's own installed copy (`~/.codex/plugins/cache/…/job-apply`), and its settings in `~/.codex/config.toml` |
|
|
127
|
+
|
|
128
|
+
Restart the app afterwards. It only deletes things that belong to this plugin; other plugins
|
|
129
|
+
and skills are left alone. A project install in a different folder has to be removed from that
|
|
130
|
+
folder (`cd` there and run `uninstall`).
|
|
131
|
+
|
|
132
|
+
Uninstalling keeps your résumé, profile and application history in `~/Documents/Job Apply`.
|
|
133
|
+
To erase those too, ask the assistant to "clear my Job Apply profile" before you uninstall,
|
|
134
|
+
or delete that folder.
|
|
135
|
+
|
|
136
|
+
> `npm uninstall -g job-apply-plugin` only removes the installer, not the plugin.
|
|
137
|
+
> Run `uninstall` first.
|
|
138
|
+
|
|
139
|
+
## Other commands
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx job-apply-plugin status # where it's installed, whether an update is out, and whether Python is OK
|
|
143
|
+
```
|
|
88
144
|
|
|
89
145
|
## Installing from the Git repository (alternative)
|
|
90
146
|
|
|
91
147
|
The repository is also a plugin marketplace for both apps.
|
|
92
148
|
|
|
93
|
-
- Claude Code: `claude plugin marketplace add
|
|
94
|
-
- Codex: `codex plugin marketplace add
|
|
149
|
+
- Claude Code: `claude plugin marketplace add filipelinemburger/job-apply` then `claude plugin install job-apply@job-apply`
|
|
150
|
+
- Codex: `codex plugin marketplace add https://github.com/filipelinemburger/job-apply.git`
|
|
95
151
|
|
|
96
152
|
## Privacy
|
|
97
153
|
|
package/bin/job-apply.js
CHANGED
|
@@ -22,6 +22,15 @@ const DEFAULT_MARKETPLACE = "local-plugins";
|
|
|
22
22
|
// ---------- helpers ----------
|
|
23
23
|
|
|
24
24
|
const home = () => os.homedir();
|
|
25
|
+
// True when two paths name the same folder, even through symlinks (e.g. /var vs /private/var on macOS).
|
|
26
|
+
const real = (p) => {
|
|
27
|
+
try {
|
|
28
|
+
return fs.realpathSync(p);
|
|
29
|
+
} catch {
|
|
30
|
+
return path.resolve(p);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const samePath = (a, b) => real(a) === real(b);
|
|
25
34
|
const exists = (p) => fs.existsSync(p);
|
|
26
35
|
const log = (msg = "") => process.stdout.write(msg + "\n");
|
|
27
36
|
|
|
@@ -117,6 +126,17 @@ const claude = {
|
|
|
117
126
|
detect: () => exists(path.join(home(), ".claude")) || onPath("claude"),
|
|
118
127
|
skillsDir: (opts) =>
|
|
119
128
|
opts.project ? path.join(process.cwd(), ".claude", "skills") : path.join(home(), ".claude", "skills"),
|
|
129
|
+
// Where uninstall/status look: --project → only this folder; otherwise the user folder plus this folder's project install.
|
|
130
|
+
skillDirs(opts) {
|
|
131
|
+
if (opts.project) return [this.skillsDir(opts)];
|
|
132
|
+
const user = this.skillsDir({});
|
|
133
|
+
const project = path.join(process.cwd(), ".claude", "skills");
|
|
134
|
+
return samePath(project, user) || samePath(process.cwd(), home()) ? [user] : [user, project];
|
|
135
|
+
},
|
|
136
|
+
installedIn(base) {
|
|
137
|
+
const markers = skillNames().map((n) => readMarker(path.join(base, n)));
|
|
138
|
+
return markers.every(Boolean) ? markers[0].version : markers.some(Boolean) ? "partial" : null;
|
|
139
|
+
},
|
|
120
140
|
|
|
121
141
|
install(opts) {
|
|
122
142
|
const base = this.skillsDir(opts);
|
|
@@ -131,17 +151,25 @@ const claude = {
|
|
|
131
151
|
},
|
|
132
152
|
|
|
133
153
|
uninstall(opts) {
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
154
|
+
let any = false;
|
|
155
|
+
for (const base of this.skillDirs(opts)) {
|
|
156
|
+
const removed = skillNames().filter((name) => removeOwnedDir(path.join(base, name)));
|
|
157
|
+
if (removed.length) {
|
|
158
|
+
any = true;
|
|
159
|
+
log(`✔ Claude Code: removed ${removed.join(", ")} from ${base}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (any) log(` Restart any open Claude Code sessions so they stop offering the skills.`);
|
|
163
|
+
else log(`• Claude Code: nothing to remove`);
|
|
137
164
|
},
|
|
138
165
|
|
|
139
166
|
status(opts) {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
167
|
+
const found = this.skillDirs(opts)
|
|
168
|
+
.map((base) => [base, this.installedIn(base)])
|
|
169
|
+
.filter(([, v]) => v);
|
|
170
|
+
if (found.length === 0) return "not installed";
|
|
171
|
+
if (found.some(([, v]) => v === "partial")) return `partially installed (${found.map(([b]) => b).join(", ")}) — run install again`;
|
|
172
|
+
return `installed v${found[0][1]} (${found.map(([b]) => b).join(", ")})`;
|
|
145
173
|
},
|
|
146
174
|
};
|
|
147
175
|
|
|
@@ -184,12 +212,68 @@ const chatgpt = {
|
|
|
184
212
|
if (i >= 0) log(` (Updating? Reinstall it from the Plugins page so ChatGPT refreshes its cached copy.)`);
|
|
185
213
|
},
|
|
186
214
|
|
|
215
|
+
cacheRoot: () => path.join(home(), ".codex", "plugins", "cache"),
|
|
216
|
+
configFile: () => path.join(home(), ".codex", "config.toml"),
|
|
217
|
+
|
|
218
|
+
// ChatGPT keeps its own installed copy at cache/<marketplace>/<plugin>/<version>/.
|
|
219
|
+
// Only folders whose manifest is this plugin (same name and author) count as ours.
|
|
220
|
+
cachedCopies() {
|
|
221
|
+
const root = this.cacheRoot();
|
|
222
|
+
if (!exists(root)) return [];
|
|
223
|
+
const ours = (manifest) => {
|
|
224
|
+
try {
|
|
225
|
+
const m = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
226
|
+
return m.name === PLUGIN_NAME && m.author?.name === PLUGIN.author?.name;
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const copies = [];
|
|
232
|
+
for (const market of fs.readdirSync(root, { withFileTypes: true })) {
|
|
233
|
+
if (!market.isDirectory()) continue;
|
|
234
|
+
const dir = path.join(root, market.name, PLUGIN_NAME);
|
|
235
|
+
if (!exists(dir)) continue;
|
|
236
|
+
const versions = fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
237
|
+
const matches = versions.some((v) =>
|
|
238
|
+
["plugin.json", path.join(".codex-plugin", "plugin.json")].some((m) => ours(path.join(dir, v.name, m)))
|
|
239
|
+
);
|
|
240
|
+
if (matches) copies.push({ market: market.name, dir });
|
|
241
|
+
}
|
|
242
|
+
return copies;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// Drops the [plugins."job-apply@<market>"] tables ChatGPT writes when the plugin is installed or toggled.
|
|
246
|
+
removeConfigEntries(markets) {
|
|
247
|
+
const file = this.configFile();
|
|
248
|
+
if (!exists(file) || markets.length === 0) return 0;
|
|
249
|
+
const headers = new Set(markets.map((m) => `[plugins."${PLUGIN_NAME}@${m}"]`));
|
|
250
|
+
const out = [];
|
|
251
|
+
let skipping = false;
|
|
252
|
+
let removed = 0;
|
|
253
|
+
for (const line of fs.readFileSync(file, "utf8").split(/(?<=\n)/)) {
|
|
254
|
+
const trimmed = line.trim();
|
|
255
|
+
if (trimmed.startsWith("[")) {
|
|
256
|
+
skipping = headers.has(trimmed.replace(/\s*#.*$/, ""));
|
|
257
|
+
if (skipping) removed++;
|
|
258
|
+
}
|
|
259
|
+
if (!skipping) out.push(line);
|
|
260
|
+
}
|
|
261
|
+
if (removed) {
|
|
262
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
263
|
+
fs.writeFileSync(tmp, out.join(""));
|
|
264
|
+
fs.renameSync(tmp, file);
|
|
265
|
+
}
|
|
266
|
+
return removed;
|
|
267
|
+
},
|
|
268
|
+
|
|
187
269
|
uninstall() {
|
|
188
270
|
const removedDir = removeOwnedDir(this.pluginDir());
|
|
189
271
|
const file = this.marketplaceFile();
|
|
190
272
|
let removedEntry = false;
|
|
273
|
+
const markets = new Set([DEFAULT_MARKETPLACE]);
|
|
191
274
|
if (exists(file)) {
|
|
192
275
|
const market = readJson(file);
|
|
276
|
+
if (typeof market.name === "string") markets.add(market.name);
|
|
193
277
|
const before = market.plugins?.length ?? 0;
|
|
194
278
|
market.plugins = (market.plugins ?? []).filter((p) => p?.name !== PLUGIN_NAME);
|
|
195
279
|
removedEntry = market.plugins.length !== before;
|
|
@@ -198,8 +282,20 @@ const chatgpt = {
|
|
|
198
282
|
else writeJson(file, market);
|
|
199
283
|
}
|
|
200
284
|
}
|
|
201
|
-
|
|
202
|
-
|
|
285
|
+
const copies = this.cachedCopies();
|
|
286
|
+
for (const c of copies) {
|
|
287
|
+
fs.rmSync(c.dir, { recursive: true, force: true });
|
|
288
|
+
markets.add(c.market);
|
|
289
|
+
}
|
|
290
|
+
const settings = this.removeConfigEntries([...markets]);
|
|
291
|
+
|
|
292
|
+
if (removedDir || removedEntry || copies.length || settings) {
|
|
293
|
+
log(`✔ ChatGPT/Codex: removed ${PLUGIN_NAME}:`);
|
|
294
|
+
if (removedDir) log(` - plugin files in ${this.pluginDir()}`);
|
|
295
|
+
if (removedEntry) log(` - its entry in ${file}`);
|
|
296
|
+
for (const c of copies) log(` - ChatGPT's installed copy in ${c.dir}`);
|
|
297
|
+
if (settings) log(` - its settings in ${this.configFile()}`);
|
|
298
|
+
log(` Restart the ChatGPT desktop app to finish.`);
|
|
203
299
|
} else {
|
|
204
300
|
log(`• ChatGPT/Codex: nothing to remove`);
|
|
205
301
|
}
|
|
@@ -214,12 +310,119 @@ const chatgpt = {
|
|
|
214
310
|
} catch {}
|
|
215
311
|
if (marker && listed) return `installed v${marker.version} (${this.pluginDir()})`;
|
|
216
312
|
if (marker || listed) return "partially installed — run install again";
|
|
313
|
+
if (this.cachedCopies().length) return "leftover files from an earlier install — run uninstall to clean up";
|
|
217
314
|
return "not installed";
|
|
218
315
|
},
|
|
219
316
|
};
|
|
220
317
|
|
|
221
318
|
const TARGETS = { claude, chatgpt };
|
|
222
319
|
|
|
320
|
+
// ---------- versions & updates ----------
|
|
321
|
+
|
|
322
|
+
// Compares x.y.z versions numerically; prerelease tags are ignored. Returns <0, 0 or >0.
|
|
323
|
+
export function compareVersions(a, b) {
|
|
324
|
+
const pa = String(a).split("-")[0].split(".").map(Number);
|
|
325
|
+
const pb = String(b).split("-")[0].split(".").map(Number);
|
|
326
|
+
for (let i = 0; i < 3; i++) {
|
|
327
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
328
|
+
if (d) return d;
|
|
329
|
+
}
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const FIRST_RELEASE = "0.1.0";
|
|
334
|
+
const registryUrl = (spec) =>
|
|
335
|
+
`${(process.env.npm_config_registry || "https://registry.npmjs.org/").replace(/\/?$/, "/")}${PKG.name}${spec ? `/${spec}` : ""}`;
|
|
336
|
+
|
|
337
|
+
async function registryGet(spec) {
|
|
338
|
+
const res = await fetch(registryUrl(spec), { signal: AbortSignal.timeout(8000), headers: { accept: "application/json" } });
|
|
339
|
+
if (res.status === 404) return null;
|
|
340
|
+
if (!res.ok) throw new Error(`npm registry answered ${res.status}`);
|
|
341
|
+
return res.json();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function latestVersion() {
|
|
345
|
+
return (await registryGet("latest"))?.version ?? null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function releasedVersions() {
|
|
349
|
+
const doc = await registryGet("");
|
|
350
|
+
return Object.keys(doc?.versions ?? {})
|
|
351
|
+
.filter((v) => /^\d+\.\d+\.\d+$/.test(v) && compareVersions(v, FIRST_RELEASE) >= 0)
|
|
352
|
+
.sort(compareVersions);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Where the plugin is installed now, as install jobs that any released version understands.
|
|
356
|
+
function installedJobs(values, opts) {
|
|
357
|
+
const flagged = flagTargets(values);
|
|
358
|
+
const want = (t) => flagged.length === 0 || flagged.includes(t);
|
|
359
|
+
const jobs = [];
|
|
360
|
+
if (want(claude)) {
|
|
361
|
+
const userDir = claude.skillsDir({});
|
|
362
|
+
for (const base of claude.skillDirs(opts)) {
|
|
363
|
+
const v = claude.installedIn(base);
|
|
364
|
+
if (!v) continue;
|
|
365
|
+
const project = !samePath(base, userDir);
|
|
366
|
+
jobs.push({ target: claude, project, version: v, where: base });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (want(chatgpt)) {
|
|
370
|
+
const v = readMarker(chatgpt.pluginDir())?.version;
|
|
371
|
+
if (v || chatgpt.status().startsWith("partially")) jobs.push({ target: chatgpt, project: false, version: v ?? "partial", where: chatgpt.pluginDir() });
|
|
372
|
+
}
|
|
373
|
+
return jobs;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function runOtherVersion(version, job) {
|
|
377
|
+
const args = ["-y", `${PKG.name}@${version}`, "install", job.target === claude ? "--claude" : "--chatgpt"];
|
|
378
|
+
if (job.project) args.push("--project");
|
|
379
|
+
const npx = process.env.JOB_APPLY_NPX || (process.platform === "win32" ? "npx.cmd" : "npx");
|
|
380
|
+
const r = spawnSync(npx, args, { stdio: "inherit", shell: process.platform === "win32" });
|
|
381
|
+
if (r.status !== 0) {
|
|
382
|
+
throw new CliError(`Installing ${PKG.name}@${version} failed. Try it directly:\n npx ${args.slice(1).join(" ")}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function update(values, opts, requested) {
|
|
387
|
+
const jobs = installedJobs(values, opts);
|
|
388
|
+
if (jobs.length === 0) {
|
|
389
|
+
log(`Job Apply isn't installed${flagTargets(values).length ? " there" : ""}. Run "npx ${PKG.name}@latest install" first.`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let target = requested;
|
|
394
|
+
if (target) {
|
|
395
|
+
if (!/^\d+\.\d+\.\d+$/.test(target)) throw new CliError(`"${target}" isn't a version number. Use something like 0.3.0.`);
|
|
396
|
+
const versions = await releasedVersions().catch(() => null);
|
|
397
|
+
if (versions && !versions.includes(target)) {
|
|
398
|
+
throw new CliError(`Version ${target} doesn't exist. Available: ${versions.join(", ")}`);
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
try {
|
|
402
|
+
target = await latestVersion();
|
|
403
|
+
} catch (err) {
|
|
404
|
+
throw new CliError(`Couldn't reach npm to find the latest version (${err.message}). Check your connection and try again.`);
|
|
405
|
+
}
|
|
406
|
+
if (!target) throw new CliError(`Couldn't find ${PKG.name} on npm.`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const current = [...new Set(jobs.map((j) => j.version))].join(", ");
|
|
410
|
+
const direction = jobs.every((j) => j.version === target)
|
|
411
|
+
? "Reinstalling"
|
|
412
|
+
: compareVersions(target, jobs[0].version === "partial" ? "0.0.0" : jobs[0].version) < 0
|
|
413
|
+
? "Switching back to"
|
|
414
|
+
: "Updating to";
|
|
415
|
+
log(`${direction} v${target} (installed: v${current}) in ${jobs.map((j) => j.target.label + (j.project ? " (this project)" : "")).join(" and ")}.`);
|
|
416
|
+
log();
|
|
417
|
+
|
|
418
|
+
for (const job of jobs) {
|
|
419
|
+
if (target === PKG.version) job.target.install({ ...opts, project: job.project });
|
|
420
|
+
else runOtherVersion(target, job);
|
|
421
|
+
}
|
|
422
|
+
log();
|
|
423
|
+
log(`Done. Your résumé, profile and history in ~/Documents/Job Apply are unchanged.`);
|
|
424
|
+
}
|
|
425
|
+
|
|
223
426
|
// ---------- CLI ----------
|
|
224
427
|
|
|
225
428
|
const HELP = `${PKG.name} v${PKG.version}
|
|
@@ -231,9 +434,12 @@ Usage
|
|
|
231
434
|
yarn dlx ${PKG.name} <command> [options]
|
|
232
435
|
|
|
233
436
|
Commands
|
|
234
|
-
install Install (or
|
|
235
|
-
|
|
236
|
-
|
|
437
|
+
install Install the plugin (asks Claude Code, ChatGPT or both)
|
|
438
|
+
update Update wherever it's installed to the latest version
|
|
439
|
+
update <v> Switch to a specific version, e.g. "update 0.3.0" (also goes back to older ones)
|
|
440
|
+
uninstall Remove the plugin from Claude Code, ChatGPT or both (alias: remove).
|
|
441
|
+
Your profile and history in ~/Documents/Job Apply are kept.
|
|
442
|
+
status Show where it's installed, whether an update is available, and check Python
|
|
237
443
|
|
|
238
444
|
Options
|
|
239
445
|
--claude Claude Code only
|
|
@@ -246,7 +452,7 @@ Options
|
|
|
246
452
|
|
|
247
453
|
Run "install" with no app flag to pick from a menu: Claude Code, ChatGPT, or both.
|
|
248
454
|
Without a terminal to ask in (CI, scripts), it installs into the apps found on this computer.
|
|
249
|
-
"uninstall" with no app flag
|
|
455
|
+
"uninstall" with no app flag shows the same menu (without a terminal, it removes from both).`;
|
|
250
456
|
|
|
251
457
|
const CHOICES = {
|
|
252
458
|
1: ["claude"],
|
|
@@ -271,16 +477,16 @@ function flagTargets(values) {
|
|
|
271
477
|
return chosen;
|
|
272
478
|
}
|
|
273
479
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
log(
|
|
278
|
-
log(` 1) Claude Code${
|
|
279
|
-
log(` 2) ChatGPT desktop / Codex${
|
|
480
|
+
// Shows the 1/2/3 menu. `notes` maps each target to a hint shown next to it; `verb` is "installed" or "removed".
|
|
481
|
+
async function askTargets({ title, notes, defaultChoice, verb }) {
|
|
482
|
+
const cancelled = `Cancelled. Nothing was ${verb}.`;
|
|
483
|
+
log(title);
|
|
484
|
+
log(` 1) Claude Code${notes(claude)}`);
|
|
485
|
+
log(` 2) ChatGPT desktop / Codex${notes(chatgpt)}`);
|
|
280
486
|
log(` 3) Both`);
|
|
281
487
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
282
488
|
rl.on("SIGINT", () => {
|
|
283
|
-
log(
|
|
489
|
+
log(`\n${cancelled}`);
|
|
284
490
|
process.exit(130);
|
|
285
491
|
});
|
|
286
492
|
try {
|
|
@@ -290,7 +496,7 @@ async function askTargets(found) {
|
|
|
290
496
|
answer = await rl.question(`Choose 1, 2 or 3 [${defaultChoice}]: `);
|
|
291
497
|
} catch (err) {
|
|
292
498
|
// Ctrl+D or a closed terminal
|
|
293
|
-
if (err?.name === "AbortError" || err?.code === "ABORT_ERR") throw new CliError(
|
|
499
|
+
if (err?.name === "AbortError" || err?.code === "ABORT_ERR") throw new CliError(cancelled);
|
|
294
500
|
throw err;
|
|
295
501
|
}
|
|
296
502
|
const ids = parseChoice(answer, defaultChoice);
|
|
@@ -303,20 +509,49 @@ async function askTargets(found) {
|
|
|
303
509
|
} finally {
|
|
304
510
|
rl.close();
|
|
305
511
|
}
|
|
306
|
-
throw new CliError(
|
|
512
|
+
throw new CliError(`No choice made. Nothing was ${verb}.`);
|
|
307
513
|
}
|
|
308
514
|
|
|
515
|
+
const interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
516
|
+
const choiceFor = (list) => (list.length === 1 ? (list[0] === claude ? "1" : "2") : "3");
|
|
517
|
+
|
|
309
518
|
async function pickInstallTargets(values) {
|
|
310
519
|
const flagged = flagTargets(values);
|
|
311
520
|
if (flagged.length) return flagged;
|
|
312
521
|
const found = Object.values(TARGETS).filter((t) => t.detect());
|
|
313
|
-
if (
|
|
522
|
+
if (interactive()) {
|
|
523
|
+
return askTargets({
|
|
524
|
+
title: "Where should Job Apply be installed?",
|
|
525
|
+
notes: (t) => (found.includes(t) ? " (found on this computer)" : ""),
|
|
526
|
+
defaultChoice: choiceFor(found),
|
|
527
|
+
verb: "installed",
|
|
528
|
+
});
|
|
529
|
+
}
|
|
314
530
|
// No terminal to ask in: fall back to the apps found here (both if none).
|
|
315
531
|
const targets = found.length ? found : Object.values(TARGETS);
|
|
316
532
|
log(`No app chosen; installing for ${targets.map((t) => t.label).join(" and ")}. Use --claude, --chatgpt or --all to choose.`);
|
|
317
533
|
return targets;
|
|
318
534
|
}
|
|
319
535
|
|
|
536
|
+
// Returns [] when there is nothing to remove.
|
|
537
|
+
async function pickUninstallTargets(values, opts) {
|
|
538
|
+
const flagged = flagTargets(values);
|
|
539
|
+
if (flagged.length) return flagged;
|
|
540
|
+
const statuses = new Map(Object.values(TARGETS).map((t) => [t, t.status(opts)]));
|
|
541
|
+
const installed = [...statuses].filter(([, s]) => s !== "not installed").map(([t]) => t);
|
|
542
|
+
if (installed.length === 0) {
|
|
543
|
+
log("Job Apply isn't installed in Claude Code or ChatGPT on this computer. Nothing to remove.");
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
if (!interactive()) return Object.values(TARGETS);
|
|
547
|
+
return askTargets({
|
|
548
|
+
title: "Remove Job Apply from which app?",
|
|
549
|
+
notes: (t) => ` (${statuses.get(t).replace(/ \(.*$/, "").replace(/ — .*$/, "")})`,
|
|
550
|
+
defaultChoice: choiceFor(installed),
|
|
551
|
+
verb: "removed",
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
320
555
|
export async function main(argv = process.argv.slice(2)) {
|
|
321
556
|
const { values, positionals } = parseArgs({
|
|
322
557
|
args: argv,
|
|
@@ -348,14 +583,28 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
348
583
|
}
|
|
349
584
|
log();
|
|
350
585
|
log(`Your résumé, profile and application history are stored locally in ~/Documents/Job Apply.`);
|
|
351
|
-
} else if (command === "uninstall") {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
586
|
+
} else if (command === "uninstall" || command === "remove") {
|
|
587
|
+
const targets = await pickUninstallTargets(values, opts);
|
|
588
|
+
if (targets.length === 0) return;
|
|
589
|
+
for (const t of targets) t.uninstall(opts);
|
|
590
|
+
log();
|
|
591
|
+
log(`Your résumé, profile and history in ~/Documents/Job Apply were kept.`);
|
|
592
|
+
log(`To erase them too, delete that folder or ask the assistant to "clear my Job Apply profile" before uninstalling.`);
|
|
593
|
+
} else if (command === "update" || command === "upgrade") {
|
|
594
|
+
await update(values, opts, positionals[1]);
|
|
355
595
|
} else if (command === "status") {
|
|
356
596
|
for (const t of Object.values(TARGETS)) log(`${t.label.padEnd(24)} ${t.status(opts)}`);
|
|
357
597
|
const py = pythonCheck();
|
|
358
598
|
log(`${"Python 3.9+".padEnd(24)} ${py.ok ? `ok (${py.version})` : py.version ? `too old (${py.version})` : "not found"}`);
|
|
599
|
+
const latest = await latestVersion().catch(() => undefined);
|
|
600
|
+
const installed = installedJobs({}, opts).map((j) => j.version).filter((v) => v !== "partial");
|
|
601
|
+
let line;
|
|
602
|
+
if (latest === undefined) line = "couldn't check npm (offline?)";
|
|
603
|
+
else if (!latest) line = "not found on npm";
|
|
604
|
+
else if (installed.some((v) => compareVersions(v, latest) < 0))
|
|
605
|
+
line = `v${latest} available — run: npx ${PKG.name}@latest update`;
|
|
606
|
+
else line = `v${latest}${installed.length ? " (up to date)" : ""}`;
|
|
607
|
+
log(`${"Latest version".padEnd(24)} ${line}`);
|
|
359
608
|
} else {
|
|
360
609
|
throw new CliError(`Unknown command "${command}". Run with --help.`);
|
|
361
610
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-apply-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Job Apply plugin for Claude Code and ChatGPT/Codex: prepare and submit job applications from a résumé and profile saved on your device.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
"resume"
|
|
15
15
|
],
|
|
16
16
|
"author": "Filipe Linemburger",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/filipelinemburger/job-apply.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/filipelinemburger/job-apply#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/filipelinemburger/job-apply/issues"
|
|
24
|
+
},
|
|
17
25
|
"type": "module",
|
|
18
26
|
"bin": {
|
|
19
27
|
"job-apply-plugin": "bin/job-apply.js"
|
package/plugin/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "job-apply",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Prepare and submit individual job applications using a résumé and profile saved on your device.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"jobs",
|