job-apply-plugin 0.1.1 → 0.3.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 +43 -7
- package/bin/job-apply.js +118 -20
- package/package.json +9 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -22,13 +22,27 @@ pnpm dlx job-apply-plugin install
|
|
|
22
22
|
yarn dlx job-apply-plugin install
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
The installer
|
|
25
|
+
The installer asks which version you want:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Where should Job Apply be installed?
|
|
29
|
+
1) Claude Code (found on this computer)
|
|
30
|
+
2) ChatGPT desktop / Codex (found on this computer)
|
|
31
|
+
3) Both
|
|
32
|
+
Choose 1, 2 or 3 [3]:
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Each version goes only into its own app's folder. To skip the menu, for example in a script, use a flag:
|
|
26
36
|
|
|
27
37
|
```bash
|
|
28
38
|
npx job-apply-plugin install --claude # Claude Code only
|
|
29
39
|
npx job-apply-plugin install --chatgpt # ChatGPT desktop / Codex only
|
|
40
|
+
npx job-apply-plugin install --all # both
|
|
30
41
|
```
|
|
31
42
|
|
|
43
|
+
When there's no terminal to show the menu in (CI, scripts) and no flag is given, it installs into
|
|
44
|
+
the apps it finds on the computer.
|
|
45
|
+
|
|
32
46
|
You can also install it globally and use the shorter command after that:
|
|
33
47
|
|
|
34
48
|
```bash
|
|
@@ -60,23 +74,45 @@ npm install -g job-apply-plugin && job-apply-plugin install
|
|
|
60
74
|
The installer never overwrites a folder it didn't create unless you pass `--force`.
|
|
61
75
|
When you add Job Apply to an existing `marketplace.json`, the other entries in that file are kept.
|
|
62
76
|
|
|
77
|
+
## Uninstall
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx job-apply-plugin uninstall
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It asks which app to remove Job Apply from, and shows where it's currently installed:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Remove Job Apply from which app?
|
|
87
|
+
1) Claude Code (installed v0.3.0)
|
|
88
|
+
2) ChatGPT desktop / Codex (not installed)
|
|
89
|
+
3) Both
|
|
90
|
+
Choose 1, 2 or 3 [1]:
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
To skip the menu, use `--claude`, `--chatgpt` or `--all`. `remove` works as another name for `uninstall`.
|
|
94
|
+
For ChatGPT, also click **Uninstall** on its Plugins page, then restart the app.
|
|
95
|
+
|
|
96
|
+
Uninstalling keeps your résumé, profile and application history in `~/Documents/Job Apply`.
|
|
97
|
+
To erase those too, ask the assistant to "clear my Job Apply profile" before you uninstall,
|
|
98
|
+
or delete that folder.
|
|
99
|
+
|
|
100
|
+
> `npm uninstall -g job-apply-plugin` only removes the installer, not the plugin.
|
|
101
|
+
> Run `uninstall` first.
|
|
102
|
+
|
|
63
103
|
## Other commands
|
|
64
104
|
|
|
65
105
|
```bash
|
|
66
106
|
npx job-apply-plugin status # where it's installed and whether Python is OK
|
|
67
107
|
npx job-apply-plugin install # run again to update
|
|
68
|
-
npx job-apply-plugin uninstall # removes the plugin and keeps your profile and history
|
|
69
108
|
```
|
|
70
109
|
|
|
71
|
-
To erase your saved profile and history, ask the assistant to "clear my Job Apply profile",
|
|
72
|
-
or delete `~/Documents/Job Apply`.
|
|
73
|
-
|
|
74
110
|
## Installing from the Git repository (alternative)
|
|
75
111
|
|
|
76
112
|
The repository is also a plugin marketplace for both apps.
|
|
77
113
|
|
|
78
|
-
- Claude Code: `claude plugin marketplace add
|
|
79
|
-
- Codex: `codex plugin marketplace add
|
|
114
|
+
- Claude Code: `claude plugin marketplace add filipelinemburger/job-apply` then `claude plugin install job-apply@job-apply`
|
|
115
|
+
- Codex: `codex plugin marketplace add https://github.com/filipelinemburger/job-apply.git`
|
|
80
116
|
|
|
81
117
|
## Privacy
|
|
82
118
|
|
package/bin/job-apply.js
CHANGED
|
@@ -8,6 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { parseArgs } from "node:util";
|
|
11
|
+
import readline from "node:readline/promises";
|
|
11
12
|
|
|
12
13
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
14
|
const PLUGIN_SRC = path.join(PACKAGE_ROOT, "plugin");
|
|
@@ -54,11 +55,12 @@ function assertReplaceable(dest, force) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function replaceDir(src, dest, { force, target }) {
|
|
58
|
+
function replaceDir(src, dest, { force, target, exclude = [] }) {
|
|
58
59
|
assertReplaceable(dest, force);
|
|
60
|
+
const skip = new Set([...SKIP, ...exclude]);
|
|
59
61
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
60
62
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
61
|
-
fs.cpSync(src, dest, { recursive: true, filter: (p) => !
|
|
63
|
+
fs.cpSync(src, dest, { recursive: true, filter: (p) => !skip.has(path.basename(p)) });
|
|
62
64
|
writeMarker(dest, target);
|
|
63
65
|
}
|
|
64
66
|
|
|
@@ -161,7 +163,8 @@ const chatgpt = {
|
|
|
161
163
|
: { name: DEFAULT_MARKETPLACE, interface: { displayName: "Local Plugins" }, plugins: [] };
|
|
162
164
|
if (!Array.isArray(market.plugins)) market.plugins = [];
|
|
163
165
|
|
|
164
|
-
|
|
166
|
+
// ChatGPT gets the portable plugin; the Claude Code manifest stays out of its copy.
|
|
167
|
+
replaceDir(PLUGIN_SRC, this.pluginDir(), { ...opts, target: this.id, exclude: [".claude-plugin"] });
|
|
165
168
|
|
|
166
169
|
const entry = {
|
|
167
170
|
name: PLUGIN_NAME,
|
|
@@ -229,29 +232,122 @@ Usage
|
|
|
229
232
|
|
|
230
233
|
Commands
|
|
231
234
|
install Install (or update) the plugin
|
|
232
|
-
uninstall Remove the plugin
|
|
235
|
+
uninstall Remove the plugin from Claude Code, ChatGPT or both (alias: remove).
|
|
236
|
+
Your profile and history in ~/Documents/Job Apply are kept.
|
|
233
237
|
status Show where the plugin is installed and check Python
|
|
234
238
|
|
|
235
239
|
Options
|
|
236
|
-
--claude
|
|
237
|
-
--chatgpt
|
|
240
|
+
--claude Claude Code only
|
|
241
|
+
--chatgpt ChatGPT desktop / Codex only (alias: --codex)
|
|
242
|
+
--all Both apps
|
|
238
243
|
--project Claude Code: use ./.claude/skills in this folder instead of ~/.claude/skills
|
|
239
244
|
--force Overwrite existing folders that were not installed by this tool
|
|
240
245
|
-h, --help Show this help
|
|
241
246
|
-v, --version
|
|
242
247
|
|
|
243
|
-
|
|
248
|
+
Run "install" with no app flag to pick from a menu: Claude Code, ChatGPT, or both.
|
|
249
|
+
Without a terminal to ask in (CI, scripts), it installs into the apps found on this computer.
|
|
250
|
+
"uninstall" with no app flag shows the same menu (without a terminal, it removes from both).`;
|
|
244
251
|
|
|
245
|
-
|
|
252
|
+
const CHOICES = {
|
|
253
|
+
1: ["claude"],
|
|
254
|
+
2: ["chatgpt"],
|
|
255
|
+
3: ["claude", "chatgpt"],
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Turns a menu answer into target ids; empty input takes the default. Returns null if unrecognised.
|
|
259
|
+
export function parseChoice(answer, defaultChoice = "3") {
|
|
260
|
+
const a = String(answer ?? "").trim().toLowerCase() || String(defaultChoice);
|
|
261
|
+
if (["1", "claude", "claude code", "c"].includes(a)) return CHOICES[1];
|
|
262
|
+
if (["2", "chatgpt", "codex", "openai", "g"].includes(a)) return CHOICES[2];
|
|
263
|
+
if (["3", "both", "all", "b"].includes(a)) return CHOICES[3];
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function flagTargets(values) {
|
|
268
|
+
if (values.all) return Object.values(TARGETS);
|
|
246
269
|
const chosen = [];
|
|
247
270
|
if (values.claude) chosen.push(claude);
|
|
248
271
|
if (values.chatgpt || values.codex) chosen.push(chatgpt);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
272
|
+
return chosen;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Shows the 1/2/3 menu. `notes` maps each target to a hint shown next to it; `verb` is "installed" or "removed".
|
|
276
|
+
async function askTargets({ title, notes, defaultChoice, verb }) {
|
|
277
|
+
const cancelled = `Cancelled. Nothing was ${verb}.`;
|
|
278
|
+
log(title);
|
|
279
|
+
log(` 1) Claude Code${notes(claude)}`);
|
|
280
|
+
log(` 2) ChatGPT desktop / Codex${notes(chatgpt)}`);
|
|
281
|
+
log(` 3) Both`);
|
|
282
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
283
|
+
rl.on("SIGINT", () => {
|
|
284
|
+
log(`\n${cancelled}`);
|
|
285
|
+
process.exit(130);
|
|
286
|
+
});
|
|
287
|
+
try {
|
|
288
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
289
|
+
let answer;
|
|
290
|
+
try {
|
|
291
|
+
answer = await rl.question(`Choose 1, 2 or 3 [${defaultChoice}]: `);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
// Ctrl+D or a closed terminal
|
|
294
|
+
if (err?.name === "AbortError" || err?.code === "ABORT_ERR") throw new CliError(cancelled);
|
|
295
|
+
throw err;
|
|
296
|
+
}
|
|
297
|
+
const ids = parseChoice(answer, defaultChoice);
|
|
298
|
+
if (ids) {
|
|
299
|
+
log();
|
|
300
|
+
return ids.map((id) => TARGETS[id]);
|
|
301
|
+
}
|
|
302
|
+
log(" Please type 1, 2 or 3.");
|
|
303
|
+
}
|
|
304
|
+
} finally {
|
|
305
|
+
rl.close();
|
|
306
|
+
}
|
|
307
|
+
throw new CliError(`No choice made. Nothing was ${verb}.`);
|
|
252
308
|
}
|
|
253
309
|
|
|
254
|
-
|
|
310
|
+
const interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
311
|
+
const choiceFor = (list) => (list.length === 1 ? (list[0] === claude ? "1" : "2") : "3");
|
|
312
|
+
|
|
313
|
+
async function pickInstallTargets(values) {
|
|
314
|
+
const flagged = flagTargets(values);
|
|
315
|
+
if (flagged.length) return flagged;
|
|
316
|
+
const found = Object.values(TARGETS).filter((t) => t.detect());
|
|
317
|
+
if (interactive()) {
|
|
318
|
+
return askTargets({
|
|
319
|
+
title: "Where should Job Apply be installed?",
|
|
320
|
+
notes: (t) => (found.includes(t) ? " (found on this computer)" : ""),
|
|
321
|
+
defaultChoice: choiceFor(found),
|
|
322
|
+
verb: "installed",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// No terminal to ask in: fall back to the apps found here (both if none).
|
|
326
|
+
const targets = found.length ? found : Object.values(TARGETS);
|
|
327
|
+
log(`No app chosen; installing for ${targets.map((t) => t.label).join(" and ")}. Use --claude, --chatgpt or --all to choose.`);
|
|
328
|
+
return targets;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Returns [] when there is nothing to remove.
|
|
332
|
+
async function pickUninstallTargets(values, opts) {
|
|
333
|
+
const flagged = flagTargets(values);
|
|
334
|
+
if (flagged.length) return flagged;
|
|
335
|
+
const statuses = new Map(Object.values(TARGETS).map((t) => [t, t.status(opts)]));
|
|
336
|
+
const installed = [...statuses].filter(([, s]) => s !== "not installed").map(([t]) => t);
|
|
337
|
+
if (installed.length === 0) {
|
|
338
|
+
log("Job Apply isn't installed in Claude Code or ChatGPT on this computer. Nothing to remove.");
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
if (!interactive()) return Object.values(TARGETS);
|
|
342
|
+
return askTargets({
|
|
343
|
+
title: "Remove Job Apply from which app?",
|
|
344
|
+
notes: (t) => ` (${statuses.get(t).replace(/ \(.*$/, "").replace(/ — .*$/, "")})`,
|
|
345
|
+
defaultChoice: choiceFor(installed),
|
|
346
|
+
verb: "removed",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
255
351
|
const { values, positionals } = parseArgs({
|
|
256
352
|
args: argv,
|
|
257
353
|
allowPositionals: true,
|
|
@@ -259,6 +355,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
259
355
|
claude: { type: "boolean" },
|
|
260
356
|
chatgpt: { type: "boolean" },
|
|
261
357
|
codex: { type: "boolean" },
|
|
358
|
+
all: { type: "boolean" },
|
|
262
359
|
project: { type: "boolean" },
|
|
263
360
|
force: { type: "boolean" },
|
|
264
361
|
help: { type: "boolean", short: "h" },
|
|
@@ -270,10 +367,9 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
270
367
|
if (values.help || !command) return log(HELP);
|
|
271
368
|
|
|
272
369
|
const opts = { project: Boolean(values.project), force: Boolean(values.force) };
|
|
273
|
-
const targets = pickTargets(values);
|
|
274
370
|
|
|
275
371
|
if (command === "install") {
|
|
276
|
-
for (const t of
|
|
372
|
+
for (const t of await pickInstallTargets(values)) t.install(opts);
|
|
277
373
|
const py = pythonCheck();
|
|
278
374
|
if (!py.ok) {
|
|
279
375
|
log();
|
|
@@ -282,9 +378,13 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
282
378
|
}
|
|
283
379
|
log();
|
|
284
380
|
log(`Your résumé, profile and application history are stored locally in ~/Documents/Job Apply.`);
|
|
285
|
-
} else if (command === "uninstall") {
|
|
381
|
+
} else if (command === "uninstall" || command === "remove") {
|
|
382
|
+
const targets = await pickUninstallTargets(values, opts);
|
|
383
|
+
if (targets.length === 0) return;
|
|
286
384
|
for (const t of targets) t.uninstall(opts);
|
|
287
|
-
log(
|
|
385
|
+
log();
|
|
386
|
+
log(`Your résumé, profile and history in ~/Documents/Job Apply were kept.`);
|
|
387
|
+
log(`To erase them too, delete that folder or ask the assistant to "clear my Job Apply profile" before uninstalling.`);
|
|
288
388
|
} else if (command === "status") {
|
|
289
389
|
for (const t of Object.values(TARGETS)) log(`${t.label.padEnd(24)} ${t.status(opts)}`);
|
|
290
390
|
const py = pythonCheck();
|
|
@@ -296,13 +396,11 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
296
396
|
|
|
297
397
|
const invokedDirectly = process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
298
398
|
if (invokedDirectly) {
|
|
299
|
-
|
|
300
|
-
main();
|
|
301
|
-
} catch (err) {
|
|
399
|
+
main().catch((err) => {
|
|
302
400
|
if (err instanceof CliError || err?.code?.startsWith?.("ERR_PARSE_ARGS")) {
|
|
303
401
|
process.stderr.write(`✖ ${err.message}\n`);
|
|
304
402
|
process.exit(1);
|
|
305
403
|
}
|
|
306
404
|
throw err;
|
|
307
|
-
}
|
|
405
|
+
});
|
|
308
406
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-apply-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.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",
|