@retasc/cli 1.13.1 → 1.15.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,73 @@ 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.15.0 (2026-08-02)
10
+
11
+ - **RTSC-524** — new command: `retasc import`. Bring a Linear, Jira, Asana, ClickUp or
12
+ Shortcut project across without opening a browser.
13
+
14
+ This was the last thing the terminal could not do. Everything else — sign in, make or join
15
+ an org, create a project, wire the folder, pull work — already worked end to end, but
16
+ importing meant stopping and finishing in the Dash.
17
+
18
+ ```
19
+ retasc import
20
+ retasc import --source jira --org-id …
21
+ ```
22
+
23
+ It walks you through the source, your credentials, which team or project to take, and then
24
+ **what each of your columns means**. That last part is the point, and it is asked rather
25
+ than guessed: a tool lets its users name their own columns, so the only honest way to know
26
+ what one means is to ask you. Pressing Enter accepts a suggestion derived from the
27
+ column's *type* in your tool, never its name — a column called "Rejected" that is really
28
+ an in-progress lane maps to `doing`, not `canceled`.
29
+
30
+ A column mapped to `review` needs a named reviewer, chosen per column, because two
31
+ "awaiting acceptance" columns can belong to different people. Columns holding work nobody
32
+ has started cannot be mapped to `review` at all, and the CLI says why rather than letting
33
+ the server refuse it later.
34
+
35
+ **Your source token is never an argument.** There is no `--token`, because anything passed
36
+ that way lands in shell history and in `ps` output. Secrets are typed with echo off, or
37
+ taken from `RETASC_IMPORT_<FIELD>` for scripted runs.
38
+
39
+ The run is confirmed before anything is written, and afterwards you are offered the
40
+ imported identity the migration just created for you, through the same prompt
41
+ `retasc join` uses.
42
+
43
+ ## 1.14.0 (2026-08-02)
44
+
45
+ - **RTSC-523** — setup asks before installing anything on your machine.
46
+
47
+ `retasc bind` and `retasc join` are one command you paste and answer, and somewhere in the
48
+ middle they ran `npm install -g @retasc/cli`. It said so, but it never asked. That is the
49
+ only step in a folder-scoped command that changes the machine rather than the folder, and
50
+ a shared npm prefix is exactly what plenty of developers keep clean.
51
+
52
+ ```
53
+ Install the retasc command on this machine?
54
+
55
+ 1) Yes, globally A `retasc` command you can use anywhere.
56
+ Best if you'll use Retasc in more than one project.
57
+ 2) No, run it on demand Nothing is installed. Your agent fetches it when it
58
+ starts, which adds a couple of seconds and needs a
59
+ network connection.
60
+ ```
61
+
62
+ Both answers leave the folder bound identically. Option 2 is not a per-project install:
63
+ nothing is written to the folder, so it needs no `package.json` and cannot fail for want
64
+ of an npm project. Your answer is remembered, because it is a question about the machine
65
+ and the machine has not changed by the time you bind a second folder.
66
+
67
+ You are only asked when it would actually happen. If `retasc` already works, there is
68
+ nothing to decide and nothing is said.
69
+
70
+ **Without a TTY it installs, exactly as before.** That is deliberate rather than a
71
+ leftover: when an agent runs setup on someone's behalf, that person never types `retasc`,
72
+ but their agent starts the MCP server every session, and the on-demand route would cost
73
+ them seconds and a network dependency every single time. `--no-install` on `bind` and
74
+ `join` declines without a TTY, for a developer whose own agent is doing the setup.
75
+
9
76
  ## 1.13.1 (2026-08-02)
10
77
 
11
78
  - **RTSC-522** — the changelog is public, and five releases that were never written down now
