@retasc/cli 1.16.0 → 1.17.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,51 @@ 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.17.0 (2026-08-02)
10
+
11
+ - **RTSC-529** — the column mapping is a numbered picker, like every other prompt.
12
+
13
+ It was the only one that made you type a word. Picking an org, a project, a source, a
14
+ list, a reviewer, a sign-in door or whether to install globally are all numbered lists
15
+ reading `Choose a number` — and the reviewer picker appears two lines below this one, so a
16
+ single import taught two input styles within four lines of output.
17
+
18
+ ```
19
+ to do
20
+ 1) todo (suggested)
21
+ 2) doing
22
+ 3) done
23
+ 4) canceled
24
+ Choose a number [1]:
25
+ ```
26
+
27
+ Enter still takes the suggestion, now marked in the list rather than described beside it.
28
+ Typing the word still works for anyone who has learned the vocabulary; it just isn't what
29
+ the prompt advertises.
30
+
31
+ ## 1.16.1 (2026-08-02)
32
+
33
+ - **RTSC-528** — two things the first real `retasc import` run turned up.
34
+
35
+ **The progress bar never appeared.** It only started drawing once it had seen the run
36
+ reported as running, and a 15-issue import finishes before that is ever observed, so the
37
+ output went straight from `Importing…` to `✓ Imported.` with a silent gap. It now draws
38
+ the moment the run starts, sweeping while it waits for counts and switching to the real
39
+ bar once they arrive. Still silent without a terminal and under `NO_COLOR`.
40
+
41
+ **The column prompt didn't say what to do.** It read
42
+ `to do [todo] (todo / doing / done / canceled):`, where nothing is a verb, so the first
43
+ person to run it had to guess that you type one of the words. Now:
44
+
45
+ ```
46
+ to do
47
+ Enter to keep todo, or type: doing, done, canceled
48
+ >
49
+ ```
50
+
51
+ The suggestion is no longer repeated among the alternatives, which is what made the old
52
+ line read as four equal options behind a mysterious bracket.
53
+
9
54
  ## 1.16.0 (2026-08-02)
10
55
 
11
56
  - **RTSC-526** — `retasc import` catches up with the Dash on four things.
