@nevermorelove/ompp 0.2.3 → 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 (2) hide show
  1. package/ompp.js +160 -33
  2. package/package.json +1 -1
package/ompp.js CHANGED
@@ -19,8 +19,76 @@
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
93
  // Modes come from two places, in priority order:
26
94
  // 1. OMPP_MODES_DIR (explicit override; repo checkout, custom folder)
@@ -277,16 +345,43 @@ function createMode(name) {
277
345
  return 0;
278
346
  }
279
347
 
348
+ const CREATE_OPTION = "__create__";
349
+
350
+ function isValidModeName(name) {
351
+ return /^[a-z0-9][a-z0-9-]*$/.test(name);
352
+ }
353
+
280
354
  async function pickMode(modeNames) {
281
- 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
+ ];
282
360
  const chosen = await select({
283
361
  message: "Pick a mode",
284
- options: modeNames.map((name) => ({ value: name, label: name })),
362
+ options,
285
363
  });
286
364
  if (isCancel(chosen)) {
287
365
  cancel("Cancelled");
288
366
  process.exit(130);
289
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
+ }
290
385
  return chosen;
291
386
  }
292
387
 
@@ -314,7 +409,39 @@ function pickModePiped(modeNames) {
314
409
 
315
410
 
316
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
+
317
442
  async function main() {
443
+ await checkForUpdate();
444
+
318
445
  const argv = process.argv.slice(2);
319
446
 
320
447
  if (argv[0] === "create") {
@@ -357,13 +484,29 @@ async function main() {
357
484
  `[ompp] skipped folders without recognized files: ${skipped.join(", ")}`,
358
485
  );
359
486
  }
360
- if (!modes.length) {
487
+ if (!modes.length && !process.stdin.isTTY) {
361
488
  console.error(
362
489
  `You have no modes yet. Create one with "ompp create <name>".`,
363
490
  );
364
491
  for (const s of SOURCES) console.error(`[ompp] looked in: ${s}`);
365
492
  return 1;
366
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
+ }
367
510
 
368
511
  const modeNames = modes.map((m) => m.name);
369
512
  let mode = null;
@@ -379,44 +522,28 @@ async function main() {
379
522
  console.error(`[ompp] available: ${modeNames.join(", ")}`);
380
523
  return 1;
381
524
  }
382
- } else {
525
+ }
526
+
527
+ if (!mode) {
383
528
  try {
384
- const name = process.stdin.isTTY
529
+ const picked = process.stdin.isTTY
385
530
  ? await pickMode(modeNames)
386
531
  : await pickModePiped(modeNames);
387
- 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
+ }
388
540
  } catch (err) {
389
541
  console.error(`[ompp] ${err.message}`);
390
542
  return 1;
391
543
  }
392
544
  }
393
545
 
394
- const modeDir = modeDirOf(mode.name, mode.source);
395
- const args = buildArgv(modeDir, userArgs);
396
- const { bin, shell } = resolveBin();
397
-
398
- if (dryRun) {
399
- console.error(`[ompp] mode: ${mode.name}`);
400
- console.error(`[ompp] omp: ${bin}${shell ? " (shell)" : ""}`);
401
- console.error(`[ompp] argv: ${JSON.stringify(args)}`);
402
- return 0;
403
- }
404
-
405
- console.error(`[ompp] mode: ${mode.name}`);
406
- const child = shell
407
- ? spawn([winQuote(bin), ...args.map(winQuote)].join(" "), {
408
- stdio: "inherit",
409
- shell: true,
410
- })
411
- : spawn(bin, args, { stdio: "inherit" });
412
- child.on("error", (err) => {
413
- console.error(`[ompp] failed to start omp: ${err.message}`);
414
- process.exitCode = 1;
415
- });
416
- child.on("exit", (code, signal) => {
417
- process.exitCode = code ?? (signal ? 1 : 0);
418
- });
419
- return 0;
546
+ return launchMode(mode, userArgs, dryRun);
420
547
  }
421
548
 
422
549
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nevermorelove/ompp",
3
- "version": "0.2.3",
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",