@nevermorelove/ompp 0.2.2 → 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.
Files changed (3) hide show
  1. package/README.md +3 -5
  2. package/ompp.js +166 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -34,12 +34,10 @@ ompp pentest --dry-run show the omp command line instead of running it
34
34
 
35
35
  Before launch it prints one line to stderr, `[ompp] mode: pentest`, so you always know where you are.
36
36
 
37
- Modes are read from up to three places, and same-named modes from earlier
38
- places win:
37
+ Modes are read from two places, and same-named modes from the first win:
39
38
 
40
- 1. `OMPP_MODES_DIR`, if you set it
41
- 2. `modes/` shipped next to the script (the repo checkout or the npm package)
42
- 3. `~/.omp/ompp/modes/`, the user-level home for your own modes
39
+ 1. `OMPP_MODES_DIR`, if you set it (a repo checkout, any custom folder)
40
+ 2. `~/.omp/ompp/modes/`, the user-level home for your own modes
43
41
 
44
42
  `ompp create` always writes to `~/.omp/ompp/modes/`, so you never edit files
45
43
  inside an installed package. The first `ompp create` makes the folder for you.
package/ompp.js CHANGED
@@ -19,19 +19,86 @@
19
19
  const fs = require("fs");
20
20
  const path = require("path");
21
21
  const { spawn } = require("child_process");
22
-
23
22
  const os = require("os");
23
+ const https = require("https");
24
+ // --- update check -----------------------------------------------------------
25
+ // One registry request per run, bounded by a 5s timeout. If the registry
26
+ // has a newer semver, print an upgrade banner.
27
+ function fetchLatestVersion() {
28
+ return new Promise((resolve) => {
29
+ const req = https.get(
30
+ "https://registry.npmjs.org/@nevermorelove%2fompp",
31
+ { timeout: 5000 },
32
+ (res) => {
33
+ if (res.statusCode !== 200) {
34
+ res.resume();
35
+ return resolve(null);
36
+ }
37
+ let body = "";
38
+ res.on("data", (c) => (body += c));
39
+ res.on("end", () => {
40
+ try {
41
+ resolve(JSON.parse(body)["dist-tags"]?.latest ?? null);
42
+ } catch {
43
+ resolve(null);
44
+ }
45
+ });
46
+ },
47
+ );
48
+ req.on("timeout", () => {
49
+ req.destroy();
50
+ resolve(null);
51
+ });
52
+ req.on("error", () => resolve(null));
53
+ });
54
+ }
55
+
56
+ function isNewerVersion(current, latest) {
57
+ const parse = (v) =>
58
+ v.split(/[.-]/).slice(0, 3).map((n) => parseInt(n, 10) || 0);
59
+ const [cmaj, cmin, cpat] = parse(current);
60
+ const [lmaj, lmin, lpat] = parse(latest);
61
+ if (lmaj !== cmaj) return lmaj > cmaj;
62
+ if (lmin !== cmin) return lmin > cmin;
63
+ return lpat > cpat;
64
+ }
65
+
66
+ function printUpdateBanner(current, latest) {
67
+ const line = `Update available! ${current} → ${latest}`;
68
+ const hint = `Run "npm i -g @nevermorelove/ompp" to update`;
69
+ const width = Math.max(line.length, hint.length) + 4;
70
+ const pad = (s) => " " + s + " ".repeat(width - 2 - s.length) + " ";
71
+ console.error("┌" + "─".repeat(width) + "┐");
72
+ console.error("│" + " ".repeat(width) + "│");
73
+ console.error("│" + pad(line) + "│");
74
+ console.error("│" + pad(hint) + "│");
75
+ console.error("│" + " ".repeat(width) + "│");
76
+ console.error("└" + "─".repeat(width) + "┘");
77
+ }
78
+
79
+ async function checkForUpdate() {
80
+ if (process.env.OMPP_NO_UPDATE_CHECK === "1") return;
81
+ if (process.env.CI) return;
82
+ // Skip dev checkouts: a repo clone updates via git, not npm.
83
+ if (fs.existsSync(path.join(__dirname, ".git"))) return;
84
+
85
+ const current = require("./package.json").version;
86
+ const latest = await fetchLatestVersion();
87
+ if (latest && isNewerVersion(current, latest)) {
88
+ printUpdateBanner(current, latest);
89
+ }
90
+ }
91
+
24
92
 