@@ -72,6 +72,19 @@ export function summaryLines(summary, label) {
72
72
  const LIME = "\x1b[38;5;154m";
73
73
  const DIM = "\x1b[38;5;240m";
74
74
  const RESET = "\x1b[0m";
75
+ /**
76
+ * An indeterminate sweep, for the stretch before the server reports counts (RTSC-528).
77
+ *
78
+ * Pure and frame-indexed rather than time-based, so a test can pin every frame. A block
79
+ * that travels is the honest shape when the total is unknown: a 0% bar reads as stalled,
80
+ * which is the very impression this is here to prevent.
81
+ */
82
+ export function sweep(frame, width = 24, color = true) {
83
+ const pos = frame % (width * 2 - 2);
84
+ const at = pos < width ? pos : width * 2 - 2 - pos; // bounce, so it never jumps
85
+ const cells = Array.from({ length: width }, (_, i) => Math.abs(i - at) <= 1 ? "\u2588" : "\u2591").join("");
86
+ return ` ${color ? LIME : ""}${cells}${color ? RESET : ""} working…`;
87
+ }
75
88
  /**
76
89
  * One line of progress bar.
77
90
  *
@@ -167,7 +180,8 @@ export async function mapStatuses(statuses, reviewers, askFn) {
167
180
  const statusMap = {};
168
181
  const reviewerByStatus = {};
169
182
  console.log(`\nWhat does each column mean? ${statuses.length} to confirm.\n` +
170
- " Enter keeps the suggestion. It comes from the column's type in your tool, not its name.");
183
+ " Press Enter to take the suggested answer, or pick another number.\n" +
184
+ " The suggestion comes from the column's type in your tool, not its name.");
171
185
  // Grouped under the source's own section names, in the source's own order within each
172
186
  // (RTSC-436/526). On a twenty-column Jira board a flat list is a wall; the sections are
173
187
  // the shape the human already has in front of them in their tool.
@@ -182,23 +196,44 @@ export async function mapStatuses(statuses, reviewers, askFn) {
182
196
  const suggested = allowed.includes(s.suggested)
183
197
  ? s.suggested
184
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;
185
217
  for (let attempt = 0;; attempt++) {
186
- const answer = (await askFn(`\n ${clean(s.name)} [${suggested}] (${allowed.join(" / ")}): `)).trim().toLowerCase();
187
- const choice = answer === "" ? suggested : answer;
188
- 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)) {
189
223
  statusMap[s.id] = choice;
190
224
  break;
191
225
  }
192
- if (choice === "review" && !reviewAllowed(s.group)) {
193
- // Say WHY, rather than repeating the list. The server would reject it anyway.
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)) {
194
229
  console.log(" Not for this column: review means finished and awaiting acceptance,");
195
230
  console.log(" and this one holds work nobody has started.");
196
231
  }
197
- else if (choice === "review") {
232
+ else if (answer === "review") {
198
233
  console.log(" No one in this org can be a reviewer yet, so review isn't available.");
199
234
  }
200
235
  else {
201
- console.log(` Pick one of: ${allowed.join(", ")}`);
236
+ console.log(` Enter a number between 1 and ${allowed.length}.`);
202
237
  }
203
238
  if (attempt >= 2)
204
239
  throw new Error("no valid choice — aborting");
@@ -224,28 +259,56 @@ export async function mapStatuses(statuses, reviewers, askFn) {
224
259
  * import failure.
225
260
  */
226
261
  function followProgress(orgId) {
227
- const tty = Boolean(stdout.isTTY) && !process.env.NO_COLOR;
228
262
  if (!stdout.isTTY)
229
263
  return () => { };
264
+ const color = !process.env.NO_COLOR;
230
265
  let stopped = false;
231
- const tick = async () => {
266
+ let frame = 0;
267
+ // Declared before the paint timer that reads it. Safe either way (the callback fires
268
+ // after this line runs), but reading a variable above its declaration is a trap to leave
269
+ // for the next person.
270
+ let latest = null;
271
+ // RTSC-528 — DRAW IMMEDIATELY, and keep drawing, rather than waiting to observe
272
+ // `status === "running"`.
273
+ //
274
+ // The first live run showed nothing at all: a 15-issue import is over in a couple of
275
+ // seconds, so the first poll landed before `runImport` had created the row and the second
276
+ // landed after it finished. `running` was never seen, so nothing was ever drawn — and the
277
+ // silent gap this exists to remove was exactly what the human got. A bar that only works
278
+ // on slow imports is a bar nobody sees while testing.
279
+ //
280
+ // So the render loop is independent of the data: it ticks on its own, showing an
281
+ // indeterminate sweep until counts arrive and the real bar once they do.
282
+ const draw = (done, total) => {
283
+ stdout.write(`\r\x1b[2K${done === null ? sweep(frame++, 24, color) : progressBar(done, total, 24, color)}`);
284
+ };
285
+ draw(null, null);
286
+ const paint = setInterval(() => {
287
+ if (!stopped && latest === null)
288
+ draw(null, null);
289
+ }, 120);
290
+ // Counts, whenever the server has them. Every failure is swallowed: this is decoration
291
+ // on a run happening regardless, and must never be what surfaces as an import failure.
292
+ const poll = async () => {
232
293
  while (!stopped) {
233
294
  try {
234
295
  const p = (await api.latestImport({ orgId }));
235
296
  if (!stopped && p && p.status === "running") {
236
- stdout.write(`\r\x1b[2K${progressBar(p.issuesDone ?? 0, p.issuesTotal ?? null, 24, tty)}`);
297
+ latest = { done: p.issuesDone ?? 0, total: p.issuesTotal ?? null };
298
+ draw(latest.done, latest.total);
237
299
  }
238
300
  }
239
301
  catch {
240
- /* progress is decoration; never let it speak for the run */
302
+ /* progress never speaks for the run */
241
303
  }
242
- await new Promise((r) => setTimeout(r, 1200));
304
+ await new Promise((r) => setTimeout(r, 700));
243
305
  }
244
306
  };
245
- void tick();
307
+ void poll();
246
308
  return () => {
247
309
  stopped = true;
248
- // Clear the line so the summary does not land on top of a half-drawn bar.
310
+ clearInterval(paint);
311
+ // Clear the line so the summary never lands on a half-drawn bar.
249
312
  stdout.write("\r\x1b[2K");
250
313
  };
251
314
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.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": {