@retasc/cli 1.27.0 → 1.29.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ release commits and the issues they reference.
6
6
 
7
7
  Dates are the npm publish date. Each entry names the RTSC issue behind it.
8
8
 
9
+ ## 1.29.0 (2026-08-19)
10
+
11
+ - **RTSC-672** — every command we hand out now says `npx @retasc/cli@latest`. npx caches
12
+ by spec, so a bare `npx @retasc/cli` can keep serving whatever version you first ran —
13
+ which meant the people most likely to re-run a command, the ones who hit a bug and were
14
+ told it was fixed, were exactly the ones liable to be served the broken build again.
15
+ `@latest` is a tag, not a pin: it re-resolves every time and can never go stale, where a
16
+ version number written into docs absolutely can.
17
+ - **RTSC-672** — a first-run failure names the build that produced it, so "it still
18
+ doesn't work" and "you're running last week's CLI" stop looking identical.
19
+
20
+ ## 1.28.0 (2026-08-19)
21
+
22
+ - **RTSC-676** — signing in opens your browser. It used to print a URL and an
23
+ eight-character code and leave you to switch windows, navigate, and retype it. Now the
24
+ page opens for you, pre-filled when the provider supplies RFC 8628's complete URL. The
25
+ URL and code are still printed first and always: there is no browser inside a container
26
+ or over SSH, which is where `bind` runs most, so the browser is a convenience layered
27
+ on top and never the only way through. A missing `xdg-open` changes nothing.
28
+ `RETASC_NO_BROWSER=1` turns it off.
29
+ - **RTSC-676** — the finish screen's closing lines are flush left, and it ends with a
30
+ blank line instead of welding the shell prompt to the last thing you read.
31
+
9
32
  ## 1.27.0 (2026-08-19)
10
33
 
11
34
  - **RTSC-673** — `bind` ends by telling you what to do, not by dumping config. It used to
package/dist/auth.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { ConvexHttpClient } from "convex/browser";
2
3
  import { makeFunctionReference } from "convex/server";
3
4
  import { loadConfig, patchConfig } from "./config.js";
@@ -28,11 +29,61 @@ async function postForm(url, body) {
28
29
  return res.json();
29
30
  }
