@retasc/cli 1.13.1 → 1.14.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,39 @@ 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.14.0 (2026-08-02)
10
+
11
+ - **RTSC-523** — setup asks before installing anything on your machine.
12
+
13
+ `retasc bind` and `retasc join` are one command you paste and answer, and somewhere in the
14
+ middle they ran `npm install -g @retasc/cli`. It said so, but it never asked. That is the
15
+ only step in a folder-scoped command that changes the machine rather than the folder, and
16
+ a shared npm prefix is exactly what plenty of developers keep clean.
17
+
18
+ ```
19
+ Install the retasc command on this machine?
20
+
21
+ 1) Yes, globally A `retasc` command you can use anywhere.
22
+ Best if you'll use Retasc in more than one project.
23
+ 2) No, run it on demand Nothing is installed. Your agent fetches it when it
24
+ starts, which adds a couple of seconds and needs a
25
+ network connection.
26
+ ```
27
+
28
+ Both answers leave the folder bound identically. Option 2 is not a per-project install:
29
+ nothing is written to the folder, so it needs no `package.json` and cannot fail for want
30
+ of an npm project. Your answer is remembered, because it is a question about the machine
31
+ and the machine has not changed by the time you bind a second folder.
32
+
33
+ You are only asked when it would actually happen. If `retasc` already works, there is
34
+ nothing to decide and nothing is said.
35
+
36
+ **Without a TTY it installs, exactly as before.** That is deliberate rather than a
37
+ leftover: when an agent runs setup on someone's behalf, that person never types `retasc`,
38
+ but their agent starts the MCP server every session, and the on-demand route would cost
39
+ them seconds and a network dependency every single time. `--no-install` on `bind` and
40
+ `join` declines without a TTY, for a developer whose own agent is doing the setup.
41
+
9
42
  ## 1.13.1 (2026-08-02)
10
43
 
11
44
  - **RTSC-522** — the changelog is public, and five releases that were never written down now
@@ -1,10 +1,10 @@
1
1
  import { api, cliError } from "../api.js";
2
2
  import { deviceLogin } from "../auth.js";
3
- import { loadConfig } from "../config.js";
3
+ import { loadConfig, patchConfig } from "../config.js";
4
4
  import { installMarker } from "./mcp.js";
5
5
  import { readLocalBinding, resolveBinding } from "../lib/binding.js";
6
6
  import { getBinding, setBinding, newWorkspaceId } from "../lib/keystore.js";
7
- import { resolveLauncher, launcherNote, selfCommand } from "../lib/launcher.js";
7
+ import { resolveLauncher, launcherNote, runsOk, selfCommand } from "../lib/launcher.js";
8
8
  import { ask, confirm, isInteractive } from "../lib/prompt.js";
9
9
  import { clean } from "../lib/text.js";
10
10
  import { VERSION } from "../version.js";
@@ -151,6 +151,63 @@ export async function rebindGuard(args) {
151
151
  }
152
152
  return { proceed: false, existing };
153
153
  }
