@nevermorelove/ompp 0.2.3 → 0.3.1

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 +157 -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,26 @@ 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
+ return createMode(picked.create);
500
+ }
501
+ return 1;
502
+ } catch (err) {
503
+ console.error(`[ompp] ${err.message}`);
504
+ return 1;
505
+ }
506
+ }
367
507
 
368
508
  const modeNames = modes.map((m) => m.name);
369
509
  let mode = null;
@@ -379,44 +519,28 @@ async function main() {
379
519
  console.error(`[ompp] available: ${modeNames.join(", ")}`);
380
520
  return 1;
381
521
  }
382
- } else {
522
+ }
523
+
524
+ if (!mode) {
383
525
  try {
384
- const name = process.stdin.isTTY
526
+ const picked = process.stdin.isTTY
385
527
  ? await pickMode(modeNames)
386
528
  : await pickModePiped(modeNames);
387
- mode = modes.find((m) => m.name === name);
529
+ if (picked && typeof picked === "object" && picked.create) {
530
+ // Scaffold and stop: the mode is all placeholders, launching it
531
+ // now would run an unconfigured mode. The user fills it in and
532
+ // launches when ready.
533
+ return createMode(picked.create);
534
+ } else {
535
+ mode = modes.find((m) => m.name === picked);
536
+ }
388
537
  } catch (err) {
389
538
  console.error(`[ompp] ${err.message}`);
390
539
  return 1;
391
540
  }
392
541
  }
393
542
 
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;
543
+ return launchMode(mode, userArgs, dryRun);
420
544
  }
421
545
 
422
546
  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.1",
4
4
  "description": "Mode presets for the omp coding agent: one folder per mode, zero config code.",
5
5
  "repository": {
6
6
  "type": "git",