@retasc/cli 1.17.0 → 1.19.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 +60 -0
- package/dist/api.js +19 -0
- package/dist/commands/bind.js +117 -8
- package/dist/commands/import.js +43 -25
- package/dist/index.js +16 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,66 @@ 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.19.0 (2026-08-03)
|
|
10
|
+
|
|
11
|
+
- **RTSC-495** — `retasc bind --setup <code>` sets a folder up with no sign-in and nothing
|
|
12
|
+
to answer, so an agent can do it on behalf of someone who does not use a terminal.
|
|
13
|
+
|
|
14
|
+
The Dash's connect step now hands over a block you paste to your agent, with this command
|
|
15
|
+
inside it. Everything the interactive `bind` would ask was already answered in the
|
|
16
|
+
browser, and the code carries those answers across. An agent's shell is not a TTY, so the
|
|
17
|
+
ordinary path refuses it outright at the first prompt.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
npx @retasc/cli bind --setup rtscsetup_…
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The code is single-use and lives ten minutes. The alternative — pasting a real API key
|
|
24
|
+
into the prompt — needed no backend at all, but would have left a long-lived credential in
|
|
25
|
+
an agent's conversation history forever.
|
|
26
|
+
|
|
27
|
+
A folder that is already connected is refused rather than silently replaced, and the
|
|
28
|
+
refusal happens BEFORE the code is spent, so a mix-up costs nothing.
|
|
29
|
+
|
|
30
|
+
- **RTSC-532** — the agent says which folder it is in, and the key remembers.
|
|
31
|
+
|
|
32
|
+
Binding the wrong folder used to look exactly like success: the agent still called in, so
|
|
33
|
+
the Dash went green, while the folder you actually work in had no Retasc in it. The prompt
|
|
34
|
+
now asks the agent to report its full path and wait before it runs anything, and the
|
|
35
|
+
confirmation names the real path instead of "This folder".
|
|
36
|
+
|
|
37
|
+
The key it creates is named after that folder, so the Keys page shows which folder each
|
|
38
|
+
key belongs to (`client-a`) instead of naming them all after the project (`ENG key`).
|
|
39
|
+
|
|
40
|
+
## 1.18.0 (2026-08-02)
|
|
41
|
+
|
|
42
|
+
- **RTSC-530** — setting up from scratch now asks where your work comes from, and imports it
|
|
43
|
+
in the same command.
|
|
44
|
+
|
|
45
|
+
`retasc bind` used to offer one thing at the project step: name a new project. So someone
|
|
46
|
+
arriving from Jira had to invent a project they didn't want, run `retasc import`
|
|
47
|
+
afterwards to get the one they did, and leave the empty one behind — and projects can't be
|
|
48
|
+
deleted individually.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Where does your work come from?
|
|
52
|
+
1) Linear
|
|
53
|
+
2) Jira
|
|
54
|
+
3) Asana
|
|
55
|
+
4) ClickUp
|
|
56
|
+
5) Shortcut
|
|
57
|
+
6) Start from scratch (name a project; your agents file into it)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Pick a tracker and the import runs right there, then the folder binds to the project it
|
|
61
|
+
created. Pick "start from scratch" and it behaves exactly as before. This is the same fork
|
|
62
|
+
the Dash has always offered; importing isn't a separate errand, it's one of the ways a
|
|
63
|
+
first project comes into existence.
|
|
64
|
+
|
|
65
|
+
Only when the org has no projects — binding a second folder in an existing org is still
|
|
66
|
+
just picking from the list. Declining at the import confirmation falls back to naming a
|
|
67
|
+
project rather than abandoning setup: you still asked to bind the folder.
|
|
68
|
+
|
|
9
69
|
## 1.17.0 (2026-08-02)
|
|
10
70
|
|
|
11
71
|
- **RTSC-529** — the column mapping is a numbered picker, like every other prompt.
|
package/dist/api.js
CHANGED
|
@@ -43,6 +43,10 @@ const fns = {
|
|
|
43
43
|
claimableGhosts: makeFunctionReference("ghosts:claimableGhosts"),
|
|
44
44
|
claimGhost: makeFunctionReference("ghosts:claimGhost"),
|
|
45
45
|
dismissGhostPrompt: makeFunctionReference("ghosts:dismissGhostPrompt"),
|
|
46
|
+
// RTSC-495 — the ONLY unauthenticated call the CLI makes. Redeemed by `bind --setup`
|
|
47
|
+
// on a machine that has never signed in, because the human answered everything in the
|
|
48
|
+
// Dash and it is her agent, not her, running this.
|
|
49
|
+
redeemSetupToken: makeFunctionReference("setupToken:redeemSetupToken"),
|
|
46
50
|
};
|
|
47
51
|
function client() {
|
|
48
52
|
const cfg = loadConfig();
|
|
@@ -51,6 +55,18 @@ function client() {
|
|
|
51
55
|
c.setAuth(cfg.token);
|
|
52
56
|
return c;
|
|
53
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* A client that NEVER attaches a session (RTSC-495).
|
|
60
|
+
*
|
|
61
|
+
* `client()` attaches `cfg.token` whenever one exists, and a setup-token redeem must not
|
|
62
|
+
* depend on it either way: the machine usually has no session at all, and where it has a
|
|
63
|
+
* STALE one, sending it risks failing an auth check on a call that never needed identity.
|
|
64
|
+
* Separate function rather than a flag, so "this call is unauthenticated" is visible at
|
|
65
|
+
* the call site instead of buried in an argument.
|
|
66
|
+
*/
|
|
67
|
+
function anonClient() {
|
|
68
|
+
return new ConvexHttpClient(loadConfig().deploymentUrl);
|
|
69
|
+
}
|
|
54
70
|
/**
|
|
55
71
|
* Read a caught backend error into the parts a command wants to show (RTSC-261).
|
|
56
72
|
*
|
|
@@ -178,6 +194,9 @@ export const api = {
|
|
|
178
194
|
runImport: (args) => withAuth(() => client().action(fns.runImport, args)),
|
|
179
195
|
latestImport: (args) => withAuth(() => client().query(fns.latestImport, args)),
|
|
180
196
|
importHistory: (args) => withAuth(() => client().query(fns.importHistory, args)),
|
|
197
|
+
// NOT wrapped in `withAuth`: there is no session to refresh, and its retry path would
|
|
198
|
+
// start a device flow — the exact interactive prompt this whole flow exists to avoid.
|
|
199
|
+
redeemSetupToken: (args) => anonClient().action(fns.redeemSetupToken, args),
|
|
181
200
|
claimableGhosts: (args) => withAuth(() => client().query(fns.claimableGhosts, args)),
|
|
182
201
|
claimGhost: (args) => withAuth(() => client().mutation(fns.claimGhost, args)),
|
|
183
202
|
dismissGhostPrompt: (args) => withAuth(() => client().mutation(fns.dismissGhostPrompt, args)),
|
package/dist/commands/bind.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import { api, cliError } from "../api.js";
|
|
2
3
|
import { deviceLogin } from "../auth.js";
|
|
3
4
|
import { loadConfig, patchConfig } from "../config.js";
|
|
@@ -208,6 +209,59 @@ function remember(globalInstall) {
|
|
|
208
209
|
patchConfig({ globalInstall });
|
|
209
210
|
return globalInstall;
|
|
210
211
|
}
|
|
212
|
+
/** Name and create a project by hand — the "start from scratch" ending. */
|
|
213
|
+
async function nameAProject(orgId) {
|
|
214
|
+
const name = await ask("New project name: ");
|
|
215
|
+
const pfx = (await ask("Project prefix (e.g. ACME): ")).toUpperCase();
|
|
216
|
+
if (!name || !pfx)
|
|
217
|
+
throw new Error("project name and prefix required");
|
|
218
|
+
const p = (await api.createProject({ orgId, name, prefix: pfx }));
|
|
219
|
+
console.log(`✓ Created project ${p.prefix}.`);
|
|
220
|
+
return { projectId: p.projectId, prefix: p.prefix };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* "Where does your work come from?" — the first project, for an org that has none.
|
|
224
|
+
*
|
|
225
|
+
* RTSC-530. Mirrors the Dash's `ProjectSetup`: the trackers first, then "start from
|
|
226
|
+
* scratch" as the last row rather than a footer escape hatch. The Dash records why it is a
|
|
227
|
+
* ROW — as a button it read as a way out of the five real options, which is the wrong
|
|
228
|
+
* framing when most people arriving have work to bring.
|
|
229
|
+
*
|
|
230
|
+
* The import flow is CALLED, not re-implemented (`runImportFlow`). A second copy of the
|
|
231
|
+
* auth prompt, target picker and column mapping is how the two drift, and the mapping is
|
|
232
|
+
* the part that decides where a whole backlog lands.
|
|
233
|
+
*
|
|
234
|
+
* Imported dynamically: `commands/import.ts` imports `pickExisting` from here, so a static
|
|
235
|
+
* import would close a cycle. Same pattern `join` uses for `identityLoop`.
|
|
236
|
+
*/
|
|
237
|
+
async function firstProject(orgId, orgLabel) {
|
|
238
|
+
let sources = [];
|
|
239
|
+
try {
|
|
240
|
+
sources = (await api.listImportSources());
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// Never block setting up a workspace on the importer list being reachable. A person
|
|
244
|
+
// who just wants an empty project should not be stopped by a source catalogue.
|
|
245
|
+
sources = [];
|
|
246
|
+
}
|
|
247
|
+
if (!sources.length)
|
|
248
|
+
return nameAProject(orgId);
|
|
249
|
+
const SCRATCH = { source: "", label: "Start from scratch (name a project; your agents file into it)" };
|
|
250
|
+
const picked = await pickExisting("Where does your work come from?", [...sources, SCRATCH], (x) => clean(x.label));
|
|
251
|
+
// Compared by REFERENCE, so a source that ever arrives with an empty id cannot be
|
|
252
|
+
// mistaken for the scratch row.
|
|
253
|
+
if (picked === SCRATCH)
|
|
254
|
+
return nameAProject(orgId);
|
|
255
|
+
const { runImportFlow } = await import("./import.js");
|
|
256
|
+
const done = await runImportFlow({ orgId, orgLabel, source: picked.source });
|
|
257
|
+
// Declining at the import confirmation is not an error, and must not abandon setup —
|
|
258
|
+
// they still asked to bind this folder. Fall back to the other branch of the same fork.
|
|
259
|
+
if (!done) {
|
|
260
|
+
console.log("\nNo import. Let's make an empty project instead.");
|
|
261
|
+
return nameAProject(orgId);
|
|
262
|
+
}
|
|
263
|
+
return done;
|
|
264
|
+
}
|
|
211
265
|
/**
|
|
212
266
|
* Everything from "which project" to a working folder: pick the project, make `retasc`
|
|
213
267
|
* durable, mint a key for that (org, project), write the binding, and wire the marker.
|
|
@@ -264,6 +318,19 @@ export async function completeWorkspaceSetup(args) {
|
|
|
264
318
|
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(", ")}`);
|
|
265
319
|
}
|
|
266
320
|
}
|
|
321
|
+
else if (isInteractive() && list.length === 0) {
|
|
322
|
+
// RTSC-530 — the FROM-SCRATCH case, and the Dash's question rather than ours.
|
|
323
|
+
//
|
|
324
|
+
// `Onboarding.tsx` asks "Where does your work come from?" and offers the five
|
|
325
|
+
// trackers AND "start from scratch" as one fork, because importing is not a separate
|
|
326
|
+
// act: it is one of the ways a first project comes into existence. The CLI used to
|
|
327
|
+
// ask only "New project name:", so someone arriving from Jira had to invent a project
|
|
328
|
+
// they did not want, run `retasc import` afterwards, and leave an empty one behind
|
|
329
|
+
// that cannot be deleted (org-granularity delete only).
|
|
330
|
+
const made = await firstProject(orgId, org);
|
|
331
|
+
projectId = made.projectId;
|
|
332
|
+
prefix = made.prefix;
|
|
333
|
+
}
|
|
267
334
|
else if (isInteractive()) {
|
|
268
335
|
const chosen = await pick("Select a project", list, (p) => `${clean(p.prefix)} — ${clean(p.name)}`);
|
|
269
336
|
if (chosen) {
|
|
@@ -271,14 +338,9 @@ export async function completeWorkspaceSetup(args) {
|
|
|
271
338
|
prefix = chosen.prefix;
|
|
272
339
|
}
|
|
273
340
|
else {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
throw new Error("project name and prefix required");
|
|
278
|
-
const p = (await api.createProject({ orgId, name, prefix: pfx }));
|
|
279
|
-
projectId = p.projectId;
|
|
280
|
-
prefix = p.prefix;
|
|
281
|
-
console.log(`✓ Created project ${p.prefix}.`);
|
|
341
|
+
const made = await nameAProject(orgId);
|
|
342
|
+
projectId = made.projectId;
|
|
343
|
+
prefix = made.prefix;
|
|
282
344
|
}
|
|
283
345
|
}
|
|
284
346
|
else if (list.length === 1) {
|
|
@@ -355,6 +417,53 @@ export async function completeWorkspaceSetup(args) {
|
|
|
355
417
|
if (isInteractive())
|
|
356
418
|
console.log(`\n${NEXT_STEP}`);
|
|
357
419
|
}
|
|
420
|
+
export async function setupFromToken(token, opts, deps = {}) {
|
|
421
|
+
const cfg = loadConfig();
|
|
422
|
+
const cwd = process.cwd();
|
|
423
|
+
const guard = deps.guard ?? rebindGuard;
|
|
424
|
+
const redeem = deps.redeem ??
|
|
425
|
+
((t, label) => api.redeemSetupToken({ token: t, label }));
|
|
426
|
+
// BEFORE the redeem. `rebindGuard` prints its own explanation and, with no TTY,
|
|
427
|
+
// refuses with exit 1 unless `--yes` — which is exactly the required behaviour, so it
|
|
428
|
+
// is reused rather than restated. No org/project passed: its idempotent-converge
|
|
429
|
+
// shortcut needs ids we do not have yet, and cannot apply to a single-use token anyway.
|
|
430
|
+
const { proceed, existing } = await guard({ cwd, mcpUrl: cfg.mcpUrl, yes: opts.yes });
|
|
431
|
+
if (!proceed)
|
|
432
|
+
return;
|
|
433
|
+
// Resolve (and if needed install) `retasc` BEFORE the token is spent, so a machine that
|
|
434
|
+
// cannot get a working launcher fails while the code is still redeemable.
|
|
435
|
+
const install = await chooseInstall(opts.install);
|
|
436
|
+
const launcher = resolveLauncher({ version: VERSION, install });
|
|
437
|
+
const note = launcherNote(launcher);
|
|
438
|
+
if (note)
|
|
439
|
+
console.log(note);
|
|
440
|
+
// RTSC-532 — the LEAF name, never the full path. It names the key in the Dash, so it
|
|
441
|
+
// has to be recognisable ("client-a → ENG") without publishing where on her disk it
|
|
442
|
+
// sits. `basename` of the cwd is exactly that.
|
|
443
|
+
const redeemed = await redeem(token.trim(), basename(cwd));
|
|
444
|
+
const workspaceId = existing?.workspaceId ?? newWorkspaceId();
|
|
445
|
+
(deps.bind ?? setBinding)(workspaceId, {
|
|
446
|
+
orgId: redeemed.orgId,
|
|
447
|
+
projectId: redeemed.projectId,
|
|
448
|
+
key: redeemed.key,
|
|
449
|
+
url: cfg.mcpUrl,
|
|
450
|
+
prefix: redeemed.prefix,
|
|
451
|
+
orgName: redeemed.orgName,
|
|
452
|
+
boundPath: cwd,
|
|
453
|
+
createdAt: Date.now(),
|
|
454
|
+
});
|
|
455
|
+
(deps.marker ?? installMarker)({ workspaceId, scope: "local", launcher });
|
|
456
|
+
// RTSC-532 — name the PATH, not "this folder". A binding is folder → project, and
|
|
457
|
+
// until this line said which folder, a wrong one was undetectable: the agent still
|
|
458
|
+
// calls in, so the Dash shows success while the folder she actually works in has no
|
|
459
|
+
// Retasc tools. Her agent relays this sentence to her, so it is also the last chance
|
|
460
|
+
// to notice.
|
|
461
|
+
console.log(`✓ ${clean(cwd)} is connected to ${clean(redeemed.orgName)} / ${clean(redeemed.prefix)}.`);
|
|
462
|
+
// NOT gated on isInteractive(), unlike the interactive path's copy of this line. There
|
|
463
|
+
// is never a TTY here, and this is the one instruction the agent has to pass on — the
|
|
464
|
+
// Dash waits on a tool call that cannot happen until the restart does (RTSC-496).
|
|
465
|
+
console.log(`\n${NEXT_STEP}`);
|
|
466
|
+
}
|
|
358
467
|
/**
|
|
359
468
|
* What to do with a workspace that is now set up.
|
|
360
469
|
*
|
package/dist/commands/import.js
CHANGED
|
@@ -312,25 +312,21 @@ function followProgress(orgId) {
|
|
|
312
312
|
stdout.write("\r\x1b[2K");
|
|
313
313
|
};
|
|
314
314
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
orgId = chosen.id;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
const orgLabel = clean(orgs.find((o) => o.id === orgId)?.name ?? "this org");
|
|
315
|
+
/**
|
|
316
|
+
* The whole import, from source to finished run, returning the project it landed in.
|
|
317
|
+
*
|
|
318
|
+
* RTSC-530 — split out of `importAction` so `retasc bind` can offer "bring your tracker
|
|
319
|
+
* across" as one of the ways a FIRST project comes into existence, the way the Dash's
|
|
320
|
+
* project step does. It calls this; it does not resemble it. Two copies of the auth prompt,
|
|
321
|
+
* target picker and column mapping is how the two drift, and the mapping is the part that
|
|
322
|
+
* decides where a whole backlog lands.
|
|
323
|
+
*
|
|
324
|
+
* Returns null when the human declines at the confirmation, so the caller can fall back
|
|
325
|
+
* rather than treat a deliberate "no" as a failure.
|
|
326
|
+
*/
|
|
327
|
+
export async function runImportFlow(opts) {
|
|
328
|
+
const { orgId, orgLabel } = opts;
|
|
329
|
+
// --- which tracker --------------------------------------------------------
|
|
334
330
|
// --- which tracker --------------------------------------------------------
|
|
335
331
|
const sources = (await api.listImportSources());
|
|
336
332
|
if (!sources.length)
|
|
@@ -343,7 +339,7 @@ export async function importAction(opts) {
|
|
|
343
339
|
console.log(`\nConnect to ${clean(src.label)}:`);
|
|
344
340
|
const auth = await collectAuth(src);
|
|
345
341
|
// --- which team/project/workspace -----------------------------------------
|
|
346
|
-
const { targets } = (await api.listImportTargets({ orgId
|
|
342
|
+
const { targets } = (await api.listImportTargets({ orgId, source: src.source, auth }));
|
|
347
343
|
if (!targets.length) {
|
|
348
344
|
cliError("NO_TARGETS", `That ${clean(src.label)} account has no ${clean(src.targetNoun)} we can import.`, "Check the credentials belong to the right account.");
|
|
349
345
|
}
|
|
@@ -355,13 +351,13 @@ export async function importAction(opts) {
|
|
|
355
351
|
let reviewerByStatus;
|
|
356
352
|
if (src.supportsStatusMapping) {
|
|
357
353
|
const { statuses } = (await api.listImportStatuses({
|
|
358
|
-
orgId
|
|
354
|
+
orgId,
|
|
359
355
|
source: src.source,
|
|
360
356
|
auth,
|
|
361
357
|
targetRef: target.id,
|
|
362
358
|
}));
|
|
363
359
|
if (statuses.length) {
|
|
364
|
-
const reviewers = (await api.listReviewCandidates({ orgId
|
|
360
|
+
const reviewers = (await api.listReviewCandidates({ orgId }));
|
|
365
361
|
const mapped = await mapStatuses(statuses, reviewers, ask);
|
|
366
362
|
statusMap = mapped.statusMap;
|
|
367
363
|
reviewerByStatus = Object.keys(mapped.reviewerByStatus).length
|
|
@@ -379,13 +375,13 @@ export async function importAction(opts) {
|
|
|
379
375
|
// gets re-run casually. Read from `importHistory` (permanent) rather than `latestImport`
|
|
380
376
|
// (live progress, swept after 24h), so this still fires for someone who imported last
|
|
381
377
|
// week and has been working in Retasc since — the person with the most to lose.
|
|
382
|
-
const history = (await api.importHistory({ orgId
|
|
378
|
+
const history = (await api.importHistory({ orgId }));
|
|
383
379
|
const warning = reimportWarning(history, src.source, src.label);
|
|
384
380
|
if (warning)
|
|
385
381
|
console.log(warning);
|
|
386
382
|
if (!opts.yes && !(await confirm("This can't be undone. Go ahead?"))) {
|
|
387
383
|
console.log("Nothing imported.");
|
|
388
|
-
return;
|
|
384
|
+
return null;
|
|
389
385
|
}
|
|
390
386
|
// --- run -------------------------------------------------------------------
|
|
391
387
|
console.log("\nImporting…\n");
|
|
@@ -396,7 +392,7 @@ export async function importAction(opts) {
|
|
|
396
392
|
let res;
|
|
397
393
|
try {
|
|
398
394
|
res = (await api.runImport({
|
|
399
|
-
orgId
|
|
395
|
+
orgId,
|
|
400
396
|
source: src.source,
|
|
401
397
|
auth,
|
|
402
398
|
target: { id: target.id, key: target.key, name: target.name },
|
|
@@ -429,4 +425,26 @@ export async function importAction(opts) {
|
|
|
429
425
|
console.log("");
|
|
430
426
|
const { identityLoop } = await import("./join.js");
|
|
431
427
|
await identityLoop(orgId, {}, orgLabel);
|
|
428
|
+
return { projectId: res.projectId, prefix: target.key };
|
|
429
|
+
}
|
|
430
|
+
/** `retasc import` — the standalone command. Resolves the org, then runs the flow above. */
|
|
431
|
+
export async function importAction(opts) {
|
|
432
|
+
if (!isInteractive() && !opts.yes) {
|
|
433
|
+
cliError("NEEDS_TERMINAL", "Importing asks what each of your columns means, so it needs a terminal.", "Run it interactively, or use the Dash.");
|
|
434
|
+
}
|
|
435
|
+
const me = (await api.me());
|
|
436
|
+
const orgs = me.orgs ?? [];
|
|
437
|
+
let orgId = opts.orgId;
|
|
438
|
+
if (!orgId) {
|
|
439
|
+
if (orgs.length === 0)
|
|
440
|
+
cliError("NO_ORG", "You're not a member of any org yet.");
|
|
441
|
+
else if (orgs.length === 1)
|
|
442
|
+
orgId = orgs[0].id;
|
|
443
|
+
else {
|
|
444
|
+
const chosen = await pickExisting("Import into which org", orgs, (o) => `${clean(o.name)}${o.slug ? ` (${clean(o.slug)})` : ""}`);
|
|
445
|
+
orgId = chosen.id;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const orgLabel = clean(orgs.find((o) => o.id === orgId)?.name ?? "this org");
|
|
449
|
+
await runImportFlow({ orgId: orgId, orgLabel, source: opts.source, yes: opts.yes });
|
|
432
450
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { loadConfig, patchConfig, saveConfig, configPath, isLoggedIn } from "./c
|
|
|
5
5
|
import { installMcp, normalizeScope } from "./commands/mcp.js";
|
|
6
6
|
import { installGate } from "./commands/gate.js";
|
|
7
7
|
import { claimAction } from "./commands/claim.js";
|
|
8
|
-
import { bindAction } from "./commands/bind.js";
|
|
8
|
+
import { bindAction, setupFromToken } from "./commands/bind.js";
|
|
9
9
|
import { joinAction } from "./commands/join.js";
|
|
10
10
|
import { identityAction } from "./commands/identity.js";
|
|
11
11
|
import { importAction } from "./commands/import.js";
|
|
@@ -183,7 +183,22 @@ program
|
|
|
183
183
|
// is present; this is how a scripted run, or a developer whose own agent runs setup,
|
|
184
184
|
// says no up front instead of being installed onto.
|
|
185
185
|
.option("--no-install", "Don't install `retasc` on this machine; wire the pinned npx launcher instead")
|
|
186
|
+
// RTSC-495 — the agent's door. Everything this command normally asks was already
|
|
187
|
+
// answered in the Dash, so the token stands in for all of it and nothing is prompted.
|
|
188
|
+
.option("--setup <token>", "Complete setup from a Dash setup code — no sign-in, no prompts")
|
|
186
189
|
.action(async (opts) => {
|
|
190
|
+
// BEFORE requireLogin: the whole point is a machine that has never signed in. The
|
|
191
|
+
// token is the authorization, and asking for a session here would refuse every
|
|
192
|
+
// caller this flag exists for.
|
|
193
|
+
//
|
|
194
|
+
// `!== undefined`, not truthiness: `--setup ""` is a stated intent to use a setup
|
|
195
|
+
// code, and it must fail as a bad code. Falling through to the interactive path
|
|
196
|
+
// would answer it with "Not signed in", which is the one diagnosis that sends the
|
|
197
|
+
// reader looking in exactly the wrong place.
|
|
198
|
+
if (opts.setup !== undefined) {
|
|
199
|
+
await setupFromToken(opts.setup, opts).catch(fail);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
187
202
|
requireLogin();
|
|
188
203
|
await bindAction(opts).catch(fail);
|
|
189
204
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retasc/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.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": {
|