roger-roger 0.1.2 → 0.1.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roger-roger",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Bring yourself back to the conversation: your coding agents reach you by Slack, a sound or a spoken line when they finish, get stuck, or need a decision, and you answer from your phone.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,7 +6,7 @@
6
6
 
7
7
  id = "roger-roger"
8
8
  name = "roger-roger"
9
- version = "0.1.2"
9
+ version = "0.1.3"
10
10
  min_herdr_version = "0.9.0"
11
11
  description = "Slack, a sound or a spoken line from the agents in your panes, and questions you answer from your phone"
12
12
  platforms = ["linux", "macos", "windows"]
@@ -180,7 +180,7 @@ export function helpText(command = "", { columns = process.stdout.columns || 100
180
180
  for (const line of wrap(TAGLINE, cols - 2)) out.push(` ${c.dim(line)}`);
181
181
  out.push("");
182
182
  out.push(` ${c.bold("Usage")}`);
183
- out.push(...table([["roger-roger <command> [flags]", "every command prints one JSON object"], ["roger-roger help <command>", "that command's flags"]], {
183
+ out.push(...table([["roger-roger <command> [flags]", "every command prints one JSON object"], ["roger-roger help <command>", "that command's flags"], ["roger-roger --version", "which copy this is"]], {
184
184
  indent: 4, columns: cols, c, maxLeft: 30, left: (l) => c.cyan("roger-roger") + " " + c.dim(l.slice("roger-roger ".length)),
185
185
  }));
186
186
  const sections = [...new Set(COMMANDS.map((x) => x.section))];
@@ -115,23 +115,37 @@ export function globalCopy(env = process.env) {
115
115
  const root = (r.stdout ?? "").trim();
116
116
  if (r.status !== 0 || !root) return null;
117
117
  const pkg = path.join(root, "roger-roger");
118
- return { root, dir: path.join(pkg, "skills", "roger-roger"), present: fs.existsSync(path.join(pkg, "package.json")) };
118
+ let version = "";
119
+ try {
120
+ version = JSON.parse(fs.readFileSync(path.join(pkg, "package.json"), "utf8")).version ?? "";
121
+ } catch {}
122
+ return { root, dir: path.join(pkg, "skills", "roger-roger"), present: Boolean(version), version };
119
123
  } catch {
120
124
  return null;
121
125
  }
122
126
  }
123
127
 
128
+ /** a < b for dotted versions; anything unparseable counts as older. */
129
+ export function olderThan(a, b) {
130
+ const parse = (v) => String(v ?? "").split(".").map((n) => parseInt(n, 10) || 0);
131
+ const [x, y] = [parse(a), parse(b)];
132
+ for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) < (y[i] ?? 0);
133
+ return false;
134
+ }
135
+
124
136
  /**
125
137
  * Which copy of the skill everything should point at, and whether to offer a global install:
126
138
  * the copy this runs from, unless that is a passing one (npx's cache, say) and there is no global
127
- * copy yet. A global copy that exists beside a checkout or a skills.sh copy is left alone: whoever
139
+ * copy yet, or the global copy is older than this one (`npx roger-roger@latest install` after an
140
+ * update). A global copy that is as new as a checkout or a skills.sh copy is left alone: whoever
128
141
  * runs this copy meant this copy.
129
142
  */
130
- export function pickTarget(running, global) {
143
+ export function pickTarget(running, global, runningVersion = VERSION) {
131
144
  if (!global) return { target: running, offer: false, reason: "npm was not found" };
132
145
  if (norm(running) === norm(global.dir)) return { target: running, offer: false, reason: "global" };
133
- if (global.present) return { target: running, offer: false, reason: "a global copy exists, but this one was run" };
134
- return { target: running, offer: true, reason: "no global copy yet" };
146
+ if (!global.present) return { target: running, offer: true, reason: "no global copy yet" };
147
+ if (runningVersion && olderThan(global.version, runningVersion)) return { target: running, offer: true, reason: "update", from: global.version, to: runningVersion };
148
+ return { target: running, offer: false, reason: "a global copy exists, but this one was run" };
135
149
  }
136
150
 
137
151
  /** `npm install -g roger-roger@latest`. Throws with npm's last words when it fails. */
@@ -422,9 +436,11 @@ export async function runInstall(args, { script, hooks, input = process.stdin, o
422
436
  if (pick.reason === "global") {
423
437
  ui.done("The roger-roger command is on your PATH", global.dir);
424
438
  } else if (pick.offer) {
425
- const yes = await ui.confirm({ message: "Put the roger-roger command on your PATH?", hint: "npm install -g roger-roger, so you and your agents can run it from anywhere" });
439
+ const yes = await ui.confirm(pick.reason === "update"
440
+ ? { message: `Update the roger-roger command from ${pick.from} to ${pick.to}?`, hint: "npm install -g roger-roger@latest; the old one would keep answering" }
441
+ : { message: "Put the roger-roger command on your PATH?", hint: "npm install -g roger-roger, so you and your agents can run it from anywhere" });
426
442
  if (yes) {
427
- const done = await attempt(ui, "Installing globally", () => ui.run("Installing roger-roger with npm", () => installGlobally(env), { done: () => ({ text: "The roger-roger command is on your PATH", detail: global.dir }) }), { skipLabel: "Carry on with this copy" });
443
+ const done = await attempt(ui, pick.reason === "update" ? "Updating" : "Installing globally", () => ui.run(`${pick.reason === "update" ? "Updating" : "Installing"} roger-roger with npm`, () => installGlobally(env), { done: () => ({ text: `The roger-roger command is on your PATH${pick.to ? `, at ${pick.to}` : ""}`, detail: global.dir }) }), { skipLabel: "Carry on with this copy" });
428
444
  if (done !== null && fs.existsSync(path.join(global.dir, "scripts", "roger-roger.mjs"))) {
429
445
  target = global.dir;
430
446
  hookScript = path.join(global.dir, "scripts", "roger-roger.mjs");
@@ -10,7 +10,7 @@
10
10
  import fs from "node:fs";
11
11
  import path from "node:path";
12
12
  import {
13
- DEFAULTS, DUCK_MODES, KINDS, METHODS, ON_EXPIRE, SPEECH_MODES, WHEN_MODES,
13
+ DEFAULTS, DUCK_MODES, KINDS, METHODS, ON_EXPIRE, SPEECH_MODES, VERSION, WHEN_MODES,
14
14
  applySetup, rogerRogerHome, configPath, listArg, listSounds, loadConfig, migrateLegacyHome, parseArgs,
15
15
  readSlackCredentials, saveConfig, slackAppToken, slackToken,
16
16
  } from "./lib.mjs";
@@ -324,6 +324,10 @@ const AGENTS = {
324
324
 
325
325
  /** `help`, `help <command>`, `--help`, or a command that doesn't exist: the overview, or one command's page. */
326
326
  function help(command, args) {
327
+ if (["--version", "-v", "-V", "version"].includes(command)) {
328
+ process.stdout.write(`${VERSION || "unversioned copy"}\n`);
329
+ return;
330
+ }
327
331
  const wanted = command === "help" ? args._[0] : "";
328
332
  const unknown = command && !["help", "--help", "-h", undefined].includes(command);
329
333
  if (unknown) process.stderr.write(`roger-roger: unknown command "${command}"\n`);