@brutalsystems/muster 0.7.18 → 0.7.19
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/identity-cli.js +20 -9
- package/dist/identity-login.js +28 -3
- package/dist/muster.js +10 -2
- package/package.json +3 -2
package/dist/identity-cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { identityAuthState } from "./identity-auth.js";
|
|
2
2
|
import { IdentityError, identityDirExists, identityMetaSchema, identityNameSchema, identityPath, listIdentities, readIdentity, writeIdentity, } from "./identity-store.js";
|
|
3
3
|
import { removeStoredToken, writeStoredToken, } from "./identity-token.js";
|
|
4
|
-
import { captureClaudeToken, redactToken, } from "./identity-login.js";
|
|
4
|
+
import { captureClaudeToken, redactToken, resolveOnPath, } from "./identity-login.js";
|
|
5
5
|
/**
|
|
6
6
|
* The one-time login for each agent. Muster never performs it: each is an
|
|
7
7
|
* interactive OAuth flow, and a command that appeared to do it for you would be
|
|
@@ -25,7 +25,12 @@ export async function setupIdentity(opts) {
|
|
|
25
25
|
// a codex or opencode identity, where the credential is an auth file the
|
|
26
26
|
// agent's own login writes into the template, it has exactly one effect: a new
|
|
27
27
|
// way to fail. Refused where the operator types it.
|
|
28
|
-
|
|
28
|
+
// Checked against undefined, not truthiness: `--token-env ""` is falsy, so a
|
|
29
|
+
// truthiness test let an empty name past every guard and then dropped it,
|
|
30
|
+
// leaving the identity on neither credential route without saying so (#69).
|
|
31
|
+
if (opts.tokenEnv !== undefined && opts.tokenEnv.trim() === "")
|
|
32
|
+
throw new IdentityError(opts.name, "--token-env takes the name of an environment variable holding the token, not the token and not an empty value");
|
|
33
|
+
if (opts.tokenEnv !== undefined && opts.agent !== "claude")
|
|
29
34
|
throw new IdentityError(name, `--token-env applies only to a claude identity; ${opts.agent} authenticates from an auth file in the template, and a token variable here would only add a way for the launch to fail`);
|
|
30
35
|
// Distinguish "no such identity" from "an identity that is there but unusable".
|
|
31
36
|
// Overwriting the second would silently repurpose a directory that still holds
|
|
@@ -150,24 +155,30 @@ export async function interactiveSetup(opts) {
|
|
|
150
155
|
stored: false,
|
|
151
156
|
note: `Run this once, then check with \`muster identities\`:\n\n ${created.login}\n`,
|
|
152
157
|
};
|
|
158
|
+
// Asked before anything is spawned, so "could not be started" is a fact about
|
|
159
|
+
// the filesystem rather than an inference from what the child printed. That
|
|
160
|
+
// inference was platform-dependent: a missing executable emits nothing under
|
|
161
|
+
// macOS and an error into the pty under Linux (#69).
|
|
162
|
+
if (!(await resolveOnPath("claude", opts.env ?? process.env)))
|
|
163
|
+
throw new IdentityError(opts.name, "`claude` is not on PATH, so the login could not be started; nothing was changed");
|
|
153
164
|
write(`Running: claude setup-token\n${"─".repeat(50)}\n`);
|
|
154
165
|
const result = await captureClaudeToken({
|
|
155
166
|
dir: created.path,
|
|
156
167
|
env: opts.env ?? process.env,
|
|
157
168
|
onData: write,
|
|
158
|
-
|
|
169
|
+
stdin: opts.stdin,
|
|
159
170
|
});
|
|
160
171
|
write(`${"─".repeat(50)}\n`);
|
|
161
172
|
// Checked BEFORE the token branch, deliberately: a login that failed writes
|
|
162
173
|
// no credential even if something token-shaped went past on the way down.
|
|
163
174
|
//
|
|
164
|
-
//
|
|
165
|
-
// not throw for a missing executable — it exits 1 having emitted zero
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
175
|
+
// Two different events land here and they need different explanations. node-pty
|
|
176
|
+
// does not throw for a missing executable — it exits 1 having emitted zero
|
|
177
|
+
// bytes — which is exactly the exit shape of a login that failed on its own or
|
|
178
|
+
// that the operator interrupted. The exit code cannot tell them apart; whether
|
|
179
|
+
// anything was ever printed can (#69).
|
|
169
180
|
if (result.exitCode !== 0)
|
|
170
|
-
throw new IdentityError(opts.name, `\`claude setup-token\`
|
|
181
|
+
throw new IdentityError(opts.name, `\`claude setup-token\` ended without storing a credential (exit ${result.exitCode}); its own output above is the error. If you interrupted it, nothing was stored and nothing was changed.`);
|
|
171
182
|
if (result.token) {
|
|
172
183
|
const { clearedRoute } = await storeIdentityToken({
|
|
173
184
|
home: opts.home,
|
package/dist/identity-login.js
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
import { access, constants } from "node:fs/promises";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Where `name` is on PATH, or undefined. The same lookup `tmuxExecutable` does,
|
|
5
|
+
* and the reason the login asks it: whether a child "could not be started" has
|
|
6
|
+
* to be decided before spawning, not inferred from what it printed. node-pty
|
|
7
|
+
* does not throw for a missing executable — it exits 1, emitting nothing on
|
|
8
|
+
* macOS and an error into the pty on Linux, so output was never the discriminator
|
|
9
|
+
* it appeared to be (#69).
|
|
10
|
+
*/
|
|
11
|
+
export async function resolveOnPath(name, env) {
|
|
12
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
13
|
+
if (!dir)
|
|
14
|
+
continue;
|
|
15
|
+
const candidate = join(dir, name);
|
|
16
|
+
try {
|
|
17
|
+
await access(candidate, constants.X_OK);
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Not here, or not executable. Keep looking.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
1
26
|
/**
|
|
2
27
|
* A Claude long-lived OAuth token, as `claude setup-token` prints it. The length
|
|
3
28
|
* floor is what separates a real token from the literal word in help text.
|
|
@@ -257,8 +282,8 @@ function attachInput(pty, input, signals) {
|
|
|
257
282
|
* status quo.
|
|
258
283
|
*/
|
|
259
284
|
export async function captureClaudeToken(opts) {
|
|
260
|
-
const
|
|
261
|
-
const pty =
|
|
285
|
+
const spawn = opts.spawn ?? (await import("node-pty")).spawn;
|
|
286
|
+
const pty = spawn("claude", ["setup-token"], {
|
|
262
287
|
cwd: opts.dir,
|
|
263
288
|
env: { ...opts.env, CLAUDE_CONFIG_DIR: opts.dir },
|
|
264
289
|
name: "xterm-256color",
|
|
@@ -278,7 +303,7 @@ export async function captureClaudeToken(opts) {
|
|
|
278
303
|
// up before rethrowing, and enters raw mode last of all.
|
|
279
304
|
let detachInput = () => { };
|
|
280
305
|
try {
|
|
281
|
-
detachInput = attachInput(pty, opts.stdin
|
|
306
|
+
detachInput = attachInput(pty, opts.stdin, opts.signals ?? process);
|
|
282
307
|
const filter = createTokenFilter(opts.onData);
|
|
283
308
|
const dataSub = pty.onData((chunk) => filter.push(chunk));
|
|
284
309
|
const exitCode = await new Promise((resolve) => {
|
package/dist/muster.js
CHANGED
|
@@ -247,13 +247,21 @@ async function main() {
|
|
|
247
247
|
// year-long token to disk on the machine the operator explicitly asked
|
|
248
248
|
// to keep no credential at rest, while never recording the variable
|
|
249
249
|
// they named.
|
|
250
|
-
if (tokenEnv)
|
|
250
|
+
if (tokenEnv !== undefined && agent !== "claude")
|
|
251
|
+
// Not the mutual-exclusion message: for codex and opencode
|
|
252
|
+
// `--interactive` captures nothing and `--token-env` does not apply
|
|
253
|
+
// at all, so describing them as two routes to choose between would
|
|
254
|
+
// be describing Claude's behaviour at a different agent (#69).
|
|
255
|
+
throw new IdentityError(identity, `--token-env applies only to a claude identity; ${agent} authenticates from an auth file in the template, and a token variable here would only add a way for the launch to fail`);
|
|
256
|
+
if (tokenEnv !== undefined)
|
|
251
257
|
throw new IdentityError(identity, "--interactive and --token-env are the two mutually exclusive credential routes: --interactive runs the login and stores a token in the identity, --token-env records the name of a variable to read one from instead. Pass one, not both.");
|
|
252
258
|
const result = await interactiveSetup({
|
|
253
259
|
home: musterHome(),
|
|
254
260
|
name: identity,
|
|
255
261
|
agent: agent,
|
|
256
262
|
isTTY: Boolean(process.stdin.isTTY),
|
|
263
|
+
// The one place the operator's real keyboard is named.
|
|
264
|
+
stdin: process.stdin,
|
|
257
265
|
});
|
|
258
266
|
process.stdout.write(`\n${result.note}\n`);
|
|
259
267
|
return;
|
|
@@ -265,7 +273,7 @@ async function main() {
|
|
|
265
273
|
home: musterHome(),
|
|
266
274
|
name: identity,
|
|
267
275
|
agent: agent,
|
|
268
|
-
...(tokenEnv ? { tokenEnv } : {}),
|
|
276
|
+
...(tokenEnv !== undefined ? { tokenEnv } : {}),
|
|
269
277
|
})) + "\n");
|
|
270
278
|
}
|
|
271
279
|
else if (command === "identities") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brutalsystems/muster",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.19",
|
|
4
4
|
"description": "Launch instructed, reachable local agent sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc && node scripts/prepare-bin.mjs",
|
|
14
14
|
"test": "vitest run",
|
|
15
|
-
"
|
|
15
|
+
"typecheck": "tsc -p tsconfig.check.json",
|
|
16
|
+
"check": "npm run typecheck && npm test && node scripts/check-tarball.mjs",
|
|
16
17
|
"test:contract": "MUSTER_CONTRACT=1 vitest run test/contract.test.ts",
|
|
17
18
|
"postinstall": "node scripts/prepare-pty.mjs",
|
|
18
19
|
"pretest": "npm run build",
|