@retasc/cli 1.16.1 → 1.18.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 +51 -0
- package/dist/commands/bind.js +69 -8
- package/dist/commands/import.js +72 -46
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,57 @@ 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.18.0 (2026-08-02)
|
|
10
|
+
|
|
11
|
+
- **RTSC-530** — setting up from scratch now asks where your work comes from, and imports it
|
|
12
|
+
in the same command.
|
|
13
|
+
|
|
14
|
+
`retasc bind` used to offer one thing at the project step: name a new project. So someone
|
|
15
|
+
arriving from Jira had to invent a project they didn't want, run `retasc import`
|
|
16
|
+
afterwards to get the one they did, and leave the empty one behind — and projects can't be
|
|
17
|
+
deleted individually.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Where does your work come from?
|
|
21
|
+
1) Linear
|
|
22
|
+
2) Jira
|
|
23
|
+
3) Asana
|
|
24
|
+
4) ClickUp
|
|
25
|
+
5) Shortcut
|
|
26
|
+
6) Start from scratch (name a project; your agents file into it)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Pick a tracker and the import runs right there, then the folder binds to the project it
|
|
30
|
+
created. Pick "start from scratch" and it behaves exactly as before. This is the same fork
|
|
31
|
+
the Dash has always offered; importing isn't a separate errand, it's one of the ways a
|
|
32
|
+
first project comes into existence.
|
|
33
|
+
|
|
34
|
+
Only when the org has no projects — binding a second folder in an existing org is still
|
|
35
|
+
just picking from the list. Declining at the import confirmation falls back to naming a
|
|
36
|
+
project rather than abandoning setup: you still asked to bind the folder.
|
|
37
|
+
|
|
38
|
+
## 1.17.0 (2026-08-02)
|
|
39
|
+
|
|
40
|
+
- **RTSC-529** — the column mapping is a numbered picker, like every other prompt.
|
|
41
|
+
|
|
42
|
+
It was the only one that made you type a word. Picking an org, a project, a source, a
|
|
43
|
+
list, a reviewer, a sign-in door or whether to install globally are all numbered lists
|
|
44
|
+
reading `Choose a number` — and the reviewer picker appears two lines below this one, so a
|
|
45
|
+
single import taught two input styles within four lines of output.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
to do
|
|
49
|
+
1) todo (suggested)
|
|
50
|
+
2) doing
|
|
51
|
+
3) done
|
|
52
|
+
4) canceled
|
|
53
|
+
Choose a number [1]:
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Enter still takes the suggestion, now marked in the list rather than described beside it.
|
|
57
|
+
Typing the word still works for anyone who has learned the vocabulary; it just isn't what
|
|
58
|
+
the prompt advertises.
|
|
59
|
+
|
|
9
60
|
## 1.16.1 (2026-08-02)
|
|
10
61
|
|
|
11
62
|
- **RTSC-528** — two things the first real `retasc import` run turned up.
|
package/dist/commands/bind.js
CHANGED
|
@@ -208,6 +208,59 @@ function remember(globalInstall) {
|
|
|
208
208
|
patchConfig({ globalInstall });
|
|
209
209
|
return globalInstall;
|
|
210
210
|
}
|
|
211
|
+
/** Name and create a project by hand — the "start from scratch" ending. */
|
|
212
|
+
async function nameAProject(orgId) {
|
|
213
|
+
const name = await ask("New project name: ");
|
|
214
|
+
const pfx = (await ask("Project prefix (e.g. ACME): ")).toUpperCase();
|
|
215
|
+
if (!name || !pfx)
|
|
216
|
+
throw new Error("project name and prefix required");
|
|
217
|
+
const p = (await api.createProject({ orgId, name, prefix: pfx }));
|
|
218
|
+
console.log(`✓ Created project ${p.prefix}.`);
|
|
219
|
+
return { projectId: p.projectId, prefix: p.prefix };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* "Where does your work come from?" — the first project, for an org that has none.
|
|
223
|
+
*
|
|
224
|
+
* RTSC-530. Mirrors the Dash's `ProjectSetup`: the trackers first, then "start from
|
|
225
|
+
* scratch" as the last row rather than a footer escape hatch. The Dash records why it is a
|
|
226
|
+
* ROW — as a button it read as a way out of the five real options, which is the wrong
|
|
227
|
+
* framing when most people arriving have work to bring.
|
|
228
|
+
*
|
|
229
|
+
* The import flow is CALLED, not re-implemented (`runImportFlow`). A second copy of the
|
|
230
|
+
* auth prompt, target picker and column mapping is how the two drift, and the mapping is
|
|
231
|
+
* the part that decides where a whole backlog lands.
|
|
232
|
+
*
|
|
233
|
+
* Imported dynamically: `commands/import.ts` imports `pickExisting` from here, so a static
|
|
234
|
+
* import would close a cycle. Same pattern `join` uses for `identityLoop`.
|
|
235
|
+
*/
|
|
236
|
+
async function firstProject(orgId, orgLabel) {
|
|
237
|
+
let sources = [];
|
|
238
|
+
try {
|
|
239
|
+
sources = (await api.listImportSources());
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Never block setting up a workspace on the importer list being reachable. A person
|
|
243
|
+
// who just wants an empty project should not be stopped by a source catalogue.
|
|
244
|
+
sources = [];
|
|
245
|
+
}
|
|
246
|
+
if (!sources.length)
|
|
247
|
+
return nameAProject(orgId);
|
|
248
|
+
const SCRATCH = { source: "", label: "Start from scratch (name a project; your agents file into it)" };
|
|
249
|
+
const picked = await pickExisting("Where does your work come from?", [...sources, SCRATCH], (x) => clean(x.label));
|
|
250
|
+
// Compared by REFERENCE, so a source that ever arrives with an empty id cannot be
|
|
251
|
+
// mistaken for the scratch row.
|
|
252
|
+
if (picked === SCRATCH)
|
|
253
|
+
return nameAProject(orgId);
|
|
254
|
+
const { runImportFlow } = await import("./import.js");
|
|
255
|
+
const done = await runImportFlow({ orgId, orgLabel, source: picked.source });
|
|
256
|
+
// Declining at the import confirmation is not an error, and must not abandon setup —
|
|
257
|
+
// they still asked to bind this folder. Fall back to the other branch of the same fork.
|
|
258
|
+
if (!done) {
|
|
259
|
+
console.log("\nNo import. Let's make an empty project instead.");
|
|
260
|
+
return nameAProject(orgId);
|
|
261
|
+
}
|
|
262
|
+
return done;
|
|
263
|
+
}
|
|
211
264
|
/**
|
|
212
265
|
* Everything from "which project" to a working folder: pick the project, make `retasc`
|
|
213
266
|
* durable, mint a key for that (org, project), write the binding, and wire the marker.
|
|
@@ -264,6 +317,19 @@ export async function completeWorkspaceSetup(args) {
|
|
|
264
317
|
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
318
|
}
|
|
266
319
|
}
|
|
320
|
+
else if (isInteractive() && list.length === 0) {
|
|
321
|
+
// RTSC-530 — the FROM-SCRATCH case, and the Dash's question rather than ours.
|
|
322
|
+
//
|
|
323
|
+
// `Onboarding.tsx` asks "Where does your work come from?" and offers the five
|
|
324
|
+
// trackers AND "start from scratch" as one fork, because importing is not a separate
|
|
325
|
+
// act: it is one of the ways a first project comes into existence. The CLI used to
|
|
326
|
+
// ask only "New project name:", so someone arriving from Jira had to invent a project
|
|
327
|
+
// they did not want, run `retasc import` afterwards, and leave an empty one behind
|
|
328
|
+
// that cannot be deleted (org-granularity delete only).
|
|
329
|
+
const made = await firstProject(orgId, org);
|
|
330
|
+
projectId = made.projectId;
|
|
331
|
+
prefix = made.prefix;
|
|
332
|
+
}
|
|
267
333
|
else if (isInteractive()) {
|
|
268
334
|
const chosen = await pick("Select a project", list, (p) => `${clean(p.prefix)} — ${clean(p.name)}`);
|
|
269
335
|
if (chosen) {
|
|
@@ -271,14 +337,9 @@ export async function completeWorkspaceSetup(args) {
|
|
|
271
337
|
prefix = chosen.prefix;
|
|
272
338
|
}
|
|
273
339
|
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}.`);
|
|
340
|
+
const made = await nameAProject(orgId);
|
|
341
|
+
projectId = made.projectId;
|
|
342
|
+
prefix = made.prefix;
|
|
282
343
|
}
|
|
283
344
|
}
|
|
284
345
|
else if (list.length === 1) {
|
package/dist/commands/import.js
CHANGED
|
@@ -180,7 +180,7 @@ export async function mapStatuses(statuses, reviewers, askFn) {
|
|
|
180
180
|
const statusMap = {};
|
|
181
181
|
const reviewerByStatus = {};
|
|
182
182
|
console.log(`\nWhat does each column mean? ${statuses.length} to confirm.\n` +
|
|
183
|
-
" Press Enter to
|
|
183
|
+
" Press Enter to take the suggested answer, or pick another number.\n" +
|
|
184
184
|
" The suggestion comes from the column's type in your tool, not its name.");
|
|
185
185
|
// Grouped under the source's own section names, in the source's own order within each
|
|
186
186
|
// (RTSC-436/526). On a twenty-column Jira board a flat list is a wall; the sections are
|
|
@@ -196,36 +196,44 @@ export async function mapStatuses(statuses, reviewers, askFn) {
|
|
|
196
196
|
const suggested = allowed.includes(s.suggested)
|
|
197
197
|
? s.suggested
|
|
198
198
|
: "todo";
|
|
199
|
+
// RTSC-529 — NUMBERED, like every other prompt in the CLI.
|
|
200
|
+
//
|
|
201
|
+
// This was the only one that asked you to type a word. Org, project, source, list,
|
|
202
|
+
// reviewer, sign-in door (RTSC-508) and install choice (RTSC-523) are all numbered
|
|
203
|
+
// pickers reading "Choose a number" — and the reviewer picker appears two lines below
|
|
204
|
+
// THIS one, so a single run taught two input styles within four lines of output.
|
|
205
|
+
//
|
|
206
|
+
// RTSC-528 tried to fix that by making the typing instruction explicit. That treated
|
|
207
|
+
// the symptom: a prompt that has to teach you its own input method is usually the
|
|
208
|
+
// wrong input method.
|
|
209
|
+
//
|
|
210
|
+
// The default is marked in the list rather than described outside it, and the words
|
|
211
|
+
// are still accepted — someone ten columns in knows the vocabulary, and typing `doing`
|
|
212
|
+
// beats re-reading a list. That is a shortcut, not the advertised path, so it is not
|
|
213
|
+
// in the instruction.
|
|
214
|
+
console.log(`\n ${clean(s.name)}`);
|
|
215
|
+
allowed.forEach((m, i) => console.log(` ${i + 1}) ${m}${m === suggested ? " (suggested)" : ""}`));
|
|
216
|
+
const defaultSlot = allowed.indexOf(suggested) + 1;
|
|
199
217
|
for (let attempt = 0;; attempt++) {
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// which made the contrast worse.
|
|
206
|
-
//
|
|
207
|
-
// The suggestion is also removed from the alternatives — it is already what Enter
|
|
208
|
-
// does, and listing it again is what made the line read as four equal options with a
|
|
209
|
-
// mysterious bracket in front.
|
|
210
|
-
const others = allowed.filter((m) => m !== suggested);
|
|
211
|
-
const answer = (await askFn(`\n ${clean(s.name)}\n` +
|
|
212
|
-
` Enter to keep ${suggested}, or type: ${others.join(", ")}\n` +
|
|
213
|
-
` > `)).trim().toLowerCase();
|
|
214
|
-
const choice = answer === "" ? suggested : answer;
|
|
215
|
-
if (allowed.includes(choice)) {
|
|
218
|
+
const answer = (await askFn(` Choose a number [${defaultSlot}]: `)).trim().toLowerCase();
|
|
219
|
+
// Enter takes the suggestion; a digit picks a row; a word still works.
|
|
220
|
+
const byNumber = /^\d+$/.test(answer) ? allowed[Number(answer) - 1] : undefined;
|
|
221
|
+
const choice = answer === "" ? suggested : (byNumber ?? answer);
|
|
222
|
+
if (choice && allowed.includes(choice)) {
|
|
216
223
|
statusMap[s.id] = choice;
|
|
217
224
|
break;
|
|
218
225
|
}
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
// Say WHY when the answer names something real but not permitted here. The server
|
|
227
|
+
// would reject it anyway, and repeating the list explains nothing.
|
|
228
|
+
if (answer === "review" && !reviewAllowed(s.group)) {
|
|
221
229
|
console.log(" Not for this column: review means finished and awaiting acceptance,");
|
|
222
230
|
console.log(" and this one holds work nobody has started.");
|
|
223
231
|
}
|
|
224
|
-
else if (
|
|
232
|
+
else if (answer === "review") {
|
|
225
233
|
console.log(" No one in this org can be a reviewer yet, so review isn't available.");
|
|
226
234
|
}
|
|
227
235
|
else {
|
|
228
|
-
console.log(`
|
|
236
|
+
console.log(` Enter a number between 1 and ${allowed.length}.`);
|
|
229
237
|
}
|
|
230
238
|
if (attempt >= 2)
|
|
231
239
|
throw new Error("no valid choice — aborting");
|
|
@@ -304,25 +312,21 @@ function followProgress(orgId) {
|
|
|
304
312
|
stdout.write("\r\x1b[2K");
|
|
305
313
|
};
|
|
306
314
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
orgId = chosen.id;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
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 --------------------------------------------------------
|
|
326
330
|
// --- which tracker --------------------------------------------------------
|
|
327
331
|
const sources = (await api.listImportSources());
|
|
328
332
|
if (!sources.length)
|
|
@@ -335,7 +339,7 @@ export async function importAction(opts) {
|
|
|
335
339
|
console.log(`\nConnect to ${clean(src.label)}:`);
|
|
336
340
|
const auth = await collectAuth(src);
|
|
337
341
|
// --- which team/project/workspace -----------------------------------------
|
|
338
|
-
const { targets } = (await api.listImportTargets({ orgId
|
|
342
|
+
const { targets } = (await api.listImportTargets({ orgId, source: src.source, auth }));
|
|
339
343
|
if (!targets.length) {
|
|
340
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.");
|
|
341
345
|
}
|
|
@@ -347,13 +351,13 @@ export async function importAction(opts) {
|
|
|
347
351
|
let reviewerByStatus;
|
|
348
352
|
if (src.supportsStatusMapping) {
|
|
349
353
|
const { statuses } = (await api.listImportStatuses({
|
|
350
|
-
orgId
|
|
354
|
+
orgId,
|
|
351
355
|
source: src.source,
|
|
352
356
|
auth,
|
|
353
357
|
targetRef: target.id,
|
|
354
358
|
}));
|
|
355
359
|
if (statuses.length) {
|
|
356
|
-
const reviewers = (await api.listReviewCandidates({ orgId
|
|
360
|
+
const reviewers = (await api.listReviewCandidates({ orgId }));
|
|
357
361
|
const mapped = await mapStatuses(statuses, reviewers, ask);
|
|
358
362
|
statusMap = mapped.statusMap;
|
|
359
363
|
reviewerByStatus = Object.keys(mapped.reviewerByStatus).length
|
|
@@ -371,13 +375,13 @@ export async function importAction(opts) {
|
|
|
371
375
|
// gets re-run casually. Read from `importHistory` (permanent) rather than `latestImport`
|
|
372
376
|
// (live progress, swept after 24h), so this still fires for someone who imported last
|
|
373
377
|
// week and has been working in Retasc since — the person with the most to lose.
|
|
374
|
-
const history = (await api.importHistory({ orgId
|
|
378
|
+
const history = (await api.importHistory({ orgId }));
|
|
375
379
|
const warning = reimportWarning(history, src.source, src.label);
|
|
376
380
|
if (warning)
|
|
377
381
|
console.log(warning);
|
|
378
382
|
if (!opts.yes && !(await confirm("This can't be undone. Go ahead?"))) {
|
|
379
383
|
console.log("Nothing imported.");
|
|
380
|
-
return;
|
|
384
|
+
return null;
|
|
381
385
|
}
|
|
382
386
|
// --- run -------------------------------------------------------------------
|
|
383
387
|
console.log("\nImporting…\n");
|
|
@@ -388,7 +392,7 @@ export async function importAction(opts) {
|
|
|
388
392
|
let res;
|
|
389
393
|
try {
|
|
390
394
|
res = (await api.runImport({
|
|
391
|
-
orgId
|
|
395
|
+
orgId,
|
|
392
396
|
source: src.source,
|
|
393
397
|
auth,
|
|
394
398
|
target: { id: target.id, key: target.key, name: target.name },
|
|
@@ -421,4 +425,26 @@ export async function importAction(opts) {
|
|
|
421
425
|
console.log("");
|
|
422
426
|
const { identityLoop } = await import("./join.js");
|
|
423
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 });
|
|
424
450
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retasc/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.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": {
|