job-apply-plugin 0.1.0 → 0.2.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 +17 -2
- package/bin/job-apply.js +83 -18
- package/package.json +1 -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
|
|
@@ -65,7 +79,8 @@ When you add Job Apply to an existing `marketplace.json`, the other entries in t
|
|
|
65
79
|
```bash
|
|
66
80
|
npx job-apply-plugin status # where it's installed and whether Python is OK
|
|
67
81
|
npx job-apply-plugin install # run again to update
|
|
68
|
-
npx job-apply-plugin uninstall # removes
|
|
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)
|
|
69
84
|
```
|
|
70
85
|
|
|
71
86
|
To erase your saved profile and history, ask the assistant to "clear my Job Apply profile",
|
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,
|
|
@@ -233,25 +236,88 @@ Commands
|
|
|
233
236
|
status Show where the plugin is installed and check Python
|
|
234
237
|
|
|
235
238
|
Options
|
|
236
|
-
--claude
|
|
237
|
-
--chatgpt
|
|
239
|
+
--claude Claude Code only
|
|
240
|
+
--chatgpt ChatGPT desktop / Codex only (alias: --codex)
|
|
241
|
+
--all Both apps
|
|
238
242
|
--project Claude Code: use ./.claude/skills in this folder instead of ~/.claude/skills
|
|
239
243
|
--force Overwrite existing folders that were not installed by this tool
|
|
240
244
|
-h, --help Show this help
|
|
241
245
|
-v, --version
|
|
242
246
|
|
|
243
|
-
|
|
247
|
+
Run "install" with no app flag to pick from a menu: Claude Code, ChatGPT, or both.
|
|
248
|
+
Without a terminal to ask in (CI, scripts), it installs into the apps found on this computer.
|
|
249
|
+
"uninstall" with no app flag removes the plugin from both apps.`;
|
|
244
250
|
|
|
245
|
-
|
|
251
|
+
const CHOICES = {
|
|
252
|
+
1: ["claude"],
|
|
253
|
+
2: ["chatgpt"],
|
|
254
|
+
3: ["claude", "chatgpt"],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Turns a menu answer into target ids; empty input takes the default. Returns null if unrecognised.
|
|
258
|
+
export function parseChoice(answer, defaultChoice = "3") {
|
|
259
|
+
const a = String(answer ?? "").trim().toLowerCase() || String(defaultChoice);
|
|
260
|
+
if (["1", "claude", "claude code", "c"].includes(a)) return CHOICES[1];
|
|
261
|
+
if (["2", "chatgpt", "codex", "openai", "g"].includes(a)) return CHOICES[2];
|
|
262
|
+
if (["3", "both", "all", "b"].includes(a)) return CHOICES[3];
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function flagTargets(values) {
|
|
267
|
+
if (values.all) return Object.values(TARGETS);
|
|
246
268
|
const chosen = [];
|
|
247
269
|
if (values.claude) chosen.push(claude);
|
|
248
270
|
if (values.chatgpt || values.codex) chosen.push(chatgpt);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
271
|
+
return chosen;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function askTargets(found) {
|
|
275
|
+
const defaultChoice = found.length === 1 ? (found[0] === claude ? "1" : "2") : "3";
|
|
276
|
+
const note = (t) => (found.includes(t) ? " (found on this computer)" : "");
|
|
277
|
+
log("Where should Job Apply be installed?");
|
|
278
|
+
log(` 1) Claude Code${note(claude)}`);
|
|
279
|
+
log(` 2) ChatGPT desktop / Codex${note(chatgpt)}`);
|
|
280
|
+
log(` 3) Both`);
|
|
281
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
282
|
+
rl.on("SIGINT", () => {
|
|
283
|
+
log("\nCancelled. Nothing was installed.");
|
|
284
|
+
process.exit(130);
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
288
|
+
let answer;
|
|
289
|
+
try {
|
|
290
|
+
answer = await rl.question(`Choose 1, 2 or 3 [${defaultChoice}]: `);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
// Ctrl+D or a closed terminal
|
|
293
|
+
if (err?.name === "AbortError" || err?.code === "ABORT_ERR") throw new CliError("Cancelled. Nothing was installed.");
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
const ids = parseChoice(answer, defaultChoice);
|
|
297
|
+
if (ids) {
|
|
298
|
+
log();
|
|
299
|
+
return ids.map((id) => TARGETS[id]);
|
|
300
|
+
}
|
|
301
|
+
log(" Please type 1, 2 or 3.");
|
|
302
|
+
}
|
|
303
|
+
} finally {
|
|
304
|
+
rl.close();
|
|
305
|
+
}
|
|
306
|
+
throw new CliError("No choice made. Nothing was installed.");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function pickInstallTargets(values) {
|
|
310
|
+
const flagged = flagTargets(values);
|
|
311
|
+
if (flagged.length) return flagged;
|
|
312
|
+
const found = Object.values(TARGETS).filter((t) => t.detect());
|
|
313
|
+
if (process.stdin.isTTY && process.stdout.isTTY) return askTargets(found);
|
|
314
|
+
// No terminal to ask in: fall back to the apps found here (both if none).
|
|
315
|
+
const targets = found.length ? found : Object.values(TARGETS);
|
|
316
|
+
log(`No app chosen; installing for ${targets.map((t) => t.label).join(" and ")}. Use --claude, --chatgpt or --all to choose.`);
|
|
317
|
+
return targets;
|
|
252
318
|
}
|
|
253
319
|
|
|
254
|
-
export function main(argv = process.argv.slice(2)) {
|
|
320
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
255
321
|
const { values, positionals } = parseArgs({
|
|
256
322
|
args: argv,
|
|
257
323
|
allowPositionals: true,
|
|
@@ -259,6 +325,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
259
325
|
claude: { type: "boolean" },
|
|
260
326
|
chatgpt: { type: "boolean" },
|
|
261
327
|
codex: { type: "boolean" },
|
|
328
|
+
all: { type: "boolean" },
|
|
262
329
|
project: { type: "boolean" },
|
|
263
330
|
force: { type: "boolean" },
|
|
264
331
|
help: { type: "boolean", short: "h" },
|
|
@@ -270,10 +337,9 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
270
337
|
if (values.help || !command) return log(HELP);
|
|
271
338
|
|
|
272
339
|
const opts = { project: Boolean(values.project), force: Boolean(values.force) };
|
|
273
|
-
const targets = pickTargets(values);
|
|
274
340
|
|
|
275
341
|
if (command === "install") {
|
|
276
|
-
for (const t of
|
|
342
|
+
for (const t of await pickInstallTargets(values)) t.install(opts);
|
|
277
343
|
const py = pythonCheck();
|
|
278
344
|
if (!py.ok) {
|
|
279
345
|
log();
|
|
@@ -283,7 +349,8 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
283
349
|
log();
|
|
284
350
|
log(`Your résumé, profile and application history are stored locally in ~/Documents/Job Apply.`);
|
|
285
351
|
} else if (command === "uninstall") {
|
|
286
|
-
|
|
352
|
+
const flagged = flagTargets(values);
|
|
353
|
+
for (const t of flagged.length ? flagged : Object.values(TARGETS)) t.uninstall(opts);
|
|
287
354
|
log(`Your local profile in ~/Documents/Job Apply was not touched.`);
|
|
288
355
|
} else if (command === "status") {
|
|
289
356
|
for (const t of Object.values(TARGETS)) log(`${t.label.padEnd(24)} ${t.status(opts)}`);
|
|
@@ -296,13 +363,11 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
296
363
|
|
|
297
364
|
const invokedDirectly = process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
298
365
|
if (invokedDirectly) {
|
|
299
|
-
|
|
300
|
-
main();
|
|
301
|
-
} catch (err) {
|
|
366
|
+
main().catch((err) => {
|
|
302
367
|
if (err instanceof CliError || err?.code?.startsWith?.("ERR_PARSE_ARGS")) {
|
|
303
368
|
process.stderr.write(`✖ ${err.message}\n`);
|
|
304
369
|
process.exit(1);
|
|
305
370
|
}
|
|
306
371
|
throw err;
|
|
307
|
-
}
|
|
372
|
+
});
|
|
308
373
|
}
|
package/package.json
CHANGED
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.2.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",
|