25
- // Modes come from up to three places, in priority order:
26
- // 1. OMPP_MODES_DIR (explicit override)
27
- // 2. modes/ next to this script (repo checkout or npm package)
28
- // 3. ~/.omp/ompp/modes (user-level default where `ompp create` writes)
29
- // Same-named modes from a higher-priority source win.
93
+ // Modes come from two places, in priority order:
94
+ // 1. OMPP_MODES_DIR (explicit override; repo checkout, custom folder)
95
+ // 2. ~/.omp/ompp/modes (the user-level home; `ompp create` writes here)
96
+ // Same-named modes from OMPP_MODES_DIR win. The folder next to the script
97
+ // is never scanned: with a global install it sits inside node_modules, and
98
+ // user modes must not live there.
30
99
  const DEFAULT_MODES_DIR = path.join(os.homedir(), ".omp", "ompp", "modes");
31
- const BUNDLED_MODES_DIR = path.join(__dirname, "modes");
32
100
  const SOURCES = [];
33
101
  if (process.env.OMPP_MODES_DIR) SOURCES.push(process.env.OMPP_MODES_DIR);
34
- SOURCES.push(BUNDLED_MODES_DIR);
35
102
  SOURCES.push(DEFAULT_MODES_DIR);
36
103
  const RESERVED_NAMES = ["create", "list", "help", "version"];
37
104
  const CONFIG_NAMES = ["config.yml", "config.yaml"];
@@ -278,16 +345,43 @@ function createMode(name) {
278
345
  return 0;
279
346
  }
280
347
 
348
+ const CREATE_OPTION = "__create__";
349
+
350
+ function isValidModeName(name) {
351
+ return /^[a-z0-9][a-z0-9-]*$/.test(name);
352
+ }
353
+
281
354
  async function pickMode(modeNames) {
282
- const { select, isCancel, cancel } = require("@clack/prompts");
355
+ const { select, text, isCancel, cancel } = require("@clack/prompts");
356
+ const options = [
357
+ ...modeNames.map((name) => ({ value: name, label: name })),
358
+ { value: CREATE_OPTION, label: "Create a new mode +" },
359
+ ];
283
360
  const chosen = await select({
284
361
  message: "Pick a mode",
285
- options: modeNames.map((name) => ({ value: name, label: name })),
362
+ options,
286
363
  });
287
364
  if (isCancel(chosen)) {
288
365
  cancel("Cancelled");
289
366
  process.exit(130);
290
367
  }
368
+ if (chosen === CREATE_OPTION) {
369
+ const name = await text({
370
+ message: "New mode name",
371
+ validate: (v) => {
372
+ if (!v || !isValidModeName(v)) {
373
+ return "Lowercase letters, digits, and dashes only";
374
+ }
375
+ if (RESERVED_NAMES.includes(v)) return `"${v}" is reserved`;
376
+ return undefined;
377
+ },
378
+ });
379
+ if (isCancel(name)) {
380
+ cancel("Cancelled");
381
+ process.exit(130);
382
+ }
383
+ return { create: name };
384
+ }
291
385
  return chosen;
292
386
  }
293
387
 
