@retasc/cli 1.7.2 → 1.9.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/dist/api.js +23 -0
- package/dist/commands/bind.js +265 -116
- package/dist/commands/claim.js +3 -0
- package/dist/commands/doctor.js +81 -1
- package/dist/commands/join.js +200 -0
- package/dist/commands/mcp.js +46 -24
- package/dist/index.js +23 -27
- package/dist/lib/binding.js +7 -2
- package/dist/lib/invite.js +38 -0
- package/dist/lib/launcher.js +200 -0
- package/dist/lib/text.js +21 -0
- package/dist/version.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const PKG = "@retasc/cli";
|
|
5
|
+
// Windows shims are .cmd files, which cannot be spawned without a shell. Everywhere
|
|
6
|
+
// else a shell is an injection surface for no benefit, so it stays off.
|
|
7
|
+
//
|
|
8
|
+
// WINDOWS IS NOT A SUPPORTED PLATFORM. The branches below are written to npm's
|
|
9
|
+
// documented layout and unit-tested for shape, but nothing here has ever been RUN on
|
|
10
|
+
// Windows. Treat them as best effort: they may well work, and a Windows bug report is
|
|
11
|
+
// not a regression against anything we claim. Kept rather than removed because throwing
|
|
12
|
+
// on win32 would break someone for whom it currently works, for no gain.
|
|
13
|
+
const WIN = process.platform === "win32";
|
|
14
|
+
/**
|
|
15
|
+
* Does `command […args] --version` actually run, and print something version-shaped?
|
|
16
|
+
*
|
|
17
|
+
* Executing is the whole point. A binary can exist on disk and still be unusable (wrong
|
|
18
|
+
* arch, broken symlink, a shim whose target was removed), and the version check also
|
|
19
|
+
* guards against spawning some UNRELATED program that happens to be called `retasc`.
|
|
20
|
+
*/
|
|
21
|
+
export function runsOk(command, args = []) {
|
|
22
|
+
let r;
|
|
23
|
+
try {
|
|
24
|
+
r = spawnSync(command, [...args, "--version"], {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
shell: WIN,
|
|
27
|
+
timeout: 60_000,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (r.error || r.status !== 0)
|
|
34
|
+
return null;
|
|
35
|
+
const out = (r.stdout || "").trim();
|
|
36
|
+
// Our `--version` prints a bare semver (commander's .version()). Anything else is a
|
|
37
|
+
// different program, and pointing a marker at it would be worse than not writing one.
|
|
38
|
+
return /^\d+\.\d+\.\d+/.test(out) ? out : null;
|
|
39
|
+
}
|
|
40
|
+
/** npm's global prefix, or null when npm itself can't be run. */
|
|
41
|
+
export function npmGlobalPrefix() {
|
|
42
|
+
let r;
|
|
43
|
+
try {
|
|
44
|
+
r = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8", shell: WIN, timeout: 60_000 });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (r.error || r.status !== 0)
|
|
50
|
+
return null;
|
|
51
|
+
const out = (r.stdout || "").trim();
|
|
52
|
+
return out || null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Where npm puts the `retasc` shim for a global install.
|
|
56
|
+
*
|
|
57
|
+
* `npm bin -g` was REMOVED in npm 9, so it can't be asked directly — the prefix has to
|
|
58
|
+
* be turned into a bin path by hand, and the layout differs by platform: POSIX nests a
|
|
59
|
+
* `bin/`, Windows puts the `.cmd` shim straight in the prefix root.
|
|
60
|
+
*/
|
|
61
|
+
export function globalBinCandidates(prefix, win = WIN) {
|
|
62
|
+
return win
|
|
63
|
+
? [join(prefix, "retasc.cmd"), join(prefix, "retasc")]
|
|
64
|
+
: [join(prefix, "bin", "retasc")];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A launcher safe to write into a SHARED file.
|
|
68
|
+
*
|
|
69
|
+
* The absolute-path form is correct for the machine that resolved it and wrong everywhere
|
|
70
|
+
* else: `./.mcp.json` is the secret-free marker the product describes as safe to commit,
|
|
71
|
+
* so a path under someone's home directory both leaks their username into a committed
|
|
72
|
+
* file and hands every teammate a command that does not exist on their machine. The bare
|
|
73
|
+
* `retasc` it replaces was at least portable. So project-scoped writes get the pinned npx
|
|
74
|
+
* form, which is slower but true on any machine.
|
|
75
|
+
*/
|
|
76
|
+
export function portableLauncher(r, version) {
|
|
77
|
+
if (r.how !== "absolute")
|
|
78
|
+
return r.launcher;
|
|
79
|
+
return { command: "npx", args: ["-y", `${PKG}@${version}`] };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* How to spell "run this CLI again", for a message printed BEFORE anything has been made
|
|
83
|
+
* durable (RTSC-492).
|
|
84
|
+
*
|
|
85
|
+
* `join` can stop for reasons that have nothing to do with us — an org with no projects,
|
|
86
|
+
* a mint that failed — and every one of those has to name the command that resumes. Until
|
|
87
|
+
* `resolveLauncher` has run there may be no `retasc` on this machine at all, so telling
|
|
88
|
+
* someone to type `retasc bind …` would be telling them to type a command that does not
|
|
89
|
+
* exist. That is the same defect as a marker naming a missing binary, just aimed at the
|
|
90
|
+
* human instead of the agent.
|
|
91
|
+
*
|
|
92
|
+
* Decided from argv rather than by probing: it costs no subprocess, and it names the form
|
|
93
|
+
* they DEMONSTRABLY have — they just used it. npx unpacks the package into its own `_npx`
|
|
94
|
+
* cache and puts nothing on PATH, so a script path under that cache is proof of an npx run.
|
|
95
|
+
*/
|
|
96
|
+
export function selfCommand(version, argv1 = process.argv[1] ?? "") {
|
|
97
|
+
return /[\\/]_npx[\\/]/.test(argv1) ? `npx -y ${PKG}@${version}` : "retasc";
|
|
98
|
+
}
|
|
99
|
+
/** Install (or upgrade to) an exact version globally. Returns null on success. */
|
|
100
|
+
function installGlobal(version) {
|
|
101
|
+
// A cold global install takes seconds with no output of its own. Silence here reads as
|
|
102
|
+
// a hang in the middle of a bind, so say what is happening before it starts.
|
|
103
|
+
console.log(" Installing the retasc CLI so your agent can start it…");
|
|
104
|
+
let r;
|
|
105
|
+
try {
|
|
106
|
+
r = spawnSync("npm", ["install", "-g", `${PKG}@${version}`], {
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
shell: WIN,
|
|
109
|
+
// A cold global install pulls the tarball and its deps; 60s is not always enough.
|
|
110
|
+
timeout: 180_000,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
return e instanceof Error ? e.message : String(e);
|
|
115
|
+
}
|
|
116
|
+
if (r.error)
|
|
117
|
+
return r.error.message;
|
|
118
|
+
if (r.status !== 0) {
|
|
119
|
+
// EACCES is by far the common one (a prefix owned by root), and its first line is
|
|
120
|
+
// the useful part — the rest is npm's log-file boilerplate.
|
|
121
|
+
const msg = (r.stderr || r.stdout || "").trim().split("\n")[0];
|
|
122
|
+
return msg || `npm exited ${r.status}`;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Decide what the marker should name, and prove it before returning it.
|
|
128
|
+
*
|
|
129
|
+
* Order matters. An already-working `retasc` is left alone (no surprise installs for
|
|
130
|
+
* someone who already manages their own). Otherwise we try to make one, and if the
|
|
131
|
+
* install lands somewhere PATH can't see, the ABSOLUTE path to that binary is used
|
|
132
|
+
* instead — verified to run, full speed, and immune to whatever is wrong with PATH.
|
|
133
|
+
*
|
|
134
|
+
* npx is last on purpose. It works, but it costs a registry round-trip on every agent
|
|
135
|
+
* start (measured ~2.4s against ~0.06s for the binary), and unpinned it would resolve
|
|
136
|
+
* `latest` each time, so an agent's MCP server could change version mid-project without
|
|
137
|
+
* anyone asking. It is pinned here for exactly that reason.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveLauncher(opts) {
|
|
140
|
+
// 1. Already usable? Leave it alone.
|
|
141
|
+
if (runsOk("retasc")) {
|
|
142
|
+
return { launcher: { command: "retasc", args: [] }, how: "on-path", verified: true };
|
|
143
|
+
}
|
|
144
|
+
const npxLauncher = { command: "npx", args: ["-y", `${PKG}@${opts.version}`] };
|
|
145
|
+
// Last resort, so it is probed too rather than assumed — the point of this whole
|
|
146
|
+
// module is that nothing gets written into a marker on faith.
|
|
147
|
+
const npxFallback = (reason) => ({
|
|
148
|
+
launcher: npxLauncher,
|
|
149
|
+
how: "npx",
|
|
150
|
+
reason,
|
|
151
|
+
verified: runsOk("npx", npxLauncher.args) !== null,
|
|
152
|
+
});
|
|
153
|
+
if (opts.install === false)
|
|
154
|
+
return npxFallback("install not attempted");
|
|
155
|
+
// 2. Try to make `retasc` real.
|
|
156
|
+
const failure = installGlobal(opts.version);
|
|
157
|
+
// 3. Prove it, by running it. An exit code of 0 is not evidence the command resolves:
|
|
158
|
+
// npm can install happily into a prefix whose bin directory PATH never searches.
|
|
159
|
+
if (!failure) {
|
|
160
|
+
if (runsOk("retasc")) {
|
|
161
|
+
return { launcher: { command: "retasc", args: [] }, how: "installed", verified: true };
|
|
162
|
+
}
|
|
163
|
+
// Installed, but PATH can't see it. Name the file directly — this is the case a
|
|
164
|
+
// shell one-liner can never recover from, because the shell fails before our code runs.
|
|
165
|
+
const prefix = npmGlobalPrefix();
|
|
166
|
+
for (const bin of prefix ? globalBinCandidates(prefix) : []) {
|
|
167
|
+
if (existsSync(bin) && runsOk(bin)) {
|
|
168
|
+
return { launcher: { command: bin, args: [] }, how: "absolute", binPath: bin, verified: true };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 4. Nothing durable. Fall back, pinned, and let the caller say why.
|
|
173
|
+
return npxFallback(failure ?? "installed, but the binary could not be located or run");
|
|
174
|
+
}
|
|
175
|
+
/** What to tell the user about the outcome. One line, or none when nothing happened. */
|
|
176
|
+
export function launcherNote(r) {
|
|
177
|
+
switch (r.how) {
|
|
178
|
+
case "on-path":
|
|
179
|
+
return null; // nothing changed; saying so is noise
|
|
180
|
+
case "installed":
|
|
181
|
+
return "✓ Installed `retasc` so your agent can start it directly.";
|
|
182
|
+
case "absolute":
|
|
183
|
+
return (`✓ Installed retasc at ${r.binPath}\n` +
|
|
184
|
+
" Its folder isn't on your PATH, so your agent will use the full path above.\n" +
|
|
185
|
+
` To type \`retasc\` yourself, add this to your shell profile:\n` +
|
|
186
|
+
` export PATH="${r.binPath?.replace(/\/retasc$/, "")}:$PATH"`);
|
|
187
|
+
case "npx":
|
|
188
|
+
// Unverified is the one outcome where the workspace is written but NOT working.
|
|
189
|
+
// Say that outright: the whole defect this replaces was a setup that reported
|
|
190
|
+
// success and left the agent unable to start.
|
|
191
|
+
if (!r.verified) {
|
|
192
|
+
return (`! Couldn't install retasc${r.reason ? ` (${r.reason})` : ""}, and npx can't start it either.\n` +
|
|
193
|
+
" The marker was written, but your agent will NOT be able to start Retasc yet.\n" +
|
|
194
|
+
" Install the CLI, then run `retasc bind` again: npm install -g @retasc/cli");
|
|
195
|
+
}
|
|
196
|
+
return (`! Couldn't install retasc globally${r.reason ? ` (${r.reason})` : ""}.\n` +
|
|
197
|
+
" Your agent will start it through npx instead, which works but is slower\n" +
|
|
198
|
+
" and needs the network. To fix it later, run: npm install -g @retasc/cli");
|
|
199
|
+
}
|
|
200
|
+
}
|
package/dist/lib/text.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip control characters from a string before printing it (RTSC-261, RTSC-492).
|
|
3
|
+
*
|
|
4
|
+
* Anything that came off the wire is untrusted output: it reaches `console.log` in a
|
|
5
|
+
* terminal that acts on escape sequences, so a value carrying `\x1b[…` can move the cursor,
|
|
6
|
+
* clear the line, or overwrite text the user is reading. That matters most where the text
|
|
7
|
+
* sits next to a decision — a name in a confirm prompt could be dressed up to look like the
|
|
8
|
+
* prompt itself.
|
|
9
|
+
*
|
|
10
|
+
* The riskiest strings in the product are the ones this exists for: an imported member's
|
|
11
|
+
* display name is chosen in ClickUp/Jira/Asana by someone who is not our user, carried
|
|
12
|
+
* across by a migration verbatim, and then shown to a human about to make an irreversible
|
|
13
|
+
* choice about it.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors `sanitize` in `convex/lib/userError.ts` and the inline cleanup in `formatError`.
|
|
16
|
+
* The three are one rule; this is the copy the CLI's own printing paths share.
|
|
17
|
+
*/
|
|
18
|
+
export function clean(s) {
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
return String(s).replace(/[\x00-\x1f\x7f\u0085\u2028\u2029]/g, " ");
|
|
21
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
// Single source of truth for the version, read from package.json at runtime relative to
|
|
5
|
+
// the COMPILED file (dist/version.js -> ../package.json). A JSON import won't work:
|
|
6
|
+
// tsconfig has rootDir "src", so importing ../package.json is outside it.
|
|
7
|
+
//
|
|
8
|
+
// It lives in its own module because two places now need it — `--version`, and the
|
|
9
|
+
// pinned npx fallback the MCP marker may name (RTSC-493). A marker pinned to a version
|
|
10
|
+
// this build doesn't match would spawn a different CLI than the one that wrote it.
|
|
11
|
+
export const VERSION = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
|
package/package.json
CHANGED