@retasc/cli 1.20.0 → 1.21.1
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 +21 -1
- package/README.md +1 -1
- package/dist/commands/gate.js +75 -0
- package/dist/commands/import.js +10 -3
- package/dist/index.js +18 -7
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ 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.21.1 (2026-08-10)
|
|
10
|
+
|
|
11
|
+
- **RTSC-643** — `retasc gate install` keys the gate to **this folder's** project, not the
|
|
12
|
+
machine-wide default. It used to read `defaultProjectPrefix` (stamped by whichever project
|
|
13
|
+
you last ran `retasc init` for), so in a bound folder it could install a commit gate keyed
|
|
14
|
+
to a different project — rejecting every valid commit — while reporting success. It now asks
|
|
15
|
+
the folder's own binding first (`whoami` over the workspace key, or the keystore's cached
|
|
16
|
+
prefix offline; a subdirectory run checks the git toplevel too), prints where the prefix
|
|
17
|
+
came from, warns when the global default disagrees, and only uses the global default when
|
|
18
|
+
the folder is genuinely unbound. Bound-but-unresolvable fails loudly instead of guessing.
|
|
19
|
+
|
|
20
|
+
## 1.21.0 (2026-08-07)
|
|
21
|
+
|
|
22
|
+
- **RTSC-527** — the re-import warning dates the last import in **your** timezone. The day
|
|
23
|
+
was formatted from `toISOString()`, which rolls back one east of Greenwich: at UTC+7 an
|
|
24
|
+
import made five hours ago read as yesterday's, which is the opposite of useful for a line
|
|
25
|
+
whose job is to say how long ago it was. The Dash gained the same date in the same shape
|
|
26
|
+
in this release, and moved onto the same durable per-source history the CLI already read,
|
|
27
|
+
so the two surfaces now describe one import the same way.
|
|
28
|
+
|
|
9
29
|
## 1.20.0 (2026-08-04)
|
|
10
30
|
|
|
11
31
|
- **RTSC-561** — the org gained an **admin** role, and the CLI stopped refusing it.
|
|
@@ -536,7 +556,7 @@ there is no TTY to run one in.
|
|
|
536
556
|
|
|
537
557
|
- **RTSC-263** — owner/member permission denials now print a readable reason and the
|
|
538
558
|
next step instead of an opaque `Server Error`. Hitting an owner-only command as a
|
|
539
|
-
member reports "Owner role required." with "Ask
|
|
559
|
+
member reports "Owner role required." with "Ask `<owner>` to create a project." on its
|
|
540
560
|
own line. The backend change does the work; the CLI already read the structured
|
|
541
561
|
payload (1.5.0), so this release carries only the hardening below.
|
|
542
562
|
- **RTSC-263** — `formatError` now strips U+0085, U+2028 and U+2029 in addition to the
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# retasc
|
|
2
2
|
|
|
3
|
-
**The issue tracker
|
|
3
|
+
**The issue tracker that hands your backlog to AI agents.**
|
|
4
4
|
|
|
5
5
|
Over [MCP](https://modelcontextprotocol.io), a heterogeneous fleet of agents atomically
|
|
6
6
|
claims unblocked, prioritized tasks and runs in parallel — server-enforced, no collisions.
|
package/dist/commands/gate.js
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { mkdirSync, writeFileSync, existsSync, chmodSync } from "node:fs";
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
|
+
import { resolveMcpConn, readMcpJson, workspacePrefix } from "../lib/claim.js";
|
|
5
|
+
import { claudeLocalRetascEntry } from "../lib/binding.js";
|
|
6
|
+
import { getBinding } from "../lib/keystore.js";
|
|
7
|
+
/** The keystore's stored prefix for the workspace marker at `dir`, if any —
|
|
8
|
+
* the offline fallback when the server can't be asked. Entry selection matches
|
|
9
|
+
* resolveConn: env workspace id first, then claude-local, then the folder
|
|
10
|
+
* marker. The cache answers ONLY for the key the live path would have asked
|
|
11
|
+
* (`b.key === conn.key`) — an env-selected key from a different org must not
|
|
12
|
+
* inherit this folder's cached prefix. */
|
|
13
|
+
function storedPrefix(dir, conn, env = process.env) {
|
|
14
|
+
const entry = claudeLocalRetascEntry(dir) ?? readMcpJson(dir)?.mcpServers?.retasc;
|
|
15
|
+
const wsId = env.RETASC_WORKSPACE || entry?.env?.RETASC_WORKSPACE;
|
|
16
|
+
const b = wsId ? getBinding(String(wsId)) : undefined;
|
|
17
|
+
if (!b || b.key !== conn.key)
|
|
18
|
+
return undefined;
|
|
19
|
+
return typeof b.prefix === "string" && b.prefix ? b.prefix : undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Server/keystore-derived text is validated BEFORE it's printed or trusted
|
|
22
|
+
* (same rule as whoami's control-char strip) — a malformed value is treated
|
|
23
|
+
* as unresolved, never echoed to the terminal. */
|
|
24
|
+
function validPrefix(p) {
|
|
25
|
+
if (typeof p !== "string")
|
|
26
|
+
return undefined;
|
|
27
|
+
const up = p.toUpperCase();
|
|
28
|
+
return PREFIX_RE.test(up) ? up : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** whoami must never hang gate install — same 10s bound as resolveBinding
|
|
31
|
+
* (binding.ts), which bare global fetch does not carry. */
|
|
32
|
+
const fetchWithTimeout = (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(10_000) });
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the prefix the gate is keyed to, folder-first (RTSC-643). The global
|
|
35
|
+
* `defaultProjectPrefix` is written only by `retasc init` — it's "whatever
|
|
36
|
+
* project you last init-ed", which in a bound folder can be a DIFFERENT
|
|
37
|
+
* workspace's prefix. So the folder's own binding is authoritative (whoami over
|
|
38
|
+
* the workspace's key, or the keystore's cached prefix offline), and the global
|
|
39
|
+
* default is consulted only when the folder is unbound. A bound folder whose
|
|
40
|
+
* prefix can't be resolved fails loudly rather than guessing: a gate keyed to
|
|
41
|
+
* the wrong project rejects every valid commit and ships to CI.
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveGatePrefix(opts) {
|
|
44
|
+
// `!== undefined` so `--prefix ""` (an unset shell var) still reaches
|
|
45
|
+
// installGate's validator and fails loudly instead of silently resolving.
|
|
46
|
+
if (opts.flag !== undefined)
|
|
47
|
+
return { prefix: opts.flag.toUpperCase(), source: "--prefix" };
|
|
48
|
+
const cfgDefault = opts.defaultPrefix?.toUpperCase();
|
|
49
|
+
// The gate is written at the git toplevel, so when the cwd (a subdirectory)
|
|
50
|
+
// carries no binding, look at the toplevel too — otherwise a subdir run would
|
|
51
|
+
// silently fall back to the global default behind a false "isn't bound".
|
|
52
|
+
const dirs = opts.dir
|
|
53
|
+
? [opts.dir]
|
|
54
|
+
: Array.from(new Set([process.cwd(), repoRoot()].filter((d) => !!d)));
|
|
55
|
+
let conn;
|
|
56
|
+
let boundDir = dirs[0];
|
|
57
|
+
for (const d of dirs) {
|
|
58
|
+
const c = resolveMcpConn({ env: opts.env, mcpJson: readMcpJson(d), dir: d });
|
|
59
|
+
if (c.key) {
|
|
60
|
+
conn = c;
|
|
61
|
+
boundDir = d;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (conn) {
|
|
66
|
+
const live = validPrefix(await workspacePrefix(conn, opts.fetchImpl ?? fetchWithTimeout));
|
|
67
|
+
const prefix = live ?? validPrefix(storedPrefix(boundDir, conn, opts.env));
|
|
68
|
+
if (prefix) {
|
|
69
|
+
const source = live ? "workspace binding" : "workspace binding (cached)";
|
|
70
|
+
const shadowed = cfgDefault && cfgDefault !== prefix ? cfgDefault : undefined;
|
|
71
|
+
return shadowed ? { prefix, source, shadowedDefault: shadowed } : { prefix, source };
|
|
72
|
+
}
|
|
73
|
+
throw new Error("this folder is bound, but its project prefix couldn't be resolved (server didn't answer, no cached prefix) — pass --prefix <PREFIX>.");
|
|
74
|
+
}
|
|
75
|
+
if (cfgDefault)
|
|
76
|
+
return { prefix: cfgDefault, source: "global config" };
|
|
77
|
+
throw new Error("no project prefix — pass --prefix <PREFIX>, or bind this folder (`retasc bind`) / run `retasc init` so it's resolved from your project.");
|
|
78
|
+
}
|
|
4
79
|
// Mirror the server's project-prefix rule (convex/manage.ts PREFIX_RE): 2–10
|
|
5
80
|
// chars, A–Z/0–9, letter-first. The prefix is interpolated into a generated
|
|
6
81
|
// bash hook and a YAML grep, so validating here keeps a stray value from
|
package/dist/commands/import.js
CHANGED
|
@@ -18,6 +18,11 @@ const GROUP_LABEL = {
|
|
|
18
18
|
};
|
|
19
19
|
/** Section order, matching the Dash. */
|
|
20
20
|
const GROUP_ORDER = ["not_started", "active", "done", "closed"];
|
|
21
|
+
/** `YYYY-MM-DD` in the local zone. Mirrored in `dash/src/lib/importGating.ts`. */
|
|
22
|
+
function localDay(d) {
|
|
23
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
24
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
25
|
+
}
|
|
21
26
|
/**
|
|
22
27
|
* What to say before a SECOND import into the same org (RTSC-526).
|
|
23
28
|
*
|
|
@@ -36,9 +41,11 @@ export function reimportWarning(history, source, label) {
|
|
|
36
41
|
const prior = history.find((h) => h.source === source);
|
|
37
42
|
if (!prior)
|
|
38
43
|
return null;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
// The reader's day, not UTC (RTSC-527). `toISOString().slice(0, 10)` was the original
|
|
45
|
+
// and it misdates by one: at UTC+7 an import made before 07:00 local prints as
|
|
46
|
+
// yesterday, which is the opposite of useful for a line whose whole job is "how long
|
|
47
|
+
// ago". The Dash's `importDay` formats identically, so one import still gets one date.
|
|
48
|
+
const when = prior.lastImportedAt ? ` (last on ${localDay(new Date(prior.lastImportedAt))})` : "";
|
|
42
49
|
return (`\n! You've imported from ${clean(label)} into this org before${when}.\n` +
|
|
43
50
|
` Re-importing re-syncs those issues, so any edits you made in Retasc to them\n` +
|
|
44
51
|
` (status, labels, and so on) will be replaced by ${clean(label)}'s version.`);
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from "commander";
|
|
|
3
3
|
import { VERSION } from "./version.js";
|
|
4
4
|
import { loadConfig, patchConfig, saveConfig, configPath, isLoggedIn } from "./config.js";
|
|
5
5
|
import { installMcp, normalizeScope } from "./commands/mcp.js";
|
|
6
|
-
import { installGate } from "./commands/gate.js";
|
|
6
|
+
import { installGate, resolveGatePrefix } from "./commands/gate.js";
|
|
7
7
|
import { claimAction } from "./commands/claim.js";
|
|
8
8
|
import { bindAction, setupFromToken } from "./commands/bind.js";
|
|
9
9
|
import { joinAction } from "./commands/join.js";
|
|
@@ -491,16 +491,27 @@ const gate = program.command("gate").description("Wire the commit↔issue tracea
|
|
|
491
491
|
gate
|
|
492
492
|
.command("install")
|
|
493
493
|
.description("Install a prefix-correct commit-msg hook + check-commit-message Action into this repo.")
|
|
494
|
-
.option("--prefix <PREFIX>", "Project prefix to enforce (default:
|
|
494
|
+
.option("--prefix <PREFIX>", "Project prefix to enforce (default: this folder's bound project)")
|
|
495
495
|
.option("--no-hook", "Skip the local commit-msg hook (CI Action only)")
|
|
496
496
|
.option("--no-action", "Skip the GitHub Action (local hook only)")
|
|
497
|
-
.action((opts) => {
|
|
497
|
+
.action(async (opts) => {
|
|
498
498
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
499
|
+
// RTSC-643: folder-first — the binding is the authoritative identity; the
|
|
500
|
+
// global default (stamped by the last `retasc init`) applies only when the
|
|
501
|
+
// folder is unbound.
|
|
502
|
+
const r = await resolveGatePrefix({
|
|
503
|
+
flag: opts.prefix,
|
|
504
|
+
defaultPrefix: loadConfig().defaultProjectPrefix,
|
|
505
|
+
});
|
|
506
|
+
if (r.source !== "--prefix")
|
|
507
|
+
console.log(`Prefix ${r.prefix} — from ${r.source}.`);
|
|
508
|
+
if (r.source === "global config") {
|
|
509
|
+
console.log(" (This folder isn't bound — `retasc bind` makes the prefix folder-scoped.)");
|
|
510
|
+
}
|
|
511
|
+
if (r.shadowedDefault) {
|
|
512
|
+
console.log(` ⚠ Ignoring global default prefix ${r.shadowedDefault} — this folder's binding wins.`);
|
|
502
513
|
}
|
|
503
|
-
installGate({ prefix, layers: { hook: opts.hook, action: opts.action } });
|
|
514
|
+
installGate({ prefix: r.prefix, layers: { hook: opts.hook, action: opts.action } });
|
|
504
515
|
}
|
|
505
516
|
catch (e) {
|
|
506
517
|
fail(e);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retasc/cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Retasc CLI
|
|
3
|
+
"version": "1.21.1",
|
|
4
|
+
"description": "Retasc CLI — 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": {
|
|
7
7
|
"retasc": "dist/index.js"
|