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.
@@ -9,7 +9,7 @@
9
9
  "name": "job-apply",
10
10
  "source": "./plugin",
11
11
  "description": "Prepare and submit individual job applications using a résumé and profile saved on your device.",
12
- "version": "0.1.1",
12
+ "version": "0.3.0",
13
13
  "category": "productivity"
14
14
  }
15
15
  ]
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 finds the apps you have and installs into each of them. To choose a target yourself:
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 <owner>/<repo>` then `claude plugin install job-apply@job-apply`
79
- - Codex: `codex plugin marketplace add <path-or-repo>`
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) => !SKIP.has(path.basename(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
- replaceDir(PLUGIN_SRC, this.pluginDir(), { ...opts, target: this.id });
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 (your profile and history in ~/Documents/Job Apply are kept)
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 Target Claude Code only
237
- --chatgpt Target ChatGPT desktop / Codex only (alias: --codex)
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
- With no target flag, every detected app is used (both if none is detected).`;
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
- function pickTargets(values) {
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
- if (chosen.length) return chosen;
250
- const detected = Object.values(TARGETS).filter((t) => t.detect());
251
- return detected.length ? detected : Object.values(TARGETS);
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
- export function main(argv = process.argv.slice(2)) {
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 targets) t.install(opts);
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(`Your local profile in ~/Documents/Job Apply was not touched.`);
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
- try {
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.1.1",
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-apply",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Prepare and submit individual job applications using a résumé and profile saved on your device.",
5
5
  "author": {
6
6
  "name": "Filipe Linemburger"
@@ -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.1.1",
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",