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.
@@ -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.0",
12
+ "version": "0.2.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
@@ -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 the plugin and keeps your profile and history
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) => !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,
@@ -233,25 +236,88 @@ Commands
233
236
  status Show where the plugin is installed and check Python
234
237
 
235
238
  Options
236
- --claude Target Claude Code only
237
- --chatgpt Target ChatGPT desktop / Codex only (alias: --codex)
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
- With no target flag, every detected app is used (both if none is detected).`;
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
- function pickTargets(values) {
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
- if (chosen.length) return chosen;
250
- const detected = Object.values(TARGETS).filter((t) => t.detect());
251
- return detected.length ? detected : Object.values(TARGETS);
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 targets) t.install(opts);
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
- for (const t of targets) t.uninstall(opts);
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
- try {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-apply-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-apply",
3
- "version": "0.1.0",
3
+ "version": "0.2.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.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",