package/dist/api.js CHANGED
@@ -28,6 +28,13 @@ const fns = {
28
28
  // Imported-identity claim (RTSC-433/473), wired for `join` by RTSC-492 and shared with
29
29
  // RTSC-477's standalone `retasc identity`. Member-gated already — nothing was widened
30
30
  // server-side for the CLI, and nothing should be.
31
+ // RTSC-524 — the import surface. Owner-gated on the server, authenticated by the same
32
+ // user session every other management call uses.
33
+ listImportSources: makeFunctionReference("import:listImportSources"),
34
+ listImportTargets: makeFunctionReference("import:listImportTargets"),
35
+ listImportStatuses: makeFunctionReference("import:listImportStatuses"),
36
+ listReviewCandidates: makeFunctionReference("import:listReviewCandidates"),
37
+ runImport: makeFunctionReference("import:runImport"),
31
38
  claimableGhosts: makeFunctionReference("ghosts:claimableGhosts"),
32
39
  claimGhost: makeFunctionReference("ghosts:claimGhost"),
33
40
  dismissGhostPrompt: makeFunctionReference("ghosts:dismissGhostPrompt"),
@@ -159,6 +166,11 @@ export const api = {
159
166
  billingStatus: (args) => withAuth(() => client().query(fns.billingStatus, args)),
160
167
  chargeHistory: (args) => withAuth(() => client().query(fns.chargeHistory, args)),
161
168
  orgPayments: (args) => withAuth(() => client().action(fns.orgPayments, args)),
169
+ listImportSources: () => withAuth(() => client().query(fns.listImportSources, {})),
170
+ listImportTargets: (args) => withAuth(() => client().action(fns.listImportTargets, args)),
171
+ listImportStatuses: (args) => withAuth(() => client().action(fns.listImportStatuses, args)),
172
+ listReviewCandidates: (args) => withAuth(() => client().query(fns.listReviewCandidates, args)),
173
+ runImport: (args) => withAuth(() => client().action(fns.runImport, args)),
162
174
  claimableGhosts: (args) => withAuth(() => client().query(fns.claimableGhosts, args)),
163
175
  claimGhost: (args) => withAuth(() => client().mutation(fns.claimGhost, args)),
164
176
  dismissGhostPrompt: (args) => withAuth(() => client().mutation(fns.dismissGhostPrompt, args)),
@@ -1,10 +1,10 @@
1
1
  import { api, cliError } from "../api.js";
2
2
  import { deviceLogin } from "../auth.js";
3
- import { loadConfig } from "../config.js";
3
+ import { loadConfig, patchConfig } from "../config.js";
4
4
  import { installMarker } from "./mcp.js";
5
5
  import { readLocalBinding, resolveBinding } from "../lib/binding.js";
6
6
  import { getBinding, setBinding, newWorkspaceId } from "../lib/keystore.js";
7
- import { resolveLauncher, launcherNote, selfCommand } from "../lib/launcher.js";
7
+ import { resolveLauncher, launcherNote, runsOk, selfCommand } from "../lib/launcher.js";
8
8
  import { ask, confirm, isInteractive } from "../lib/prompt.js";
9
9
  import { clean } from "../lib/text.js";
10
10
  import { VERSION } from "../version.js";
@@ -151,6 +151,63 @@ export async function rebindGuard(args) {
151
151
  }
152
152
  return { proceed: false, existing };
153
153
  }
154
+ /**
155
+ * May we install `retasc` on this machine? (RTSC-523)
156
+ *
157
+ * Asked only when it would actually happen. `runsOk` is the same probe
158
+ * `resolveLauncher` opens with, so if `retasc` already works there is nothing to decide and
159
+ * a question would have exactly one possible outcome.
160
+ *
161
+ * A DEFAULT is right here, unlike the sign-in door (RTSC-508), which deliberately has none.
162
+ * A wrong answer costs a couple of seconds per agent start, not a second identity that
163
+ * cannot be merged back. The two questions look alike and are not.
164
+ *
165
+ * NO TTY ⇒ install, silently, exactly as before. That is not laziness about the default: it
166
+ * is the right answer for the person it affects most. The non-technical member of RTSC-497
167
+ * never opens a terminal — her AGENT runs this (RTSC-495/496), with no TTY. She will never
168
+ * type `retasc` herself, but her agent spawns the MCP server on every start, and the npx
169
+ * route would cost her seconds and a network dependency every single time, forever.
170
+ * `--no-install` exists for the developer who wants to decline without a TTY.
171
+ */
172
+ export async function chooseInstall(
173
+ /** `--no-install` ⇒ false. Undefined means "not stated". */
174
+ flag, deps = {}) {
175
+ if (flag === false)
176
+ return false;
177
+ const onPath = deps.onPath ?? (() => runsOk("retasc") !== null);
178
+ // Nothing to install, so nothing to ask.
179
+ if (onPath())
180
+ return true;
181
+ const remembered = (deps.remembered ?? (() => loadConfig().globalInstall))();
182
+ if (remembered !== undefined)
183
+ return remembered;
184
+ const interactive = deps.interactive ?? isInteractive;
185
+ if (!interactive())
186
+ return true;
187
+ console.log("\nInstall the retasc command on this machine?");
188
+ console.log(" 1) Yes, globally A `retasc` command you can use anywhere.");
189
+ console.log(" Best if you'll use Retasc in more than one project.");
190
+ console.log(" 2) No, run it on demand Nothing is installed. Your agent fetches it when it");
191
+ console.log(" starts, which adds a couple of seconds and needs a");
192
+ console.log(" network connection.");
193
+ const askFn = deps.askFn ?? ask;
194
+ for (let attempt = 0; attempt < 3; attempt++) {
195
+ const a = await askFn("Choose a number [1]: ");
196
+ // Either answer leaves this folder bound identically — only the machine differs — so
197
+ // an empty answer taking the default is safe here in a way it is not for the door.
198
+ if (a === "" || a === "1")
199
+ return remember(true);
200
+ if (a === "2")
201
+ return remember(false);
202
+ console.log("Please enter 1 or 2.");
203
+ }
204
+ throw new Error("no valid choice — aborting");
205
+ }
206
+ /** Record the answer, so binding a second folder does not re-ask about the same machine. */
207
+ function remember(globalInstall) {
208
+ patchConfig({ globalInstall });
209
+ return globalInstall;
210
+ }
154
211
  /**
155
212
  * Everything from "which project" to a working folder: pick the project, make `retasc`
156
213
  * durable, mint a key for that (org, project), write the binding, and wire the marker.
@@ -237,7 +294,15 @@ export async function completeWorkspaceSetup(args) {
237
294
  // name one proved to run on this machine. Resolved (and announced) here so the install
238
295
  // can't land after a key exists, and so every later message knows what to tell the user
239
296
  // to type.
240
- const launcher = resolveLauncher({ version: VERSION });
297
+ //
298
+ // RTSC-523 — and ASK first, because this is the one step in a folder-scoped command that
299
+ // changes the MACHINE. It used to just run `npm install -g`. The project already holds
300
+ // the opposite position everywhere else: `resolveLauncher` leaves a working `retasc`
301
+ // alone ("no surprise installs for someone who already manages their own"), and RTSC-520
302
+ // decided an update must ask. The first command a new developer runs was the one place
303
+ // we did it anyway.
304
+ const install = await chooseInstall(opts.install);
305
+ const launcher = resolveLauncher({ version: VERSION, install });
241
306
  const note = launcherNote(launcher);
242
307
  if (note)
243
308
  console.log(note);
@@ -0,0 +1,226 @@
1
+ import { stdin, stdout } from "node:process";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { api, cliError, formatError } from "../api.js";
4
+ import { clean } from "../lib/text.js";
5
+ import { ask, confirm, isInteractive } from "../lib/prompt.js";
6
+ import { pickExisting } from "./bind.js";
7
+ /** The five destinations a column can be mapped to. Mirrors `MappedStatus`. */
8
+ const MAPPED = ["todo", "doing", "review", "done", "canceled"];
9
+ /**
10
+ * May this column be mapped to `review`?
11
+ *
12
+ * Mirrors `reviewAllowedForGroup` in `convex/lib/importAdapter.ts`, and the reasoning is
13
+ * the server's: `review` means the work is FINISHED and waiting for someone to accept it,
14
+ * and it is non-terminal, so it keeps blocking dependents. Routing a not-yet-started column
15
+ * there manufactures review issues with nothing to review, each jamming whatever depends on
16
+ * it. Offering an option the server will reject is worse than not offering it.
17
+ */
18
+ export function reviewAllowed(group) {
19
+ return group === "active" || group === "done";
20
+ }
21
+ /** Read a secret without echoing it, so a token never lands in scrollback. */
22
+ async function askSecret(question) {
23
+ if (!stdin.isTTY)
24
+ return await ask(question);
25
+ const rl = createInterface({ input: stdin, output: stdout, terminal: true });
26
+ // readline has no built-in masking, so suppress the echo ourselves and restore it in
27
+ // `finally` — a thrown error mid-prompt must not leave the terminal silent.
28
+ const out = stdout;
29
+ const real = out.write.bind(out);
30
+ try {
31
+ const answer = rl.question(question);
32
+ out.write = (chunk) => (chunk.includes("\n") ? real(chunk) : true);
33
+ const value = await answer;
34
+ return value.trim();
35
+ }
36
+ finally {
37
+ out.write = real;
38
+ rl.close();
39
+ real("\n");
40
+ }
41
+ }
42
+ /**
43
+ * Collect the source's credentials.
44
+ *
45
+ * NEVER from an argument. A token passed as `--token …` lands in shell history, in `ps`
46
+ * output, and in any transcript of the session. Secret fields are read with echo off; an
47
+ * env var (`RETASC_IMPORT_<KEY>`) covers the scripted case without either problem.
48
+ */
49
+ async function collectAuth(src) {
50
+ const auth = {};
51
+ for (const f of src.authFields) {
52
+ const envKey = `RETASC_IMPORT_${f.key.toUpperCase()}`;
53
+ const fromEnv = process.env[envKey];
54
+ if (fromEnv) {
55
+ console.log(` ${clean(f.label)}: taken from ${envKey}`);
56
+ auth[f.key] = fromEnv;
57
+ continue;
58
+ }
59
+ if (!isInteractive()) {
60
+ cliError("MISSING_AUTH", `${clean(src.label)} needs ${clean(f.label)}, and there is no terminal to ask on.`, `Set ${envKey} and run again.`);
61
+ }
62
+ if (f.hint)
63
+ console.log(` ${clean(f.hint)}`);
64
+ const value = f.secret
65
+ ? await askSecret(` ${clean(f.label)} (hidden): `)
66
+ : await ask(` ${clean(f.label)}: `);
67
+ if (!value)
68
+ cliError("MISSING_AUTH", `${clean(f.label)} is required.`);
69
+ auth[f.key] = value;
70
+ }
71
+ return auth;
72
+ }
73
+ /**
74
+ * Walk every column and record what it means. The heart of the command.
75
+ *
76
+ * Each row arrives with a `suggested` value derived server-side from the source's own
77
+ * STRUCTURAL type, never from the column's name, so pressing Enter through the list is a
78
+ * defensible mapping rather than a guess. This is a review-and-correct surface, exactly as
79
+ * the Dash panel is.
80
+ */
81
+ export async function mapStatuses(statuses, reviewers, askFn) {
82
+ const statusMap = {};
83
+ const reviewerByStatus = {};
84
+ console.log(`\nWhat does each column mean? ${statuses.length} to confirm.\n` +
85
+ " Enter keeps the suggestion. It comes from the column's type in your tool, not its name.");
86
+ // The source's own order, so the list reads the way it does in the tool they know.
87
+ for (const s of [...statuses].sort((a, b) => a.order - b.order)) {
88
+ const allowed = MAPPED.filter((m) => m !== "review" || (reviewAllowed(s.group) && reviewers.length > 0));
89
+ const suggested = allowed.includes(s.suggested)
90
+ ? s.suggested
91
+ : "todo";
92
+ for (let attempt = 0;; attempt++) {
93
+ const answer = (await askFn(`\n ${clean(s.name)} [${suggested}] (${allowed.join(" / ")}): `)).trim().toLowerCase();
94
+ const choice = answer === "" ? suggested : answer;
95
+ if (allowed.includes(choice)) {
96
+ statusMap[s.id] = choice;
97
+ break;
98
+ }
99
+ if (choice === "review" && !reviewAllowed(s.group)) {
100
+ // Say WHY, rather than repeating the list. The server would reject it anyway.
101
+ console.log(" Not for this column: review means finished and awaiting acceptance,");
102
+ console.log(" and this one holds work nobody has started.");
103
+ }
104
+ else if (choice === "review") {
105
+ console.log(" No one in this org can be a reviewer yet, so review isn't available.");
106
+ }
107
+ else {
108
+ console.log(` Pick one of: ${allowed.join(", ")}`);
109
+ }
110
+ if (attempt >= 2)
111
+ throw new Error("no valid choice — aborting");
112
+ }
113
+ // A review column needs a named reviewer, per column rather than per run: two
114
+ // "awaiting acceptance" columns can legitimately belong to different people.
115
+ if (statusMap[s.id] === "review") {
116
+ const who = await pickExisting(` Who reviews "${clean(s.name)}"?`, reviewers, (m) => clean(m.name), askFn);
117
+ reviewerByStatus[s.id] = who.id;
118
+ }
119
+ }
120
+ return { statusMap, reviewerByStatus };
121
+ }
122
+ export async function importAction(opts) {
123
+ if (!isInteractive() && !opts.yes) {
124
+ cliError("NEEDS_TERMINAL", "Importing asks what each of your columns means, so it needs a terminal.", "Run it interactively, or use the Dash.");
125
+ }
126
+ // --- which org ------------------------------------------------------------
127
+ const me = (await api.me());
128
+ const orgs = me.orgs ?? [];
129
+ let orgId = opts.orgId;
130
+ if (!orgId) {
131
+ if (orgs.length === 0)
132
+ cliError("NO_ORG", "You're not a member of any org yet.");
133
+ else if (orgs.length === 1)
134
+ orgId = orgs[0].id;
135
+ else {
136
+ const chosen = await pickExisting("Import into which org", orgs, (o) => `${clean(o.name)}${o.slug ? ` (${clean(o.slug)})` : ""}`);
137
+ orgId = chosen.id;
138
+ }
139
+ }
140
+ const orgLabel = clean(orgs.find((o) => o.id === orgId)?.name ?? "this org");
141
+ // --- which tracker --------------------------------------------------------
142
+ const sources = (await api.listImportSources());
143
+ if (!sources.length)
144
+ cliError("NO_SOURCES", "No import sources are available.");
145
+ const src = opts.source
146
+ ? sources.find((s) => s.source === opts.source) ??
147
+ cliError("UNKNOWN_SOURCE", `There's no import source called "${clean(opts.source)}".`, `Try one of: ${sources.map((s) => s.source).join(", ")}`)
148
+ : await pickExisting("Bring work across from", sources, (s) => clean(s.label));
149
+ // --- credentials ----------------------------------------------------------
150
+ console.log(`\nConnect to ${clean(src.label)}:`);
151
+ const auth = await collectAuth(src);
152
+ // --- which team/project/workspace -----------------------------------------
153
+ const { targets } = (await api.listImportTargets({ orgId: orgId, source: src.source, auth }));
154
+ if (!targets.length) {
155
+ cliError("NO_TARGETS", `That ${clean(src.label)} account has no ${clean(src.targetNoun)} we can import.`, "Check the credentials belong to the right account.");
156
+ }
157
+ const target = targets.length === 1
158
+ ? targets[0]
159
+ : await pickExisting(`Which ${clean(src.targetNoun)}`, targets, (t) => `${clean(t.name)} (${clean(t.key)})`);
160
+ // --- what each column means ------------------------------------------------
161
+ let statusMap;
162
+ let reviewerByStatus;
163
+ if (src.supportsStatusMapping) {
164
+ const { statuses } = (await api.listImportStatuses({
165
+ orgId: orgId,
166
+ source: src.source,
167
+ auth,
168
+ targetRef: target.id,
169
+ }));
170
+ if (statuses.length) {
171
+ const reviewers = (await api.listReviewCandidates({ orgId: orgId }));
172
+ const mapped = await mapStatuses(statuses, reviewers, ask);
173
+ statusMap = mapped.statusMap;
174
+ reviewerByStatus = Object.keys(mapped.reviewerByStatus).length
175
+ ? mapped.reviewerByStatus
176
+ : undefined;
177
+ }
178
+ }
179
+ // --- confirm ---------------------------------------------------------------
180
+ // Named in full before anything is written. An import creates a project's worth of
181
+ // issues and mints a placeholder per source author, and there is no per-project delete —
182
+ // undoing a mistake means an org-level cleanup.
183
+ console.log(`\nAbout to import ${clean(target.name)} from ${clean(src.label)} into org ${orgLabel},\n` +
184
+ `as a new project. Issues, comments and their authors come across.`);
185
+ if (!opts.yes && !(await confirm("This can't be undone. Go ahead?"))) {
186
+ console.log("Nothing imported.");
187
+ return;
188
+ }
189
+ // --- run -------------------------------------------------------------------
190
+ console.log("\nImporting. This can take a few minutes on a big project…");
191
+ let res;
192
+ try {
193
+ res = (await api.runImport({
194
+ orgId: orgId,
195
+ source: src.source,
196
+ auth,
197
+ target: { id: target.id, key: target.key, name: target.name },
198
+ statusMap,
199
+ reviewerByStatus,
200
+ }));
201
+ }
202
+ catch (e) {
203
+ // The run is server-side, so a dead connection here does NOT mean a dead import.
204
+ // Saying "failed" would be a guess, and the wrong one sends someone re-importing on
205
+ // top of a run that is still writing.
206
+ const { code, message, hint } = formatError(e);
207
+ console.error(`\n✗ ${code ? `${code}: ` : ""}${message}`);
208
+ if (hint)
209
+ console.error(` → ${hint}`);
210
+ console.error(" If this was a connection problem the import may still be running.");
211
+ console.error(" Check the Dash before running it again.");
212
+ throw e;
213
+ }
214
+ console.log("\n✓ Imported.");
215
+ for (const [k, v] of Object.entries(res.summary ?? {})) {
216
+ if (typeof v === "number" || typeof v === "string")
217
+ console.log(` ${k.padEnd(18)}${clean(v)}`);
218
+ }
219
+ // The import just minted a placeholder for whoever authored that work, and the person
220
+ // who ran it is very often one of them. `identityLoop` is the same prompt `join` uses and
221
+ // is per-source since RTSC-507, so offering it here costs one round trip and saves them
222
+ // finding it later.
223
+ console.log("");
224
+ const { identityLoop } = await import("./join.js");
225
+ await identityLoop(orgId, {}, orgLabel);
226
+ }
package/dist/config.js CHANGED
@@ -61,6 +61,10 @@ export function loadConfig() {
61
61
  loginProvider: stored.loginProvider === "github" || stored.loginProvider === "google"
62
62
  ? stored.loginProvider
63
63
  : undefined,
64
+ // Same rule (RTSC-523): only a real boolean counts. Anything else reads as
65
+ // "never asked", so a mangled file leads to a question rather than to an
66
+ // install nobody agreed to.
67
+ globalInstall: typeof stored.globalInstall === "boolean" ? stored.globalInstall : undefined,
64
68
  };
65
69
  }
