@retasc/cli 1.24.0 → 1.26.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 +26 -0
- package/dist/api.js +3 -1
- package/dist/commands/bind.js +1 -1
- package/dist/commands/identity.js +3 -1
- package/dist/commands/invite.js +76 -7
- package/dist/index.js +19 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,32 @@ 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.26.0 (2026-08-19)
|
|
10
|
+
|
|
11
|
+
- **RTSC-670** — `retasc bind` can sign you in again. It has opened with the device flow
|
|
12
|
+
since 1.20-era, but a leftover session guard in the command wrapper exited first, so on
|
|
13
|
+
a machine that had never signed in — the only state a new owner is ever in — `bind`
|
|
14
|
+
printed "Not signed in" and stopped. Owner onboarding dead-ended at its first
|
|
15
|
+
documented command, on every release for three weeks. The guard was correct when it was
|
|
16
|
+
written and became redundant the moment `bind` grew its own sign-in; nothing noticed
|
|
17
|
+
because no test ran the CLI with no session. Four now do.
|
|
18
|
+
- **RTSC-671** — errors name the command you actually ran. Told to `npx @retasc/cli bind`
|
|
19
|
+
and then answered with "Run `retasc login` first", you were pointed at a binary that
|
|
20
|
+
invocation never installs — a recoverable stumble turned into a dead end. Every "run X
|
|
21
|
+
first" message now renders `npx -y @retasc/cli@<version> …` or `retasc …` to match how
|
|
22
|
+
you started it.
|
|
23
|
+
|
|
24
|
+
## 1.25.0 (2026-08-19)
|
|
25
|
+
|
|
26
|
+
- **RTSC-669** — the invite project picker takes several projects. 1.24.0 shipped it
|
|
27
|
+
single-select, which left "two of these four" expressible nowhere: the workaround —
|
|
28
|
+
repeating `--project-id` — needs ids that nothing in the CLI lists, which is the gap
|
|
29
|
+
the picker existed to close. Answer `2,4` (or `2 4`) and the invite grants exactly
|
|
30
|
+
those. Naming every project collapses to all projects, so the grant follows the org as
|
|
31
|
+
it grows rather than freezing today's list. `1) All projects` can't be combined with
|
|
32
|
+
individual ones: that's a contradiction about the widest grant there is, so it re-asks
|
|
33
|
+
instead of guessing which half you meant.
|
|
34
|
+
|
|
9
35
|
## 1.24.0 (2026-08-19)
|
|
10
36
|
|
|
11
37
|
- **RTSC-667** — `retasc members invite` asks instead of demanding ids. It used to
|
package/dist/api.js
CHANGED
|
@@ -2,6 +2,8 @@ import { ConvexHttpClient } from "convex/browser";
|
|
|
2
2
|
import { makeFunctionReference } from "convex/server";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
4
|
import { refreshSession, deviceLogin } from "./auth.js";
|
|
5
|
+
import { selfCommand } from "./lib/launcher.js";
|
|
6
|
+
import { VERSION } from "./version.js";
|
|
5
7
|
// Typed-ish references to the public management functions in convex/manage.ts.
|
|
6
8
|
// The CLI is a standalone package, so we reference functions by name rather than
|
|
7
9
|
// importing the parent's generated api.
|
|
@@ -163,7 +165,7 @@ async function withAuth(call) {
|
|
|
163
165
|
if (await refreshSession())
|
|
164
166
|
return await call();
|
|
165
167
|
if (!process.stdout.isTTY) {
|
|
166
|
-
throw new Error(
|
|
168
|
+
throw new Error(`Session expired and could not refresh. Run \`${selfCommand(VERSION)} login\`.`);
|
|
167
169
|
}
|
|
168
170
|
console.error("Session expired — re-authenticating with GitHub…");
|
|
169
171
|
await deviceLogin();
|
package/dist/commands/bind.js
CHANGED
|
@@ -493,7 +493,7 @@ export async function ensureSignedIn() {
|
|
|
493
493
|
if (loadConfig().token)
|
|
494
494
|
return;
|
|
495
495
|
if (!isInteractive()) {
|
|
496
|
-
throw new Error(
|
|
496
|
+
throw new Error(`Not signed in. Run \`${selfCommand(VERSION)} login\` first (no TTY here for the device flow).`);
|
|
497
497
|
}
|
|
498
498
|
// RTSC-508: don't name a provider here. `deviceLogin` asks which door, and
|
|
499
499
|
// announcing "GitHub" before the question would be wrong for the invited
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { api } from "../api.js";
|
|
2
2
|
import { clean } from "../lib/text.js";
|
|
3
3
|
import { isInteractive } from "./bind.js";
|
|
4
|
+
import { selfCommand } from "../lib/launcher.js";
|
|
5
|
+
import { VERSION } from "../version.js";
|
|
4
6
|
import { identityLoop } from "./join.js";
|
|
5
7
|
/**
|
|
6
8
|
* Which org's placeholders to offer.
|
|
@@ -65,7 +67,7 @@ function report(outcome, orgLabel) {
|
|
|
65
67
|
console.log("Noted. That covers those tools only, so a later import will ask again.");
|
|
66
68
|
return;
|
|
67
69
|
case "left":
|
|
68
|
-
console.log(
|
|
70
|
+
console.log(`Nothing linked. Run \`${selfCommand(VERSION)} identity\` again whenever you want to look.`);
|
|
69
71
|
return;
|
|
70
72
|
case "skipped":
|
|
71
73
|
// Only reachable non-interactively; the interactive guard below catches that first.
|
package/dist/commands/invite.js
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
import { cliError } from "../api.js";
|
|
2
2
|
import { pickExisting, isInteractive, ask } from "./bind.js";
|
|
3
3
|
import { clean } from "../lib/text.js";
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Parse an answer to the multi-select prompt into 1-based positions (RTSC-669).
|
|
6
|
+
*
|
|
7
|
+
* Exported for its tests: this is the whole of the input rule, and the rule is the part
|
|
8
|
+
* that goes wrong. Returns null for anything that isn't a clean selection, so every
|
|
9
|
+
* rejection re-prompts rather than resolving to a subset of what was typed — picking
|
|
10
|
+
* SOME of the projects somebody listed is worse than picking none, because they would
|
|
11
|
+
* never see which ones went missing.
|
|
12
|
+
*
|
|
13
|
+
* `max` is the highest offerable position.
|
|
14
|
+
*/
|
|
15
|
+
export function parseSelection(answer, max) {
|
|
16
|
+
const tokens = answer.split(/[\s,]+/).filter(Boolean);
|
|
17
|
+
if (!tokens.length)
|
|
18
|
+
return null;
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const t of tokens) {
|
|
21
|
+
// Digits ONLY. `Number()` alone accepts "0x4", "4e0", "+4" and "4.0", and RTSC-269
|
|
22
|
+
// is the record of what happens when a non-canonical spelling reaches a branch it
|
|
23
|
+
// was never meant to: a typo picked the destructive option instead of re-asking.
|
|
24
|
+
if (!/^\d+$/.test(t))
|
|
25
|
+
return null;
|
|
26
|
+
const n = Number(t);
|
|
27
|
+
if (n < 1 || n > max)
|
|
28
|
+
return null;
|
|
29
|
+
if (!out.includes(n))
|
|
30
|
+
out.push(n); // "2,2" is a typo, not a double selection
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
7
34
|
/**
|
|
8
35
|
* Which org is this invite for?
|
|
9
36
|
*
|
|
@@ -31,9 +58,22 @@ export async function chooseInviteOrg(orgs, opts = {}) {
|
|
|
31
58
|
/**
|
|
32
59
|
* Which projects may the invitee reach? `undefined` ⇒ every project.
|
|
33
60
|
*
|
|
61
|
+
* MULTI-select (RTSC-669), because the data model always was: `projectIds` is an array
|
|
62
|
+
* and the Dash modal has picked any combination since RTSC-665. Shipping a single-select
|
|
63
|
+
* picker here left "two of these four" expressible nowhere — the documented workaround,
|
|
64
|
+
* repeating `--project-id`, needs ids that nothing in the CLI lists, which is the very
|
|
65
|
+
* gap the picker was added to close.
|
|
66
|
+
*
|
|
67
|
+
* Hand-rolled rather than widened out of `choose`/`pickExisting`: those are single-select
|
|
68
|
+
* by contract and `bind`, `join` and the identity prompt all rely on it. Teaching them to
|
|
69
|
+
* return "one or many" would put that ambiguity into every picker in the product to serve
|
|
70
|
+
* one caller.
|
|
71
|
+
*
|
|
34
72
|
* The menu's FIRST entry is "All projects" — an explicit option, never the thing you
|
|
35
|
-
* get by not answering
|
|
36
|
-
*
|
|
73
|
+
* get by not answering, and never combinable with the others (that is a contradiction
|
|
74
|
+
* about the widest grant there is, so it re-prompts rather than resolving). Handing
|
|
75
|
+
* someone the whole org should be a choice somebody made; that is the same call the
|
|
76
|
+
* Dash's invite form makes, for the same reason.
|
|
37
77
|
*
|
|
38
78
|
* Two cases skip the question and mean "every project", which is what this command has
|
|
39
79
|
* always done when no project was named:
|
|
@@ -47,6 +87,35 @@ export async function chooseInviteProjects(projects, opts = {}) {
|
|
|
47
87
|
const interactive = opts.interactive ?? isInteractive();
|
|
48
88
|
if (!interactive)
|
|
49
89
|
return undefined;
|
|
50
|
-
const
|
|
51
|
-
|
|
90
|
+
const askFn = opts.askFn ?? ask;
|
|
91
|
+
console.log("\nWhich projects can they see:");
|
|
92
|
+
console.log(" 1) All projects");
|
|
93
|
+
projects.forEach((p, i) => console.log(` ${i + 2}) ${clean(p.name)}`));
|
|
94
|
+
const max = projects.length + 1;
|
|
95
|
+
// Three attempts then abort, the cap `choose` uses: an EOF stdin returns "" forever,
|
|
96
|
+
// and a prompt that re-asks forever is a hang rather than a failure.
|
|
97
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
98
|
+
const picked = parseSelection(await askFn("Choose a number, or several (e.g. 2,4): "), max);
|
|
99
|
+
if (!picked) {
|
|
100
|
+
console.log(`Please enter numbers between 1 and ${max}, separated by commas.`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const wantsAll = picked.includes(1);
|
|
104
|
+
if (wantsAll && picked.length > 1) {
|
|
105
|
+
// Not resolvable either way, so it must not be resolved. "All projects AND
|
|
106
|
+
// Marketing" is a contradiction about the widest grant in the product, and
|
|
107
|
+
// guessing which half they meant is how someone hands over an org by accident.
|
|
108
|
+
console.log("1) All projects can't be combined with individual projects. Pick one or the other.");
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (wantsAll)
|
|
112
|
+
return undefined;
|
|
113
|
+
const chosen = picked.map((n) => projects[n - 2]);
|
|
114
|
+
// Naming every project IS "all projects", and storing it as a list would freeze the
|
|
115
|
+
// grant at today's set — a project added next month would be missing from a scope
|
|
116
|
+
// its author chose as everything. `normalizeInviteScope` collapses this server-side
|
|
117
|
+
// too; doing it here as well keeps the confirmation honest about what was granted.
|
|
118
|
+
return chosen.length === projects.length ? undefined : chosen.map((p) => p.id);
|
|
119
|
+
}
|
|
120
|
+
throw new Error("no valid choice — aborting");
|
|
52
121
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +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
5
|
import { loadConfig, patchConfig, saveConfig, configPath, isLoggedIn } from "./config.js";
|
|
5
6
|
import { installMcp, normalizeScope } from "./commands/mcp.js";
|
|
6
7
|
import { installGate, resolveGatePrefix } from "./commands/gate.js";
|
|
@@ -25,7 +26,10 @@ program
|
|
|
25
26
|
.version(VERSION);
|
|
26
27
|
function requireLogin() {
|
|
27
28
|
if (!isLoggedIn()) {
|
|
28
|
-
|
|
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
|
|
31
|
+
// it here answers a failure with a second `command not found`.
|
|
32
|
+
console.error(`Not signed in. Run \`${selfCommand(VERSION)} login\` first.`);
|
|
29
33
|
process.exit(1);
|
|
30
34
|
}
|
|
31
35
|
}
|
|
@@ -87,7 +91,7 @@ program
|
|
|
87
91
|
// confirm scope before any work. Resolved from the local key, server-enforced.
|
|
88
92
|
const local = readLocalBinding(process.cwd());
|
|
89
93
|
if (local?.markerOnly) {
|
|
90
|
-
console.log(
|
|
94
|
+
console.log(`This folder → has a Retasc marker but no key on this machine. Run \`${selfCommand(VERSION)} bind\`.\n`);
|
|
91
95
|
}
|
|
92
96
|
else if (local) {
|
|
93
97
|
try {
|
|
@@ -107,10 +111,10 @@ program
|
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
else {
|
|
110
|
-
console.log(
|
|
114
|
+
console.log(`This folder → not bound. Run \`${selfCommand(VERSION)} bind\`.\n`);
|
|
111
115
|
}
|
|
112
116
|
if (!isLoggedIn()) {
|
|
113
|
-
console.log(
|
|
117
|
+
console.log(`Not signed in (management). Run \`${selfCommand(VERSION)} login\` to manage orgs/projects.`);
|
|
114
118
|
return;
|
|
115
119
|
}
|
|
116
120
|
try {
|
|
@@ -200,7 +204,16 @@ program
|
|
|
200
204
|
await setupFromToken(opts.setup, opts).catch(fail);
|
|
201
205
|
return;
|
|
202
206
|
}
|
|
203
|
-
requireLogin()
|
|
207
|
+
// NO requireLogin here (RTSC-670). `bindAction` opens with `ensureSignedIn`, which
|
|
208
|
+
// runs the device flow when there is no session — that IS the documented first step
|
|
209
|
+
// of owner onboarding, and it is the only state a new owner is ever in. This guard
|
|
210
|
+
// predated that (it was correct in 285ec74, when bindAction had no sign-in of its
|
|
211
|
+
// own) and `ensureSignedIn` arriving in 72182c3 left it in front of a door it
|
|
212
|
+
// permanently closed: every `bind` on a fresh machine exited 1 before reaching it.
|
|
213
|
+
//
|
|
214
|
+
// `ensureSignedIn` also refuses BETTER — it says why ("no TTY here for the device
|
|
215
|
+
// flow") where this said only that you weren't signed in, which is the diagnosis
|
|
216
|
+
// that sends someone looking in the wrong place.
|
|
204
217
|
await bindAction(opts).catch(fail);
|
|
205
218
|
});
|
|
206
219
|
// `retasc doctor` — verify this folder's binding + flag any illegal global server.
|
|
@@ -527,7 +540,7 @@ gate
|
|
|
527
540
|
if (r.source !== "--prefix")
|
|
528
541
|
console.log(`Prefix ${r.prefix} — from ${r.source}.`);
|
|
529
542
|
if (r.source === "global config") {
|
|
530
|
-
console.log(
|
|
543
|
+
console.log(` (This folder isn't bound — \`${selfCommand(VERSION)} bind\` makes the prefix folder-scoped.)`);
|
|
531
544
|
}
|
|
532
545
|
if (r.shadowedDefault) {
|
|
533
546
|
console.log(` ⚠ Ignoring global default prefix ${r.shadowedDefault} — this folder's binding wins.`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retasc/cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Retasc CLI
|
|
3
|
+
"version": "1.26.0",
|
|
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": {
|
|
7
7
|
"retasc": "dist/index.js"
|