@@ -315,7 +409,39 @@ function pickModePiped(modeNames) {
315
409
 
316
410
 
317
411
 
412
+ // Spawn omp in the given mode.
413
+ function launchMode(mode, userArgs, dryRun) {
414
+ const modeDir = modeDirOf(mode.name, mode.source);
415
+ const args = buildArgv(modeDir, userArgs);
416
+ const { bin, shell } = resolveBin();
417
+
418
+ if (dryRun) {
419
+ console.error(`[ompp] mode: ${mode.name}`);
420
+ console.error(`[ompp] omp: ${bin}${shell ? " (shell)" : ""}`);
421
+ console.error(`[ompp] argv: ${JSON.stringify(args)}`);
422
+ return 0;
423
+ }
424
+
425
+ console.error(`[ompp] mode: ${mode.name}`);
426
+ const child = shell
427
+ ? spawn([winQuote(bin), ...args.map(winQuote)].join(" "), {
428
+ stdio: "inherit",
429
+ shell: true,
430
+ })
431
+ : spawn(bin, args, { stdio: "inherit" });
432
+ child.on("error", (err) => {
433
+ console.error(`[ompp] failed to start omp: ${err.message}`);
434
+ process.exitCode = 1;
435
+ });
436
+ child.on("exit", (code, signal) => {
437
+ process.exitCode = code ?? (signal ? 1 : 0);
438
+ });
439
+ return 0;
440
+ }
441
+
318
442
  async function main() {
443
+ await checkForUpdate();
444
+
319
445
  const argv = process.argv.slice(2);
320
446
 
321
447
  if (argv[0] === "create") {
@@ -358,13 +484,29 @@ async function main() {
358
484
  `[ompp] skipped folders without recognized files: ${skipped.join(", ")}`,
359
485
  );
360
486
  }
361
- if (!modes.length) {
487
+ if (!modes.length && !process.stdin.isTTY) {
362
488
  console.error(
363
489
  `You have no modes yet. Create one with "ompp create <name>".`,
364
490
  );
365
491
  for (const s of SOURCES) console.error(`[ompp] looked in: ${s}`);
366
492
  return 1;
367
493
  }
494
+ if (!modes.length) {
495
+ // TTY with zero modes: jump straight into creating the first one.
496
+ try {
497
+ const picked = await pickMode([]);
498
+ if (picked && typeof picked === "object" && picked.create) {
499
+ const created = createMode(picked.create);
500
+ if (created !== 0) return created;
501
+ const mode2 = { name: picked.create, source: DEFAULT_MODES_DIR };
502
+ return launchMode(mode2, userArgs, dryRun);
503
+ }
504
+ return 1;
505
+ } catch (err) {
506
+ console.error(`[ompp] ${err.message}`);
507
+ return 1;
508
+ }
509
+ }
368
510
 
369
511
  const modeNames = modes.map((m) => m.name);
370
512
  let mode = null;
@@ -380,44 +522,28 @@ async function main() {
380
522
  console.error(`[ompp] available: ${modeNames.join(", ")}`);
381
523
  return 1;
382
524
  }
383
- } else {
525
+ }
526
+
527
+ if (!mode) {
384
528
  try {
385
- const name = process.stdin.isTTY
529
+ const picked = process.stdin.isTTY
386
530
  ? await pickMode(modeNames)
387
531
  : await pickModePiped(modeNames);
388
- mode = modes.find((m) => m.name === name);
532
+ if (picked && typeof picked === "object" && picked.create) {
533
+ const created = createMode(picked.create);
534
+ if (created !== 0) return created;
535
+ // Launch the freshly created mode right away.
536
+ mode = { name: picked.create, source: DEFAULT_MODES_DIR };
537
+ } else {
538
+ mode = modes.find((m) => m.name === picked);
539
+ }
389
540
  } catch (err) {
390
541
  console.error(`[ompp] ${err.message}`);
391
542
  return 1;
392
543
  }
393
544
  }
394
545
 
395
- const modeDir = modeDirOf(mode.name, mode.source);
396
- const args = buildArgv(modeDir, userArgs);
397
- const { bin, shell } = resolveBin();
398
-
399
- if (dryRun) {
400
- console.error(`[ompp] mode: ${mode.name}`);
401
- console.error(`[ompp] omp: ${bin}${shell ? " (shell)" : ""}`);
402
- console.error(`[ompp] argv: ${JSON.stringify(args)}`);
403
- return 0;
404
- }
405
-
406
- console.error(`[ompp] mode: ${mode.name}`);
407
- const child = shell
408
- ? spawn([winQuote(bin), ...args.map(winQuote)].join(" "), {
409
- stdio: "inherit",
410
- shell: true,
411
- })
412
- : spawn(bin, args, { stdio: "inherit" });
413
- child.on("error", (err) => {
414
- console.error(`[ompp] failed to start omp: ${err.message}`);
415
- process.exitCode = 1;
416
- });
417
- child.on("exit", (code, signal) => {
418
- process.exitCode = code ?? (signal ? 1 : 0);
419
- });
420
- return 0;
546
+ return launchMode(mode, userArgs, dryRun);
421
547
  }
422
548
 
423
549
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nevermorelove/ompp",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Mode presets for the omp coding agent: one folder per mode, zero config code.",
5
5
  "repository": {
6
6
  "type": "git",