30
31
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
31
- /** Print the two lines a human has to act on, identically for both doors. */
32
- function announceCode(where, url, code) {
32
+ /**
33
+ * Open a URL in the machine's browser, best effort (RTSC-676).
34
+ *
35
+ * BEST EFFORT is the whole contract. There is no browser inside a container or over
36
+ * SSH, which is where `bind` most often runs, so this must never be the only way
37
+ * through and must never turn a failure of its own into a failure of sign-in. It is
38
+ * spawned detached and every outcome is swallowed: the printed URL and code below are
39
+ * the real path, and this only saves the person who does have a browser a trip through
40
+ * their address bar.
41
+ *
42
+ * `xdg-open` on Linux is frequently absent even under a desktop; that is a miss, not an
43
+ * error. `start` needs a shell on Windows, and its first quoted argument is the window
44
+ * TITLE, hence the empty string.
45
+ */
46
+ function openBrowser(url) {
47
+ if (process.env.RETASC_NO_BROWSER)
48
+ return;
49
+ const [cmd, args] = process.platform === "darwin"
50
+ ? ["open", [url]]
51
+ : process.platform === "win32"
52
+ ? ["start", ['""', url]]
53
+ : ["xdg-open", [url]];
54
+ try {
55
+ const child = spawn(cmd, args, {
56
+ stdio: "ignore",
57
+ detached: true,
58
+ shell: process.platform === "win32",
59
+ });
60
+ // Without this the CLI would wait on a browser that outlives it by hours.
61
+ child.unref();
62
+ // A missing `xdg-open` surfaces here rather than as a throw.
63
+ child.on("error", () => { });
64
+ }
65
+ catch {
66
+ /* no browser, no shell, no matter — the URL is printed below */
67
+ }
68
+ }
69
+ /**
70
+ * Print the two lines a human has to act on, identically for both doors, and open the
71
+ * browser for them if this machine has one.
72
+ *
73
+ * The lines are printed FIRST and unconditionally. Someone reading a terminal in a
74
+ * container has to see the URL whatever happened, and an auto-open that silently
75
+ * failed must leave the screen exactly as useful as it was before (RTSC-676).
76
+ *
77
+ * `complete` is RFC 8628's pre-filled URL when the provider sent one. It is opened in
78
+ * preference to the plain one, but never PRINTED in its place: the code stays on its own
79
+ * line so it can be read aloud, retyped on a phone, or pasted into a browser on another
80
+ * machine — none of which a link helps with.
81
+ */
82
+ function announceCode(where, url, code, complete) {
33
83
  console.log(`\n Open: ${url}`);
34
84
  console.log(` Enter code: ${code}\n`);
35
85
  console.log(` Waiting for ${where} authorization…`);
86
+ openBrowser(complete || url);
36
87
  }
37
88
  /** Run GitHub's device flow (talking to GitHub directly) and return its access token. */
38
89
  async function githubDeviceToken() {
@@ -46,7 +97,7 @@ async function githubDeviceToken() {
46
97
  if (!start.device_code) {
47
98
  throw new Error(`GitHub device-flow start failed: ${JSON.stringify(start)}`);
48
99
  }
49
- announceCode("GitHub", start.verification_uri, start.user_code);
100
+ announceCode("GitHub", start.verification_uri, start.user_code, start.verification_uri_complete);
50
101
  // Poll GitHub for the access token.
51
102
  let intervalMs = (start.interval || 5) * 1000;
52
103
  const deadline = Date.now() + start.expires_in * 1000;
@@ -80,7 +131,7 @@ async function githubDeviceToken() {
80
131
  */
81
132
  async function googleDeviceToken(convex) {
82
133
  const start = (await convex.action(googleDeviceStart, {}));
83
- announceCode("Google", start.verificationUrl, start.userCode);
134
+ announceCode("Google", start.verificationUrl, start.userCode, start.verificationUrlComplete);
84
135
  let intervalMs = (start.intervalSeconds || 5) * 1000;
85
136
  const deadline = Date.now() + start.expiresInSeconds * 1000;
86
137
  while (Date.now() < deadline) {
@@ -5,7 +5,7 @@ import { loadConfig, patchConfig } from "../config.js";
5
5
  import { installMarker, printMarkerBlock } from "./mcp.js";
6
6
  import { readLocalBinding, resolveBinding } from "../lib/binding.js";
7
7
  import { getBinding, setBinding, newWorkspaceId } from "../lib/keystore.js";
8
- import { resolveLauncher, launcherNote, runsOk, selfCommand } from "../lib/launcher.js";
8
+ import { resolveLauncher, launcherNote, runsOk, selfCommand, versionStamp } from "../lib/launcher.js";
9
9
  import { ask, confirm, isInteractive } from "../lib/prompt.js";
10
10
  import { clean } from "../lib/text.js";
11
11
  import { card, DOT } from "../lib/card.js";
@@ -446,7 +446,7 @@ export async function completeWorkspaceSetup(args) {
446
446
  // AFTER the confirmation, and after everything that can throw, so it is only ever
447
447
  // printed by a run that actually finished. Nothing below it can fail.
448
448
  if (isInteractive())
449
- console.log(`\n${NEXT_STEP}\n\n${whatNow(pfx, emptyProject)}`);
449
+ console.log(`\n${NEXT_STEP}\n\n${whatNow(pfx, emptyProject)}\n`);
450
450
  }
451
451
  /**
452
452
  * The two sentences under `NEXT_STEP`, chosen by whether there is anything to pull yet
@@ -477,12 +477,15 @@ export function projectIsEmpty(p) {
477
477
  return p.counter === 0;
478
478
  }
479
479
  export function whatNow(prefix, empty) {
480
+ // FLUSH LEFT (RTSC-676). Indented, these read as subordinate to the line above them
481
+ // when they are in fact continuing it — the eye takes the block for a nested detail
482
+ // rather than the instruction it is.
480
483
  return empty
481
- ? ` ${prefix} is empty, so start by talking with the agent. Tell it what you're\n` +
482
- ` building and it will file the work as it goes.\n\n` +
483
- ` Once there's a backlog, saying "next issue" is how you pull from it.`
484
- : ` There's already work in ${prefix}. Say "next issue" and your agent takes\n` +
485
- ` the top unblocked one and starts.`;
484
+ ? `${prefix} is empty, so start by talking with the agent. Tell it what you're\n` +
485
+ `building and it will file the work as it goes.\n\n` +
486
+ `Once there's a backlog, saying "next issue" is how you pull from it.`
487
+ : `There's already work in ${prefix}. Say "next issue" and your agent takes\n` +
488
+ `the top unblocked one and starts.`;
486
489
  }
487
490
  export async function setupFromToken(token, opts, deps = {}) {
488
491
  const cfg = loadConfig();
@@ -560,7 +563,11 @@ export async function ensureSignedIn() {
560
563
  if (loadConfig().token)
561
564
  return;
562
565
  if (!isInteractive()) {
563
- throw new Error(`Not signed in. Run \`${selfCommand(VERSION)} login\` first (no TTY here for the device flow).`);
566
+ // RTSC-672 the stamp rides in the HINT, not the message. `formatError` keeps only
567
+ // the first line of a message (deliberately, to strip stack noise), so a second line
568
+ // here would be silently dropped — which is how a diagnostic aimed at silent failures
569
+ // would itself fail silently.
570
+ cliError("UNAUTHENTICATED", `Not signed in. Run \`${selfCommand(VERSION)} login\` first (no TTY here for the device flow).`, versionStamp(VERSION).trim());
564
571
  }
565
572
  // RTSC-508: don't name a provider here. `deviceLogin` asks which door, and
566
573
  // announcing "GitHub" before the question would be wrong for the invited
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { VERSION } from "./version.js";
4
- import { selfCommand } from "./lib/launcher.js";
4
+ import { selfCommand, versionStamp } from "./lib/launcher.js";
5
5
  import { loadConfig, patchConfig, saveConfig, configPath, isLoggedIn } from "./config.js";
6
6
  import { installMcp, normalizeScope } from "./commands/mcp.js";
7
7
  import { installGate, resolveGatePrefix } from "./commands/gate.js";
@@ -27,9 +27,13 @@ program
27
27
  function requireLogin() {
28
28
  if (!isLoggedIn()) {
29
29
  // RTSC-671 — `selfCommand` renders the form they actually used. The docs send a
30
- // new owner through `npx @retasc/cli`, which never puts `retasc` on PATH, so naming
30
+ // new owner through `npx @retasc/cli@latest`, which never puts `retasc` on PATH, so naming
31
31
  // it here answers a failure with a second `command not found`.
32
32
  console.error(`Not signed in. Run \`${selfCommand(VERSION)} login\` first.`);
33
+ // RTSC-672 — say which build spoke. A stale CLI and a broken one produce the same
34
+ // sentence otherwise, and the people most likely to re-run this are the ones who
35
+ // were just told a fix shipped.
36
+ console.error(versionStamp(VERSION));
33
37
  process.exit(1);
34
38
  }
35
39
  }
@@ -403,7 +407,7 @@ members
403
407
  // them in, binds the folder and wires their agent, so this must not tell them to run
404
408
  // `retasc login` first — nor name a `retasc` binary they don't have yet.
405
409
  console.log(` Share it. In the folder their agent works in, they run:`);
406
- console.log(` npx @retasc/cli join ${res.code}`);
410
+ console.log(` npx @retasc/cli@latest join ${res.code}`);
407
411
  }
408
412
  catch (e) {
409
413
  fail(e);
@@ -96,6 +96,25 @@ export function portableLauncher(r, version) {
96
96
  export function selfCommand(version, argv1 = process.argv[1] ?? "") {
97
97
  return /[\\/]_npx[\\/]/.test(argv1) ? `npx -y ${PKG}@${version}` : "retasc";
98
98
  }
99
+ /**
100
+ * A one-line stamp of which build just spoke (RTSC-672).
101
+ *
102
+ * A failure report is only diagnosable if it says what produced it. After 1.26.0 fixed
103
+ * sign-in, a container kept printing the 1.25.0 message and the only way to learn why was
104
+ * a round trip asking someone to run `--version` — because nothing in the output said.
105
+ * Worse, the population most likely to re-run a command is the population that hit a bug
106
+ * and was told it is fixed, which is exactly the population liable to be holding a stale
107
+ * build. "It still doesn't work" and "you are running last week's CLI" look identical
108
+ * without this.
109
+ *
110
+ * The EXACT version, not `@latest`: this names the code that emitted the message, which
111
+ * is a different question from how to fetch the current one. `npx` in the prefix when
112
+ * that is how they started, so the line doubles as a runnable command.
113
+ */
114
+ export function versionStamp(version, argv1 = process.argv[1] ?? "") {
115
+ const how = /[\\/]_npx[\\/]/.test(argv1) ? `npx ${PKG}@${version}` : `${PKG} ${version}`;
116
+ return ` (${how})`;
117
+ }
99
118
  /** Install (or upgrade to) an exact version globally. Returns null on success. */
100
119
  function installGlobal(version) {
101
120
  // A cold global install takes seconds with no output of its own. Silence here reads as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.27.0",
3
+ "version": "1.29.0",
4
4
  "description": "Retasc CLI \u2014 the issue tracker AI agents pull work from. Sign in with GitHub or Google, create projects, mint agent API keys, and wire your agent to the Retasc MCP server in one command.",
5
5
  "type": "module",
6
6
  "bin": {