66
70
  /** Move a corrupt config aside to a unique sibling so a human can recover any
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { claimAction } from "./commands/claim.js";
8
8
  import { bindAction } from "./commands/bind.js";
9
9
  import { joinAction } from "./commands/join.js";
10
10
  import { identityAction } from "./commands/identity.js";
11
+ import { importAction } from "./commands/import.js";
11
12
  import { doctorAction } from "./commands/doctor.js";
12
13
  import { billingAction } from "./commands/billing.js";
13
14
  import { isNetworkError, readLocalBinding, resolveBinding } from "./lib/binding.js";
@@ -178,6 +179,10 @@ program
178
179
  .option("--agent <name>", "Agent member name (default: auto)")
179
180
  .option("--runtime <runtime>", "Agent runtime", "claude-code")
180
181
  .option("-y, --yes", "Don't prompt to confirm replacing an existing binding")
182
+ // RTSC-523 — decline the global install without a TTY. `bind`/`join` ask when a human
183
+ // is present; this is how a scripted run, or a developer whose own agent runs setup,
184
+ // says no up front instead of being installed onto.
185
+ .option("--no-install", "Don't install `retasc` on this machine; wire the pinned npx launcher instead")
181
186
  .action(async (opts) => {
182
187
  requireLogin();
183
188
  await bindAction(opts).catch(fail);
@@ -400,6 +405,10 @@ program
400
405
  // RTSC-477 — name the way back. `--yes` skips the identity question and must never answer
401
406
  // it, so the flag that causes the gap is the right place to say how to close it.
402
407
  .option("-y, --yes", "Don't prompt to replace an existing binding, and skip the identity question (ask it later with `retasc identity`)")
408
+ // RTSC-523 — decline the global install without a TTY. `bind`/`join` ask when a human
409
+ // is present; this is how a scripted run, or a developer whose own agent runs setup,
410
+ // says no up front instead of being installed onto.
411
+ .option("--no-install", "Don't install `retasc` on this machine; wire the pinned npx launcher instead")
403
412
  .allowExcessArguments(false)
404
413
  .action(async (link, opts) => {
405
414
  await joinAction(link, opts).catch(fail);
@@ -421,6 +430,24 @@ program
421
430
  requireLogin();
422
431
  await identityAction({ orgId: opts.orgId }).catch(fail);
423
432
  });
433
+ // RTSC-524 — bring another tracker across without leaving the terminal. The status
434
+ // mapping is ASKED, never guessed: a source lets its users name their own columns, so the
435
+ // only honest way to know what one means is the human running the import (the reasoning is
436
+ // recorded in convex/lib/importAdapter.ts, which deleted the name-based heuristics after a
437
+ // `Rejected` column imported live work as `canceled`, out of dispatch, silently).
438
+ program
439
+ .command("import")
440
+ .description("Bring a Linear/Jira/Asana/ClickUp/Shortcut project across into a new Retasc project.")
441
+ .option("--org-id <id>", "Which org to import into (defaults to your only one).")
442
+ .option("--source <source>", "Skip the source picker: linear | jira | asana | clickup | shortcut")
443
+ // No `--token`: a credential passed as an argument lands in shell history and in `ps`.
444
+ // Secrets are read with echo off, or from RETASC_IMPORT_<FIELD>.
445
+ .option("-y, --yes", "Skip the final confirmation (the column mapping is still asked)")
446
+ .allowExcessArguments(false)
447
+ .action(async (opts) => {
448
+ requireLogin();
449
+ await importAction({ orgId: opts.orgId, source: opts.source, yes: opts.yes }).catch(fail);
450
+ });
424
451
  // --- mcp wiring ------------------------------------------------------------
425
452
  const mcp = program.command("mcp").description("Wire the Retasc MCP server into your agent.");
426
453
  mcp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retasc/cli",
3
- "version": "1.13.1",
3
+ "version": "1.15.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": {