154
+ /**
155
+ * May we install `retasc` on this machine? (RTSC-523)
156
+ *
157
+ * Asked only when it would actually happen. `runsOk` is the same probe
158
+ * `resolveLauncher` opens with, so if `retasc` already works there is nothing to decide and
159
+ * a question would have exactly one possible outcome.
160
+ *
161
+ * A DEFAULT is right here, unlike the sign-in door (RTSC-508), which deliberately has none.
162
+ * A wrong answer costs a couple of seconds per agent start, not a second identity that
163
+ * cannot be merged back. The two questions look alike and are not.
164
+ *
165
+ * NO TTY ⇒ install, silently, exactly as before. That is not laziness about the default: it
166
+ * is the right answer for the person it affects most. The non-technical member of RTSC-497
167
+ * never opens a terminal — her AGENT runs this (RTSC-495/496), with no TTY. She will never
168
+ * type `retasc` herself, but her agent spawns the MCP server on every start, and the npx
169
+ * route would cost her seconds and a network dependency every single time, forever.
170
+ * `--no-install` exists for the developer who wants to decline without a TTY.
171
+ */
172
+ export async function chooseInstall(
173
+ /** `--no-install` ⇒ false. Undefined means "not stated". */
174
+ flag, deps = {}) {
175
+ if (flag === false)
176
+ return false;
177
+ const onPath = deps.onPath ?? (() => runsOk("retasc") !== null);
178
+ // Nothing to install, so nothing to ask.
179
+ if (onPath())
180
+ return true;
181
+ const remembered = (deps.remembered ?? (() => loadConfig().globalInstall))();
182
+ if (remembered !== undefined)
183
+ return remembered;
184
+ const interactive = deps.interactive ?? isInteractive;
185
+ if (!interactive())
186
+ return true;
187
+ console.log("\nInstall the retasc command on this machine?");
188
+ console.log(" 1) Yes, globally A `retasc` command you can use anywhere.");
189
+ console.log(" Best if you'll use Retasc in more than one project.");
190
+ console.log(" 2) No, run it on demand Nothing is installed. Your agent fetches it when it");
191
+ console.log(" starts, which adds a couple of seconds and needs a");
192
+ console.log(" network connection.");
193
+ const askFn = deps.askFn ?? ask;
194
+ for (let attempt = 0; attempt < 3; attempt++) {
195
+ const a = await askFn("Choose a number [1]: ");
196
+ // Either answer leaves this folder bound identically — only the machine differs — so
197
+ // an empty answer taking the default is safe here in a way it is not for the door.
198
+ if (a === "" || a === "1")
199
+ return remember(true);
200
+ if (a === "2")
201
+ return remember(false);
202
+ console.log("Please enter 1 or 2.");
203
+ }
204
+ throw new Error("no valid choice — aborting");
205
+ }
206
+ /** Record the answer, so binding a second folder does not re-ask about the same machine. */
207
+ function remember(globalInstall) {
208
+ patchConfig({ globalInstall });
209
+ return globalInstall;
210
+ }
154
211
  /**
155
212
  * Everything from "which project" to a working folder: pick the project, make `retasc`
156
213
  * durable, mint a key for that (org, project), write the binding, and wire the marker.
@@ -237,7 +294,15 @@ export async function completeWorkspaceSetup(args) {
237
294
  // name one proved to run on this machine. Resolved (and announced) here so the install
238
295
  // can't land after a key exists, and so every later message knows what to tell the user
239
296
  // to type.
240
- const launcher = resolveLauncher({ version: VERSION });
297
+ //
298
+ // RTSC-523 — and ASK first, because this is the one step in a folder-scoped command that
299
+ // changes the MACHINE. It used to just run `npm install -g`. The project already holds
300
+ // the opposite position everywhere else: `resolveLauncher` leaves a working `retasc`
301
+ // alone ("no surprise installs for someone who already manages their own"), and RTSC-520
302
+ // decided an update must ask. The first command a new developer runs was the one place
303
+ // we did it anyway.
304
+ const install = await chooseInstall(opts.install);
305
+ const launcher = resolveLauncher({ version: VERSION, install });
241
306
  const note = launcherNote(launcher);
242
307
  if (note)
243
308
  console.log(note);
package/dist/config.js CHANGED
@@ -61,6 +61,10 @@ export function loadConfig() {
61
61
  loginProvider: stored.loginProvider === "github" || stored.loginProvider === "google"
62
62
  ? stored.loginProvider
63
63
  : undefined,
64
+ // Same rule (RTSC-523): only a real boolean counts. Anything else reads as
65
+ // "never asked", so a mangled file leads to a question rather than to an
66
+ // install nobody agreed to.
67
+ globalInstall: typeof stored.globalInstall === "boolean" ? stored.globalInstall : undefined,
64
68
  };
65
69
  }
66
70
  /** Move a corrupt config aside to a unique sibling so a human can recover any
package/dist/index.js CHANGED
@@ -178,6 +178,10 @@ program
178
178
  .option("--agent <name>", "Agent member name (default: auto)")
179
179
  .option("--runtime <runtime>", "Agent runtime", "claude-code")
180
180
  .option("-y, --yes", "Don't prompt to confirm replacing an existing binding")
181
+ // RTSC-523 — decline the global install without a TTY. `bind`/`join` ask when a human
182
+ // is present; this is how a scripted run, or a developer whose own agent runs setup,
183
+ // says no up front instead of being installed onto.
184
+ .option("--no-install", "Don't install `retasc` on this machine; wire the pinned npx launcher instead")
181
185
  .action(async (opts) => {
182
186
  requireLogin();
183
187
  await bindAction(opts).catch(fail);
@@ -400,6 +404,10 @@ program
400
404
  // RTSC-477 — name the way back. `--yes` skips the identity question and must never answer
401
405
  // it, so the flag that causes the gap is the right place to say how to close it.
402
406
  .option("-y, --yes", "Don't prompt to replace an existing binding, and skip the identity question (ask it later with `retasc identity`)")
407
+ // RTSC-523 — decline the global install without a TTY. `bind`/`join` ask when a human
408
+ // is present; this is how a scripted run, or a developer whose own agent runs setup,
409
+ // says no up front instead of being installed onto.
410
+ .option("--no-install", "Don't install `retasc` on this machine; wire the pinned npx launcher instead")
403
411
  .allowExcessArguments(false)
404
412
  .action(async (link, opts) => {
405
413
  await joinAction(link, opts).catch(fail);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.13.1",
3
+ "version": "1.14.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": {