@retasc/cli 1.25.0 → 1.27.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 +27 -0
- package/dist/api.js +3 -1
- package/dist/commands/bind.js +78 -11
- package/dist/commands/identity.js +3 -1
- package/dist/commands/mcp.js +20 -5
- package/dist/index.js +19 -6
- package/dist/lib/card.js +66 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,33 @@ 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.27.0 (2026-08-19)
|
|
10
|
+
|
|
11
|
+
- **RTSC-673** — `bind` ends by telling you what to do, not by dumping config. It used to
|
|
12
|
+
close on fourteen lines of MCP JSON plus "(Claude Code CLI not detected)": a block
|
|
13
|
+
instructing you to paste something we had just written to disk for you, and a note that
|
|
14
|
+
reads like a fault on a screen whose whole content is that everything worked. Between
|
|
15
|
+
them they buried the one thing a new owner needed. Now it prints a receipt card — org,
|
|
16
|
+
project, where the key went, what landed in your repo — and then says to start your
|
|
17
|
+
agent. The closing lines follow the project: an empty one invites you to describe what
|
|
18
|
+
you're building, a project with work in it tells you to say "next issue". The JSON still
|
|
19
|
+
prints when the marker could not be written anywhere, which is the case it exists for.
|
|
20
|
+
|
|
21
|
+
## 1.26.0 (2026-08-19)
|
|
22
|
+
|
|
23
|
+
- **RTSC-670** — `retasc bind` can sign you in again. It has opened with the device flow
|
|
24
|
+
since 1.20-era, but a leftover session guard in the command wrapper exited first, so on
|
|
25
|
+
a machine that had never signed in — the only state a new owner is ever in — `bind`
|
|
26
|
+
printed "Not signed in" and stopped. Owner onboarding dead-ended at its first
|
|
27
|
+
documented command, on every release for three weeks. The guard was correct when it was
|
|
28
|
+
written and became redundant the moment `bind` grew its own sign-in; nothing noticed
|
|
29
|
+
because no test ran the CLI with no session. Four now do.
|
|
30
|
+
- **RTSC-671** — errors name the command you actually ran. Told to `npx @retasc/cli bind`
|
|
31
|
+
and then answered with "Run `retasc login` first", you were pointed at a binary that
|
|
32
|
+
invocation never installs — a recoverable stumble turned into a dead end. Every "run X
|
|
33
|
+
first" message now renders `npx -y @retasc/cli@<version> …` or `retasc …` to match how
|
|
34
|
+
you started it.
|
|
35
|
+
|
|
9
36
|
## 1.25.0 (2026-08-19)
|
|
10
37
|
|
|
11
38
|
- **RTSC-669** — the invite project picker takes several projects. 1.24.0 shipped it
|
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
|
@@ -2,12 +2,13 @@ import { basename } from "node:path";
|
|
|
2
2
|
import { api, cliError } from "../api.js";
|
|
3
3
|
import { deviceLogin } from "../auth.js";
|
|
4
4
|
import { loadConfig, patchConfig } from "../config.js";
|
|
5
|
-
import { installMarker } from "./mcp.js";
|
|
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
8
|
import { resolveLauncher, launcherNote, runsOk, selfCommand } from "../lib/launcher.js";
|
|
9
9
|
import { ask, confirm, isInteractive } from "../lib/prompt.js";
|
|
10
10
|
import { clean } from "../lib/text.js";
|
|
11
|
+
import { card, DOT } from "../lib/card.js";
|
|
11
12
|
import { VERSION } from "../version.js";
|
|
12
13
|
// RTSC-508: `ask`/`confirm`/`isInteractive` now live in lib/prompt.ts so `auth.ts`
|
|
13
14
|
// can use them without closing an import cycle (bind → auth → bind). Re-exported
|
|
@@ -217,7 +218,7 @@ async function nameAProject(orgId) {
|
|
|
217
218
|
throw new Error("project name and prefix required");
|
|
218
219
|
const p = (await api.createProject({ orgId, name, prefix: pfx }));
|
|
219
220
|
console.log(`✓ Created project ${p.prefix}.`);
|
|
220
|
-
return { projectId: p.projectId, prefix: p.prefix };
|
|
221
|
+
return { projectId: p.projectId, prefix: p.prefix, empty: true };
|
|
221
222
|
}
|
|
222
223
|
/**
|
|
223
224
|
* "Where does your work come from?" — the first project, for an org that has none.
|
|
@@ -260,7 +261,8 @@ async function firstProject(orgId, orgLabel) {
|
|
|
260
261
|
console.log("\nNo import. Let's make an empty project instead.");
|
|
261
262
|
return nameAProject(orgId);
|
|
262
263
|
}
|
|
263
|
-
|
|
264
|
+
// An import that ran has work in it by definition — that is what it did.
|
|
265
|
+
return { ...done, empty: false };
|
|
264
266
|
}
|
|
265
267
|
/**
|
|
266
268
|
* Everything from "which project" to a working folder: pick the project, make `retasc`
|
|
@@ -284,6 +286,11 @@ export async function completeWorkspaceSetup(args) {
|
|
|
284
286
|
// --- resolve project (pick / flag / create) --------------------------------
|
|
285
287
|
let projectId = opts.projectId;
|
|
286
288
|
let prefix;
|
|
289
|
+
// RTSC-673 — does this project have anything in it yet? Decides which two sentences
|
|
290
|
+
// close the run. UNKNOWN defaults to "has work": a --project-id we never listed could
|
|
291
|
+
// be either, and telling someone with 200 issues that their project is empty is the
|
|
292
|
+
// worse half of that guess.
|
|
293
|
+
let emptyProject = false;
|
|
287
294
|
if (!projectId && opts.project && opts.prefix) {
|
|
288
295
|
if (!args.canCreateProject) {
|
|
289
296
|
cliError("FORBIDDEN", "Only an owner or admin can create a project.", `Ask one of them to add one, then pass --project-id <id>.`);
|
|
@@ -291,10 +298,13 @@ export async function completeWorkspaceSetup(args) {
|
|
|
291
298
|
const p = (await api.createProject({ orgId, name: opts.project, prefix: opts.prefix }));
|
|
292
299
|
projectId = p.projectId;
|
|
293
300
|
prefix = p.prefix;
|
|
301
|
+
emptyProject = true;
|
|
294
302
|
console.log(`✓ Created project ${p.prefix}.`);
|
|
295
303
|
}
|
|
296
304
|
if (!projectId) {
|
|
297
305
|
const { projects } = (await api.listProjects({ orgId }));
|
|
306
|
+
// `counter` is the last ISSUED number, so 0 means nothing was ever filed here —
|
|
307
|
+
// which is the only question RTSC-673 asks of it. It is NOT an open count.
|
|
298
308
|
const list = projects ?? [];
|
|
299
309
|
if (!args.canCreateProject) {
|
|
300
310
|
// A member cannot create one, so an empty org is a dead end here — and it has to say
|
|
@@ -308,11 +318,13 @@ export async function completeWorkspaceSetup(args) {
|
|
|
308
318
|
if (list.length === 1) {
|
|
309
319
|
projectId = list[0].id;
|
|
310
320
|
prefix = list[0].prefix;
|
|
321
|
+
emptyProject = projectIsEmpty(list[0]);
|
|
311
322
|
}
|
|
312
323
|
else if (isInteractive()) {
|
|
313
324
|
const chosen = await pickExisting("Select a project", list, (p) => `${clean(p.prefix)} — ${clean(p.name)}`);
|
|
314
325
|
projectId = chosen.id;
|
|
315
326
|
prefix = chosen.prefix;
|
|
327
|
+
emptyProject = projectIsEmpty(chosen);
|
|
316
328
|
}
|
|
317
329
|
else {
|
|
318
330
|
cliError("AMBIGUOUS", `Org ${org} has ${list.length} projects, so one has to be named.`, `Pass --project-id <id> (or run interactively): ${list.map((p) => `${p.prefix}=${p.id}`).join(", ")}`);
|
|
@@ -330,17 +342,20 @@ export async function completeWorkspaceSetup(args) {
|
|
|
330
342
|
const made = await firstProject(orgId, org);
|
|
331
343
|
projectId = made.projectId;
|
|
332
344
|
prefix = made.prefix;
|
|
345
|
+
emptyProject = made.empty;
|
|
333
346
|
}
|
|
334
347
|
else if (isInteractive()) {
|
|
335
348
|
const chosen = await pick("Select a project", list, (p) => `${clean(p.prefix)} — ${clean(p.name)}`);
|
|
336
349
|
if (chosen) {
|
|
337
350
|
projectId = chosen.id;
|
|
338
351
|
prefix = chosen.prefix;
|
|
352
|
+
emptyProject = projectIsEmpty(chosen);
|
|
339
353
|
}
|
|
340
354
|
else {
|
|
341
355
|
const made = await nameAProject(orgId);
|
|
342
356
|
projectId = made.projectId;
|
|
343
357
|
prefix = made.prefix;
|
|
358
|
+
emptyProject = made.empty;
|
|
344
359
|
}
|
|
345
360
|
}
|
|
346
361
|
else if (list.length === 1) {
|
|
@@ -376,7 +391,8 @@ export async function completeWorkspaceSetup(args) {
|
|
|
376
391
|
runtime: opts.runtime ?? "claude-code",
|
|
377
392
|
keyName: prefix ? `${prefix} key` : undefined,
|
|
378
393
|
}));
|
|
379
|
-
|
|
394
|
+
// The key is named in the receipt card below, not here — one mention, in the place
|
|
395
|
+
// that says where it went (RTSC-673).
|
|
380
396
|
// RTSC-92: the secret stays OUT of the repo. Store it in the home keystore
|
|
381
397
|
// keyed by a workspace id; reuse this folder's existing id (so a re-bind, or a
|
|
382
398
|
// teammate's committed marker, keeps the same id) or mint a fresh one.
|
|
@@ -394,16 +410,31 @@ export async function completeWorkspaceSetup(args) {
|
|
|
394
410
|
args.onBound?.();
|
|
395
411
|
// Per-folder only (local scope), always watchdog. The marker carries only the
|
|
396
412
|
// workspace id — no secret — so ./.mcp.json is safe to commit.
|
|
397
|
-
installMarker({ workspaceId, scope: "local", launcher });
|
|
398
|
-
// Confirm the binding the same way the agent will see it
|
|
413
|
+
const marker = installMarker({ workspaceId, scope: "local", launcher, quiet: true });
|
|
414
|
+
// Confirm the binding the same way the agent will see it, and print it as the receipt
|
|
415
|
+
// (RTSC-673). Resolved rather than echoed from what we just sent: this is the last
|
|
416
|
+
// chance to notice a folder bound to something other than what was asked for, and a
|
|
417
|
+
// card built from our own inputs could never show that.
|
|
418
|
+
let bound = null;
|
|
399
419
|
try {
|
|
400
420
|
const b = await resolveBinding(cfg.mcpUrl, minted.key);
|
|
401
|
-
|
|
402
|
-
` Agents launched here can only ever read or write ${clean(b.project.prefix)}.`);
|
|
421
|
+
bound = { org: clean(b.org.name), prefix: clean(b.project.prefix), name: b.project.name };
|
|
403
422
|
}
|
|
404
423
|
catch {
|
|
405
|
-
/* binding written; whoami confirmation is best-effort */
|
|
424
|
+
/* binding written; whoami confirmation is best-effort — fall back to what we sent */
|
|
406
425
|
}
|
|
426
|
+
const orgLabel = bound?.org ?? clean(args.orgLabel ?? "");
|
|
427
|
+
const pfx = bound?.prefix ?? clean(prefix ?? "");
|
|
428
|
+
console.log("\n" +
|
|
429
|
+
card(`✓ This folder is bound to ${orgLabel} / ${pfx}`, [
|
|
430
|
+
{ label: "Org", value: orgLabel },
|
|
431
|
+
{ label: "Project", value: bound?.name ? `${pfx} ${DOT} ${bound.name}` : pfx },
|
|
432
|
+
{ label: "Agent key", value: `${minted.key.slice(0, 14)}…`, note: "never leaves ~/.retasc" },
|
|
433
|
+
{ label: "MCP wired", value: marker.where, note: "secret-free, safe to commit" },
|
|
434
|
+
], `Agents launched here can only ever read or write ${pfx}.`));
|
|
435
|
+
// Only when we could write it nowhere. See printMarkerBlock.
|
|
436
|
+
if (!marker.wroteFile && marker.where === "")
|
|
437
|
+
printMarkerBlock(workspaceId, launcher);
|
|
407
438
|
// RTSC-519 — say that setup FINISHED, and what to do with it.
|
|
408
439
|
//
|
|
409
440
|
// Here rather than in either command, because it is true of both and this is the tail
|
|
@@ -415,7 +446,43 @@ export async function completeWorkspaceSetup(args) {
|
|
|
415
446
|
// AFTER the confirmation, and after everything that can throw, so it is only ever
|
|
416
447
|
// printed by a run that actually finished. Nothing below it can fail.
|
|
417
448
|
if (isInteractive())
|
|
418
|
-
console.log(`\n${NEXT_STEP}`);
|
|
449
|
+
console.log(`\n${NEXT_STEP}\n\n${whatNow(pfx, emptyProject)}`);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The two sentences under `NEXT_STEP`, chosen by whether there is anything to pull yet
|
|
453
|
+
* (RTSC-673).
|
|
454
|
+
*
|
|
455
|
+
* `NEXT_STEP` says how to connect an agent. It cannot say what to ASK it, because the
|
|
456
|
+
* right first thing differs entirely: a project created ten seconds ago has an empty
|
|
457
|
+
* queue, and telling that person to say "next issue" sends them to a blank result as
|
|
458
|
+
* their first experience of the product. Telling the person who just imported 142 issues
|
|
459
|
+
* to "describe what you're building" wastes the work they already have.
|
|
460
|
+
*
|
|
461
|
+
* No count in either branch, deliberately. `projects.counter` is the last ISSUED number,
|
|
462
|
+
* so a project with 200 created and 195 closed still reads 200 — printing it as an open
|
|
463
|
+
* count would be a precise lie. The importer already reports its own totals during the
|
|
464
|
+
* import step, which is where a real number belongs. A true count here needs an
|
|
465
|
+
* open-issue query the CLI does not have yet.
|
|
466
|
+
*
|
|
467
|
+
* "0 issues" is never printed for the empty case either: zero is the expected state for
|
|
468
|
+
* a project someone just named, and stating it as a quantity reads like a failure.
|
|
469
|
+
*/
|
|
470
|
+
export function projectIsEmpty(p) {
|
|
471
|
+
// `counter` is the last ISSUED number, never a count of open work: a project with 200
|
|
472
|
+
// filed and 195 closed still reads 200. So it answers exactly one question — has
|
|
473
|
+
// anything ever been filed here — which is the only one `whatNow` asks. Absent
|
|
474
|
+
// (an older payload, or a caller that never listed the project) reads as NOT empty:
|
|
475
|
+
// telling someone with 200 issues that their project is empty is the worse half of
|
|
476
|
+
// that guess.
|
|
477
|
+
return p.counter === 0;
|
|
478
|
+
}
|
|
479
|
+
export function whatNow(prefix, empty) {
|
|
480
|
+
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.`;
|
|
419
486
|
}
|
|
420
487
|
export async function setupFromToken(token, opts, deps = {}) {
|
|
421
488
|
const cfg = loadConfig();
|
|
@@ -493,7 +560,7 @@ export async function ensureSignedIn() {
|
|
|
493
560
|
if (loadConfig().token)
|
|
494
561
|
return;
|
|
495
562
|
if (!isInteractive()) {
|
|
496
|
-
throw new Error(
|
|
563
|
+
throw new Error(`Not signed in. Run \`${selfCommand(VERSION)} login\` first (no TTY here for the device flow).`);
|
|
497
564
|
}
|
|
498
565
|
// RTSC-508: don't name a provider here. `deviceLogin` asks which door, and
|
|
499
566
|
// 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/mcp.js
CHANGED
|
@@ -229,15 +229,30 @@ export function installMarker(opts) {
|
|
|
229
229
|
const shared = portableLauncher(resolved, VERSION);
|
|
230
230
|
const res = tryClaudeCliMarker(opts.workspaceId, scope, scope === "project" ? shared : resolved.launcher);
|
|
231
231
|
if (res.ok) {
|
|
232
|
-
|
|
232
|
+
if (!opts.quiet)
|
|
233
|
+
console.log(`✓ Registered Retasc watchdog (secret-free marker, scope: ${scope}).`);
|
|
234
|
+
return { where: `Claude Code (${scope})`, wroteFile: false };
|
|
233
235
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
236
|
+
// Always ./.mcp.json, whatever scope was asked for, so always the shared form.
|
|
237
|
+
const path = writeProjectMcpJson(mcpMarkerEntry(opts.workspaceId, shared));
|
|
238
|
+
if (!opts.quiet) {
|
|
237
239
|
console.log(`✓ Wrote secret-free watchdog marker to ${path}`);
|
|
238
240
|
console.log(` ${fallbackNote(res)}`);
|
|
239
241
|
}
|
|
242
|
+
return { where: path, wroteFile: true };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The MCP config block, printed ONLY when we could not write it anywhere (RTSC-673).
|
|
246
|
+
*
|
|
247
|
+
* It exists for someone wiring a client we do not know how to configure, and that is a
|
|
248
|
+
* real case. It is not the case where we just wrote the identical content to
|
|
249
|
+
* `.mcp.json` for them — there it was fourteen lines instructing a person to perform a
|
|
250
|
+
* job already finished, at the end of the one screen that should have said what to do
|
|
251
|
+
* next. `retasc doctor` prints it on demand for anyone who does need it.
|
|
252
|
+
*/
|
|
253
|
+
export function printMarkerBlock(workspaceId, launcher) {
|
|
254
|
+
const shared = portableLauncher(launcher ?? resolveLauncher({ version: VERSION, install: false }), VERSION);
|
|
240
255
|
console.log("\nMarker block (any stdio MCP client) — no secret, safe to commit:\n");
|
|
241
|
-
console.log(mcpMarkerConfigBlock(
|
|
256
|
+
console.log(mcpMarkerConfigBlock(workspaceId, shared));
|
|
242
257
|
console.log("\nThe key lives in your home keystore (~/.retasc/bindings.json), not in the repo.");
|
|
243
258
|
}
|
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/dist/lib/card.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { clean } from "./text.js";
|
|
2
|
+
// RTSC-673 — the receipt a finished `bind` prints.
|
|
3
|
+
//
|
|
4
|
+
// It replaces a tail that ended on fourteen lines of MCP config plus a note reading
|
|
5
|
+
// "(Claude Code CLI not detected)". Both were wrong at the moment they were read: the
|
|
6
|
+
// JSON told someone to paste a block we had just written to disk for them, and the note
|
|
7
|
+
// announced a detection miss on a screen whose whole content is that everything worked.
|
|
8
|
+
// Between them they buried the one thing a new owner needed, which is what to do next.
|
|
9
|
+
//
|
|
10
|
+
// A box rather than a list because this is a RECEIPT: bounded, glanced at once,
|
|
11
|
+
// confirming the four facts someone might reasonably doubt (which org, which project,
|
|
12
|
+
// where the secret went, what got written into their repo). The call to action stays
|
|
13
|
+
// OUTSIDE it, so the thing to act on is not one more row to scan past.
|
|
14
|
+
/** Inner width. 62 + borders fits an 80-column terminal with room to spare. */
|
|
15
|
+
const W = 62;
|
|
16
|
+
/**
|
|
17
|
+
* Does this terminal render box-drawing characters and `…` reliably?
|
|
18
|
+
*
|
|
19
|
+
* Windows `cmd.exe` is the one that does not, and it is the same distinction
|
|
20
|
+
* `lib/launcher.ts` already draws for spawning. A garbled box is worse than a plain
|
|
21
|
+
* one: it reads as corruption at the exact moment we are telling someone everything
|
|
22
|
+
* worked.
|
|
23
|
+
*/
|
|
24
|
+
const FANCY = process.platform !== "win32";
|
|
25
|
+
const B = FANCY
|
|
26
|
+
? { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", ml: "├", mr: "┤", dot: "·", ell: "…" }
|
|
27
|
+
: { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", ml: "+", mr: "+", dot: "-", ell: "..." };
|
|
28
|
+
/**
|
|
29
|
+
* Fit `s` to `n` columns, ellipsing rather than letting it blow the box open.
|
|
30
|
+
*
|
|
31
|
+
* Truncation, not padding alone: an org can be named anything, and a name one character
|
|
32
|
+
* over the width turns every following line into a ragged edge. Padding is the easy half
|
|
33
|
+
* and the half that looks fine in testing, because test fixtures are short.
|
|
34
|
+
*/
|
|
35
|
+
function fit(s, n) {
|
|
36
|
+
const t = clean(s);
|
|
37
|
+
if (t.length <= n)
|
|
38
|
+
return t.padEnd(n);
|
|
39
|
+
return t.slice(0, Math.max(0, n - B.ell.length)) + B.ell;
|
|
40
|
+
}
|
|
41
|
+
/** One bordered card: a title line, labelled rows, then a footer line. */
|
|
42
|
+
export function card(title, rows, footer) {
|
|
43
|
+
const inner = W - 2; // one space of padding each side
|
|
44
|
+
const line = (s) => `${B.v} ${fit(s, inner)} ${B.v}`;
|
|
45
|
+
const rule = (l, r) => `${l}${B.h.repeat(W)}${r}`;
|
|
46
|
+
const LABEL = 12;
|
|
47
|
+
const VALUE = 20;
|
|
48
|
+
const out = [rule(B.tl, B.tr), line(title), rule(B.ml, B.mr)];
|
|
49
|
+
for (const r of rows) {
|
|
50
|
+
// A note sits in a third column, so the values stay in one scannable rail. Without a
|
|
51
|
+
// note the value simply runs on — padding it anyway would leave a trench of spaces
|
|
52
|
+
// in the middle of the card.
|
|
53
|
+
const body = r.note
|
|
54
|
+
? `${fit(r.label, LABEL)}${fit(r.value, VALUE)}${r.note}`
|
|
55
|
+
: `${fit(r.label, LABEL)}${r.value}`;
|
|
56
|
+
out.push(line(body));
|
|
57
|
+
}
|
|
58
|
+
if (footer) {
|
|
59
|
+
out.push(rule(B.ml, B.mr));
|
|
60
|
+
out.push(line(footer));
|
|
61
|
+
}
|
|
62
|
+
out.push(rule(B.bl, B.br));
|
|
63
|
+
return out.join("\n");
|
|
64
|
+
}
|
|
65
|
+
/** The `·` the CLI already uses between a prefix and its name, ASCII-safe. */
|
|
66
|
+
export const DOT = B.dot;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retasc/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.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": {
|