@amdahl/cli 0.3.3 → 0.3.5
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/README.md +232 -58
- package/dist/amdahl.js +851 -264
- package/package.json +1 -1
package/dist/amdahl.js
CHANGED
|
@@ -315,8 +315,8 @@ var require_directives = __commonJS({
|
|
|
315
315
|
if (parts.length < 2)
|
|
316
316
|
return false;
|
|
317
317
|
}
|
|
318
|
-
const [handle,
|
|
319
|
-
this.tags[handle] =
|
|
318
|
+
const [handle, prefix2] = parts;
|
|
319
|
+
this.tags[handle] = prefix2;
|
|
320
320
|
return true;
|
|
321
321
|
}
|
|
322
322
|
case "%YAML": {
|
|
@@ -366,10 +366,10 @@ var require_directives = __commonJS({
|
|
|
366
366
|
const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s);
|
|
367
367
|
if (!suffix)
|
|
368
368
|
onError(`The ${source} tag has no suffix`);
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
369
|
+
const prefix2 = this.tags[handle];
|
|
370
|
+
if (prefix2) {
|
|
371
371
|
try {
|
|
372
|
-
return
|
|
372
|
+
return prefix2 + decodeURIComponent(suffix);
|
|
373
373
|
} catch (error) {
|
|
374
374
|
onError(String(error));
|
|
375
375
|
return null;
|
|
@@ -385,9 +385,9 @@ var require_directives = __commonJS({
|
|
|
385
385
|
* taking into account current tag prefixes and defaults.
|
|
386
386
|
*/
|
|
387
387
|
tagString(tag) {
|
|
388
|
-
for (const [handle,
|
|
389
|
-
if (tag.startsWith(
|
|
390
|
-
return handle + escapeTagName(tag.substring(
|
|
388
|
+
for (const [handle, prefix2] of Object.entries(this.tags)) {
|
|
389
|
+
if (tag.startsWith(prefix2))
|
|
390
|
+
return handle + escapeTagName(tag.substring(prefix2.length));
|
|
391
391
|
}
|
|
392
392
|
return tag[0] === "!" ? tag : `!<${tag}>`;
|
|
393
393
|
}
|
|
@@ -404,11 +404,11 @@ var require_directives = __commonJS({
|
|
|
404
404
|
tagNames = Object.keys(tags);
|
|
405
405
|
} else
|
|
406
406
|
tagNames = [];
|
|
407
|
-
for (const [handle,
|
|
408
|
-
if (handle === "!!" &&
|
|
407
|
+
for (const [handle, prefix2] of tagEntries) {
|
|
408
|
+
if (handle === "!!" && prefix2 === "tag:yaml.org,2002:")
|
|
409
409
|
continue;
|
|
410
|
-
if (!doc || tagNames.some((tn) => tn.startsWith(
|
|
411
|
-
lines.push(`%TAG ${handle} ${
|
|
410
|
+
if (!doc || tagNames.some((tn) => tn.startsWith(prefix2)))
|
|
411
|
+
lines.push(`%TAG ${handle} ${prefix2}`);
|
|
412
412
|
}
|
|
413
413
|
return lines.join("\n");
|
|
414
414
|
}
|
|
@@ -443,14 +443,14 @@ var require_anchors = __commonJS({
|
|
|
443
443
|
});
|
|
444
444
|
return anchors;
|
|
445
445
|
}
|
|
446
|
-
function findNewAnchor(
|
|
446
|
+
function findNewAnchor(prefix2, exclude) {
|
|
447
447
|
for (let i = 1; true; ++i) {
|
|
448
|
-
const name = `${
|
|
448
|
+
const name = `${prefix2}${i}`;
|
|
449
449
|
if (!exclude.has(name))
|
|
450
450
|
return name;
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
|
-
function createNodeAnchors(doc,
|
|
453
|
+
function createNodeAnchors(doc, prefix2) {
|
|
454
454
|
const aliasObjects = [];
|
|
455
455
|
const sourceObjects = /* @__PURE__ */ new Map();
|
|
456
456
|
let prevAnchors = null;
|
|
@@ -458,7 +458,7 @@ var require_anchors = __commonJS({
|
|
|
458
458
|
onAnchor: (source) => {
|
|
459
459
|
aliasObjects.push(source);
|
|
460
460
|
prevAnchors ?? (prevAnchors = anchorNames(doc));
|
|
461
|
-
const anchor = findNewAnchor(
|
|
461
|
+
const anchor = findNewAnchor(prefix2, prevAnchors);
|
|
462
462
|
prevAnchors.add(anchor);
|
|
463
463
|
return anchor;
|
|
464
464
|
},
|
|
@@ -2451,10 +2451,10 @@ var require_int = __commonJS({
|
|
|
2451
2451
|
var stringifyNumber = require_stringifyNumber();
|
|
2452
2452
|
var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value);
|
|
2453
2453
|
var intResolve = (str2, offset, radix, { intAsBigInt }) => intAsBigInt ? BigInt(str2) : parseInt(str2.substring(offset), radix);
|
|
2454
|
-
function intStringify(node, radix,
|
|
2454
|
+
function intStringify(node, radix, prefix2) {
|
|
2455
2455
|
const { value } = node;
|
|
2456
2456
|
if (intIdentify(value) && value >= 0)
|
|
2457
|
-
return
|
|
2457
|
+
return prefix2 + value.toString(radix);
|
|
2458
2458
|
return stringifyNumber.stringifyNumber(node);
|
|
2459
2459
|
}
|
|
2460
2460
|
var intOct = {
|
|
@@ -2916,11 +2916,11 @@ var require_int2 = __commonJS({
|
|
|
2916
2916
|
const n = parseInt(str2, radix);
|
|
2917
2917
|
return sign === "-" ? -1 * n : n;
|
|
2918
2918
|
}
|
|
2919
|
-
function intStringify(node, radix,
|
|
2919
|
+
function intStringify(node, radix, prefix2) {
|
|
2920
2920
|
const { value } = node;
|
|
2921
2921
|
if (intIdentify(value)) {
|
|
2922
2922
|
const str2 = value.toString(radix);
|
|
2923
|
-
return value < 0 ? "-" +
|
|
2923
|
+
return value < 0 ? "-" + prefix2 + str2.substr(1) : prefix2 + str2;
|
|
2924
2924
|
}
|
|
2925
2925
|
return stringifyNumber.stringifyNumber(node);
|
|
2926
2926
|
}
|
|
@@ -7365,26 +7365,45 @@ var require_dist = __commonJS({
|
|
|
7365
7365
|
|
|
7366
7366
|
// src/io.ts
|
|
7367
7367
|
import { spawn } from "child_process";
|
|
7368
|
+
import { accessSync, constants, statSync } from "fs";
|
|
7368
7369
|
import { homedir, hostname } from "os";
|
|
7369
|
-
import { createInterface } from "readline";
|
|
7370
|
+
import { createInterface as createInterface2 } from "readline";
|
|
7370
7371
|
|
|
7371
7372
|
// src/select.ts
|
|
7372
|
-
import { emitKeypressEvents } from "readline";
|
|
7373
|
+
import { createInterface, emitKeypressEvents } from "readline";
|
|
7373
7374
|
var HINT = "(Use the arrow keys, then Enter)";
|
|
7374
|
-
var MULTI_HINT = "
|
|
7375
|
+
var MULTI_HINT = "\u2191\u2193 move \xB7 Space tick/untick \xB7 Enter to accept";
|
|
7375
7376
|
var OFF = "\x1B[0m";
|
|
7377
|
+
var GREY = "\x1B[90m";
|
|
7378
|
+
var GREY_OFF = "\x1B[39m";
|
|
7376
7379
|
var HIDE_CURSOR = "\x1B[?25l";
|
|
7377
7380
|
var SHOW_CURSOR = "\x1B[?25h";
|
|
7378
7381
|
var CLEAR_LINE = "\x1B[2K";
|
|
7379
7382
|
var CLEAR_DOWN = "\x1B[0J";
|
|
7380
|
-
var up = (n) => `\x1B[${n}A
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7383
|
+
var up = (n) => n > 0 ? `\x1B[${n}A` : "";
|
|
7384
|
+
var ESCAPES = /\u001b\[[0-9;?]*[A-Za-z]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
|
|
7385
|
+
function eraseUp(lines) {
|
|
7386
|
+
return `\r${up(lines)}${CLEAR_DOWN}`;
|
|
7387
|
+
}
|
|
7388
|
+
function screenLines(text2, columns) {
|
|
7389
|
+
const width = [...text2.replace(ESCAPES, "")].length;
|
|
7390
|
+
if (!columns || columns < 1) return 1;
|
|
7391
|
+
return Math.max(1, Math.ceil(width / columns));
|
|
7392
|
+
}
|
|
7393
|
+
function runMenu(input, output, head, rows, onKey) {
|
|
7394
|
+
const { question, note, hint } = head;
|
|
7395
|
+
const width = () => head.columns?.();
|
|
7396
|
+
let headLines = 0;
|
|
7397
|
+
let drawn = 0;
|
|
7398
|
+
const frame = () => {
|
|
7399
|
+
const cols = width();
|
|
7400
|
+
const lines = [...rows(), ...fitHint(hint, cols)];
|
|
7401
|
+
drawn = lines.reduce((n, line) => n + screenLines(line, cols), 0);
|
|
7402
|
+
return lines.map((line) => `${CLEAR_LINE}\r${line}
|
|
7403
|
+
`).join("");
|
|
7404
|
+
};
|
|
7386
7405
|
const wasRaw = input.isRaw === true;
|
|
7387
|
-
return new Promise((
|
|
7406
|
+
return new Promise((resolve2, reject) => {
|
|
7388
7407
|
let done = false;
|
|
7389
7408
|
const finish = (outcome) => {
|
|
7390
7409
|
if (done) return;
|
|
@@ -7398,19 +7417,22 @@ function runMenu(input, output, question, hint, rows, onKey) {
|
|
|
7398
7417
|
}
|
|
7399
7418
|
input.pause();
|
|
7400
7419
|
if ("pick" in outcome) {
|
|
7401
|
-
output.write(
|
|
7420
|
+
output.write(`\r${up(headLines + drawn)}${CLEAR_DOWN}${question} ${outcome.summary}
|
|
7402
7421
|
${SHOW_CURSOR}`);
|
|
7403
|
-
|
|
7422
|
+
resolve2(outcome.pick);
|
|
7404
7423
|
} else {
|
|
7405
|
-
output.write(
|
|
7424
|
+
output.write(`\r${up(headLines + drawn)}${CLEAR_DOWN}${question}
|
|
7425
|
+
${SHOW_CURSOR}`);
|
|
7406
7426
|
reject(outcome.error);
|
|
7407
7427
|
}
|
|
7408
7428
|
};
|
|
7409
7429
|
const onKeypress = (text2, key) => {
|
|
7410
7430
|
const name = key?.name ?? text2;
|
|
7411
|
-
if (key?.ctrl && (name === "c" || name === "d")
|
|
7431
|
+
if (key?.ctrl && (name === "c" || name === "d") || name === "escape" || name === "q" && !key?.ctrl) {
|
|
7432
|
+
return finish({ error: new PromptCancelled() });
|
|
7433
|
+
}
|
|
7412
7434
|
const result = onKey(text2, name);
|
|
7413
|
-
if (result === "redraw") output.write(
|
|
7435
|
+
if (result === "redraw") output.write(`\r${up(drawn)}${CLEAR_DOWN}${frame()}`);
|
|
7414
7436
|
else if (result) finish(result);
|
|
7415
7437
|
};
|
|
7416
7438
|
const onError = (err) => finish({ error: err });
|
|
@@ -7418,8 +7440,11 @@ ${SHOW_CURSOR}`);
|
|
|
7418
7440
|
try {
|
|
7419
7441
|
emitKeypressEvents(input);
|
|
7420
7442
|
input.setRawMode?.(true);
|
|
7421
|
-
|
|
7422
|
-
|
|
7443
|
+
const cols = width();
|
|
7444
|
+
const top = [question, ...note ? note.split("\n").flatMap((line) => wrapWords(line, cols)) : []];
|
|
7445
|
+
headLines = top.reduce((n, line) => n + screenLines(line, cols), 0);
|
|
7446
|
+
output.write(`${HIDE_CURSOR}${top.map((line) => `${line}
|
|
7447
|
+
`).join("")}${frame()}`);
|
|
7423
7448
|
input.on("keypress", onKeypress);
|
|
7424
7449
|
input.on("error", onError);
|
|
7425
7450
|
process.on("SIGINT", onSigint);
|
|
@@ -7429,6 +7454,26 @@ ${draw()}`);
|
|
|
7429
7454
|
}
|
|
7430
7455
|
});
|
|
7431
7456
|
}
|
|
7457
|
+
function fitHint(hint, columns) {
|
|
7458
|
+
if (!columns || [...hint].length <= columns) return [hint];
|
|
7459
|
+
const lines = [];
|
|
7460
|
+
for (const part of hint.split(" \xB7 ")) {
|
|
7461
|
+
const last = lines[lines.length - 1];
|
|
7462
|
+
if (last !== void 0 && [...`${last} \xB7 ${part}`].length <= columns) lines[lines.length - 1] = `${last} \xB7 ${part}`;
|
|
7463
|
+
else lines.push(part);
|
|
7464
|
+
}
|
|
7465
|
+
return lines;
|
|
7466
|
+
}
|
|
7467
|
+
function wrapWords(text2, columns) {
|
|
7468
|
+
if (!columns || [...text2].length <= columns) return [text2];
|
|
7469
|
+
const lines = [];
|
|
7470
|
+
for (const word of text2.split(" ")) {
|
|
7471
|
+
const last = lines[lines.length - 1];
|
|
7472
|
+
if (last !== void 0 && [...`${last} ${word}`].length <= columns) lines[lines.length - 1] = `${last} ${word}`;
|
|
7473
|
+
else lines.push(word);
|
|
7474
|
+
}
|
|
7475
|
+
return lines;
|
|
7476
|
+
}
|
|
7432
7477
|
function marker(options) {
|
|
7433
7478
|
return options.color ? options.trueColor ? "\x1B[1;38;2;253;75;3m" : "\x1B[1;38;5;202m" : "\x1B[1m";
|
|
7434
7479
|
}
|
|
@@ -7442,7 +7487,7 @@ function selectMenu(input, output, question, choices, options = {}) {
|
|
|
7442
7487
|
let current = Math.min(Math.max(options.initial ?? 0, 0), choices.length - 1);
|
|
7443
7488
|
const on = marker(options);
|
|
7444
7489
|
const rows = () => choices.map((c, i) => i === current ? `${on}> ${c}${OFF}` : ` ${c}`);
|
|
7445
|
-
return runMenu(input, output, question, HINT, rows, (text2, name) => {
|
|
7490
|
+
return runMenu(input, output, { question, hint: HINT, columns: options.columns }, rows, (text2, name) => {
|
|
7446
7491
|
if (name === "return" || name === "enter") return { pick: current, summary: choices[current] ?? "" };
|
|
7447
7492
|
const next = move(current, choices.length, text2, name);
|
|
7448
7493
|
if (next === null || next === current) return null;
|
|
@@ -7451,14 +7496,24 @@ function selectMenu(input, output, question, choices, options = {}) {
|
|
|
7451
7496
|
});
|
|
7452
7497
|
}
|
|
7453
7498
|
function multiSelectMenu(input, output, question, choices, options = {}) {
|
|
7454
|
-
let current = 0;
|
|
7455
7499
|
const ticked = choices.map((_, i) => options.ticked?.[i] === true);
|
|
7456
|
-
const
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7500
|
+
const start = Math.min(Math.max(options.initial ?? 0, 0), choices.length - 1);
|
|
7501
|
+
let current = ticked.includes(true) ? ticked.indexOf(true) : start;
|
|
7502
|
+
const pointer = `${marker(options)}>${OFF}`;
|
|
7503
|
+
const pad = Math.max(...choices.map((c) => c.length));
|
|
7504
|
+
const label = (i) => {
|
|
7505
|
+
const tag = options.tags?.[i];
|
|
7506
|
+
const name = choices[i] ?? "";
|
|
7507
|
+
if (!tag) return name;
|
|
7508
|
+
const cols = options.columns?.() ?? Infinity;
|
|
7509
|
+
const grey = options.color ? `${GREY}${tag}${GREY_OFF}` : tag;
|
|
7510
|
+
if (6 + pad + 3 + tag.length <= cols) return `${name.padEnd(pad)} ${grey}`;
|
|
7511
|
+
if (6 + name.length + 1 + tag.length <= cols) return `${name} ${grey}`;
|
|
7512
|
+
return name;
|
|
7513
|
+
};
|
|
7514
|
+
const rows = () => choices.map((_, i) => `${i === current ? pointer : " "} [${ticked[i] ? "x" : " "}] ${label(i)}`);
|
|
7515
|
+
const head = { question, note: options.note, hint: options.hint ?? MULTI_HINT, columns: options.columns };
|
|
7516
|
+
return runMenu(input, output, head, rows, (text2, name) => {
|
|
7462
7517
|
if (name === "return" || name === "enter") {
|
|
7463
7518
|
const picks = choices.flatMap((_, i) => ticked[i] ? [i] : []);
|
|
7464
7519
|
return { pick: picks, summary: picks.map((i) => choices[i]).join(", ") || "none" };
|
|
@@ -7514,6 +7569,56 @@ function listenForEnter(input, onEnter, onCancel) {
|
|
|
7514
7569
|
}
|
|
7515
7570
|
return stop;
|
|
7516
7571
|
}
|
|
7572
|
+
var PASTE_ON = "\x1B[?2004h";
|
|
7573
|
+
var PASTE_OFF = "\x1B[?2004l";
|
|
7574
|
+
var PASTE_SETTLE_MS = 150;
|
|
7575
|
+
function readPaste(input, output, question) {
|
|
7576
|
+
output.write(`${question}
|
|
7577
|
+
`);
|
|
7578
|
+
const rl = createInterface({ input, output, terminal: true, prompt: "" });
|
|
7579
|
+
output.write(PASTE_ON);
|
|
7580
|
+
return new Promise((resolve2, reject) => {
|
|
7581
|
+
const lines = [];
|
|
7582
|
+
let pasting = false;
|
|
7583
|
+
let settle;
|
|
7584
|
+
let settled = false;
|
|
7585
|
+
const finish = (outcome) => {
|
|
7586
|
+
if (settled) return;
|
|
7587
|
+
settled = true;
|
|
7588
|
+
clearTimeout(settle);
|
|
7589
|
+
input.removeListener("keypress", onKeypress);
|
|
7590
|
+
output.write(PASTE_OFF);
|
|
7591
|
+
rl.close();
|
|
7592
|
+
if ("text" in outcome) resolve2(outcome.text);
|
|
7593
|
+
else reject(outcome.error);
|
|
7594
|
+
};
|
|
7595
|
+
const done = () => finish({ text: lines.join("\n").trim() });
|
|
7596
|
+
const onKeypress = (_text, key) => {
|
|
7597
|
+
if (key?.name === "paste-start") pasting = true;
|
|
7598
|
+
else if (key?.name === "paste-end") pasting = false;
|
|
7599
|
+
else if (settle && key?.name !== "return" && key?.name !== "enter") {
|
|
7600
|
+
clearTimeout(settle);
|
|
7601
|
+
settle = void 0;
|
|
7602
|
+
}
|
|
7603
|
+
};
|
|
7604
|
+
input.on("keypress", onKeypress);
|
|
7605
|
+
rl.on("line", (line) => {
|
|
7606
|
+
if (line.trim() || pasting) {
|
|
7607
|
+
lines.push(line);
|
|
7608
|
+
return;
|
|
7609
|
+
}
|
|
7610
|
+
if (lines.length === 0) return;
|
|
7611
|
+
lines.push(line);
|
|
7612
|
+
clearTimeout(settle);
|
|
7613
|
+
settle = setTimeout(done, PASTE_SETTLE_MS);
|
|
7614
|
+
});
|
|
7615
|
+
rl.on("SIGINT", () => {
|
|
7616
|
+
output.write("\n");
|
|
7617
|
+
finish({ error: new PromptCancelled() });
|
|
7618
|
+
});
|
|
7619
|
+
rl.on("close", done);
|
|
7620
|
+
});
|
|
7621
|
+
}
|
|
7517
7622
|
|
|
7518
7623
|
// src/io.ts
|
|
7519
7624
|
var PromptCancelled = class extends Error {
|
|
@@ -7522,20 +7627,20 @@ var PromptCancelled = class extends Error {
|
|
|
7522
7627
|
this.name = "PromptCancelled";
|
|
7523
7628
|
}
|
|
7524
7629
|
};
|
|
7525
|
-
var runProcess = (command, args, options) => new Promise((
|
|
7630
|
+
var runProcess = (command, args, options) => new Promise((resolve2) => {
|
|
7526
7631
|
let stdout = "";
|
|
7527
7632
|
let stderr = "";
|
|
7528
7633
|
let child;
|
|
7529
7634
|
try {
|
|
7530
7635
|
child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
7531
7636
|
} catch (err) {
|
|
7532
|
-
|
|
7637
|
+
resolve2({ code: -1, stdout: "", stderr: String(err) });
|
|
7533
7638
|
return;
|
|
7534
7639
|
}
|
|
7535
7640
|
child.stdout.on("data", (d) => stdout += d.toString("utf8"));
|
|
7536
7641
|
child.stderr.on("data", (d) => stderr += d.toString("utf8"));
|
|
7537
|
-
child.on("error", (err) =>
|
|
7538
|
-
child.on("close", (code) =>
|
|
7642
|
+
child.on("error", (err) => resolve2({ code: -1, stdout, stderr: String(err) }));
|
|
7643
|
+
child.on("close", (code) => resolve2({ code: code ?? -1, stdout, stderr }));
|
|
7539
7644
|
child.stdin.on("error", () => {
|
|
7540
7645
|
});
|
|
7541
7646
|
if (options?.input !== void 0) child.stdin.end(options.input);
|
|
@@ -7547,8 +7652,8 @@ async function readAllStdin() {
|
|
|
7547
7652
|
return Buffer.concat(chunks).toString("utf8");
|
|
7548
7653
|
}
|
|
7549
7654
|
function promptLine(question) {
|
|
7550
|
-
const rl =
|
|
7551
|
-
return new Promise((
|
|
7655
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
7656
|
+
return new Promise((resolve2, reject) => {
|
|
7552
7657
|
let settled = false;
|
|
7553
7658
|
const cancel = () => {
|
|
7554
7659
|
if (settled) return;
|
|
@@ -7562,10 +7667,26 @@ function promptLine(question) {
|
|
|
7562
7667
|
rl.question(question, (answer) => {
|
|
7563
7668
|
settled = true;
|
|
7564
7669
|
rl.close();
|
|
7565
|
-
|
|
7670
|
+
resolve2(answer);
|
|
7566
7671
|
});
|
|
7567
7672
|
});
|
|
7568
7673
|
}
|
|
7674
|
+
function runOnTerminal(command, args) {
|
|
7675
|
+
const hold = () => {
|
|
7676
|
+
};
|
|
7677
|
+
process.on("SIGINT", hold);
|
|
7678
|
+
return new Promise((resolve2) => {
|
|
7679
|
+
let child;
|
|
7680
|
+
try {
|
|
7681
|
+
child = spawn(command, args, { stdio: "inherit" });
|
|
7682
|
+
} catch {
|
|
7683
|
+
resolve2(-1);
|
|
7684
|
+
return;
|
|
7685
|
+
}
|
|
7686
|
+
child.on("error", () => resolve2(-1));
|
|
7687
|
+
child.on("close", (code) => resolve2(code ?? -1));
|
|
7688
|
+
}).finally(() => process.off("SIGINT", hold));
|
|
7689
|
+
}
|
|
7569
7690
|
function uninterruptible(fn) {
|
|
7570
7691
|
const hold = () => {
|
|
7571
7692
|
};
|
|
@@ -7576,6 +7697,15 @@ function uninterruptible(fn) {
|
|
|
7576
7697
|
setImmediate(() => process.off("SIGINT", hold));
|
|
7577
7698
|
}
|
|
7578
7699
|
}
|
|
7700
|
+
function isExecutable(path) {
|
|
7701
|
+
try {
|
|
7702
|
+
if (!statSync(path).isFile()) return false;
|
|
7703
|
+
accessSync(path, constants.X_OK);
|
|
7704
|
+
return true;
|
|
7705
|
+
} catch {
|
|
7706
|
+
return false;
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7579
7709
|
function openerFor(platform, url) {
|
|
7580
7710
|
if (platform === "darwin") return ["open", [url]];
|
|
7581
7711
|
if (platform === "win32") return ["cmd", ["/c", "start", '""', url]];
|
|
@@ -7590,23 +7720,30 @@ function realIo() {
|
|
|
7590
7720
|
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
7591
7721
|
stderrIsTTY: Boolean(process.stderr.isTTY),
|
|
7592
7722
|
readStdin: readAllStdin,
|
|
7723
|
+
readPaste: (question) => readPaste(process.stdin, process.stderr, question),
|
|
7593
7724
|
prompt: promptLine,
|
|
7594
7725
|
select: (question, choices, options) => selectMenu(process.stdin, process.stderr, question, choices, {
|
|
7595
7726
|
...options,
|
|
7596
|
-
trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "")
|
|
7727
|
+
trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? ""),
|
|
7728
|
+
columns: () => process.stderr.columns
|
|
7597
7729
|
}),
|
|
7598
7730
|
multiSelect: (question, choices, options) => multiSelectMenu(process.stdin, process.stderr, question, choices, {
|
|
7599
7731
|
...options,
|
|
7600
|
-
trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "")
|
|
7732
|
+
trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? ""),
|
|
7733
|
+
columns: () => process.stderr.columns
|
|
7601
7734
|
}),
|
|
7602
7735
|
listenForEnter: (onEnter, onCancel) => listenForEnter(process.stdin, onEnter, onCancel),
|
|
7603
7736
|
uninterruptible,
|
|
7604
7737
|
run: runProcess,
|
|
7738
|
+
runInteractive: runOnTerminal,
|
|
7605
7739
|
openUrl: async (url) => {
|
|
7606
7740
|
const [cmd, args] = openerFor(process.platform, url);
|
|
7607
7741
|
return (await runProcess(cmd, args)).code === 0;
|
|
7608
7742
|
},
|
|
7609
7743
|
homedir: homedir(),
|
|
7744
|
+
scriptPath: process.argv[1],
|
|
7745
|
+
columns: () => process.stderr.columns,
|
|
7746
|
+
isExecutable,
|
|
7610
7747
|
cwd: process.cwd(),
|
|
7611
7748
|
platform: process.platform,
|
|
7612
7749
|
hostname: hostname(),
|
|
@@ -7620,6 +7757,18 @@ function realIo() {
|
|
|
7620
7757
|
// src/args.ts
|
|
7621
7758
|
import { parseArgs } from "util";
|
|
7622
7759
|
|
|
7760
|
+
// src/command-name.ts
|
|
7761
|
+
var prefix = "amdahl";
|
|
7762
|
+
function startedByNpx(io) {
|
|
7763
|
+
return io.env.npm_command === "exec" || io.env.npm_lifecycle_event === "npx" || /[\\/]_npx[\\/]/.test(io.scriptPath ?? "");
|
|
7764
|
+
}
|
|
7765
|
+
function setCommandName(io) {
|
|
7766
|
+
prefix = startedByNpx(io) ? "npx -y amdahl" : "amdahl";
|
|
7767
|
+
}
|
|
7768
|
+
function amdahlCommand(rest = "") {
|
|
7769
|
+
return rest ? `${prefix} ${rest}` : prefix;
|
|
7770
|
+
}
|
|
7771
|
+
|
|
7623
7772
|
// src/errors.ts
|
|
7624
7773
|
var EXIT = {
|
|
7625
7774
|
ok: 0,
|
|
@@ -7656,7 +7805,10 @@ var CliError = class extends Error {
|
|
|
7656
7805
|
function usageError(message2, hint) {
|
|
7657
7806
|
return new CliError("usage", message2, EXIT.usage, hint);
|
|
7658
7807
|
}
|
|
7659
|
-
|
|
7808
|
+
function notSignedIn() {
|
|
7809
|
+
return `Not signed in. Run \`${amdahlCommand("login")}\`, or set AMDAHL_KEY.`;
|
|
7810
|
+
}
|
|
7811
|
+
var NETWORK_HINT = "Check your internet connection, then run the same command again.";
|
|
7660
7812
|
var CODE_EXITS = {
|
|
7661
7813
|
oauth_required: EXIT.forbidden,
|
|
7662
7814
|
cli_client_required: EXIT.forbidden,
|
|
@@ -7694,7 +7846,10 @@ var GLOBAL_OPTIONS = {
|
|
|
7694
7846
|
version: { type: "boolean", short: "v" },
|
|
7695
7847
|
verbose: { type: "boolean" }
|
|
7696
7848
|
};
|
|
7849
|
+
var SETUP_OPTIONS = { "no-browser": { type: "boolean" } };
|
|
7697
7850
|
var COMMAND_OPTIONS = {
|
|
7851
|
+
setup: SETUP_OPTIONS,
|
|
7852
|
+
help: {},
|
|
7698
7853
|
login: {
|
|
7699
7854
|
workspace: { type: "string" },
|
|
7700
7855
|
"no-browser": { type: "boolean" },
|
|
@@ -7747,12 +7902,12 @@ function parseCommandLine(argv) {
|
|
|
7747
7902
|
try {
|
|
7748
7903
|
loose = parseArgs({ args: argv, options: allOptions(), allowPositionals: true, strict: false });
|
|
7749
7904
|
} catch (err) {
|
|
7750
|
-
throw usageError(
|
|
7905
|
+
throw usageError(...parseErrorMessage(err));
|
|
7751
7906
|
}
|
|
7752
7907
|
const positionals = loose.positionals;
|
|
7753
7908
|
const first = positionals[0];
|
|
7754
7909
|
if (first === void 0) {
|
|
7755
|
-
return { command: null, args: [], flags: strictParse(argv, GLOBAL_OPTIONS).values };
|
|
7910
|
+
return { command: null, args: [], flags: strictParse(argv, { ...GLOBAL_OPTIONS, ...SETUP_OPTIONS }).values };
|
|
7756
7911
|
}
|
|
7757
7912
|
let words = 1;
|
|
7758
7913
|
let name = first;
|
|
@@ -7762,7 +7917,7 @@ function parseCommandLine(argv) {
|
|
|
7762
7917
|
const ref = second && second.includes("/") ? second : "<publisher>/<skill>";
|
|
7763
7918
|
throw usageError(
|
|
7764
7919
|
second === void 0 ? "`amdahl skills` needs a subcommand." : `Unknown command \`amdahl skills ${second}\`.`,
|
|
7765
|
-
`To add a skill: npx amdahl skills add ${ref}`
|
|
7920
|
+
`To add a skill: npx -y amdahl skills add ${ref}`
|
|
7766
7921
|
);
|
|
7767
7922
|
}
|
|
7768
7923
|
if (second === void 0) throw usageError(`\`amdahl ${first}\` needs a subcommand.`);
|
|
@@ -7771,7 +7926,7 @@ function parseCommandLine(argv) {
|
|
|
7771
7926
|
}
|
|
7772
7927
|
name = ALIASES[name] ?? name;
|
|
7773
7928
|
const spec = COMMAND_OPTIONS[name];
|
|
7774
|
-
if (!spec) throw usageError(`Unknown command \`${positionals.slice(0, words).join(" ")}
|
|
7929
|
+
if (!spec) throw usageError(`Unknown command \`${positionals.slice(0, words).join(" ")}\`.`, `Run \`${amdahlCommand("--help")}\` to see the commands.`);
|
|
7775
7930
|
const strict = strictParse(argv, { ...GLOBAL_OPTIONS, ...spec });
|
|
7776
7931
|
return { command: name, args: strict.positionals.slice(words), flags: strict.values };
|
|
7777
7932
|
}
|
|
@@ -7783,39 +7938,64 @@ function strictParse(argv, options) {
|
|
|
7783
7938
|
values: parsed.values
|
|
7784
7939
|
};
|
|
7785
7940
|
} catch (err) {
|
|
7786
|
-
throw usageError(
|
|
7941
|
+
throw usageError(...parseErrorMessage(err));
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
function parseErrorMessage(err) {
|
|
7945
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
7946
|
+
const code = err?.code;
|
|
7947
|
+
const help = `Run \`${amdahlCommand("--help")}\`.`;
|
|
7948
|
+
if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
7949
|
+
const option = /'(-[^']*)'/.exec(message2)?.[1] ?? message2;
|
|
7950
|
+
return [`Unknown option ${option}.`, help];
|
|
7787
7951
|
}
|
|
7952
|
+
if (code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") {
|
|
7953
|
+
const option = /'(--?[\w-]+)/.exec(message2)?.[1];
|
|
7954
|
+
if (option && /argument missing/.test(message2)) return [`${option} needs a value.`, help];
|
|
7955
|
+
if (option) return [`${option} takes no value.`, help];
|
|
7956
|
+
}
|
|
7957
|
+
return [message2, help];
|
|
7788
7958
|
}
|
|
7789
7959
|
var USAGE = `Usage: amdahl <command> [options]
|
|
7790
7960
|
|
|
7791
7961
|
What you can do:
|
|
7792
|
-
amdahl Set up Claude Code, Cursor or Codex,
|
|
7793
|
-
|
|
7794
|
-
|
|
7962
|
+
amdahl Set up Claude Code, Cursor or Codex, or try
|
|
7963
|
+
Amdahl in this terminal. In a terminal it
|
|
7964
|
+
asks which. Run by an agent or with no
|
|
7965
|
+
terminal, it sets up that agent, or every
|
|
7966
|
+
client it finds, and asks nothing (also:
|
|
7967
|
+
amdahl setup)
|
|
7795
7968
|
amdahl optimize draft.md Rewrite an outbound email or LinkedIn message
|
|
7796
|
-
|
|
7969
|
+
(with no file, at a terminal, paste it)
|
|
7970
|
+
amdahl status Check that this workspace can optimize
|
|
7797
7971
|
amdahl keys create --name ci Make an API key for a server or CI
|
|
7798
|
-
amdahl install claude-code Connect
|
|
7972
|
+
amdahl install claude-code Connect one AI client (also codex, cursor)
|
|
7799
7973
|
amdahl skills add <publisher>/<skill>
|
|
7800
|
-
Add a skill from https://amdahl.ai/skills
|
|
7801
|
-
its SKILL.md, or its
|
|
7802
|
-
|
|
7974
|
+
Add a skill from https://amdahl.ai/skills
|
|
7975
|
+
to this project: its SKILL.md, or its folder
|
|
7976
|
+
Everything Amdahl can do: https://docs.amdahl.ai/skills/amdahl/SKILL.md
|
|
7803
7977
|
|
|
7804
7978
|
Commands:
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7979
|
+
setup [--no-browser] The guided setup (bare amdahl)
|
|
7980
|
+
login [--workspace <slug>] [--no-browser] [--port <n>]
|
|
7981
|
+
Sign in (alias: auth login)
|
|
7982
|
+
logout [--all] Sign out (alias: auth logout)
|
|
7983
|
+
whoami [--show-token] Who you are (alias: auth status)
|
|
7984
|
+
auth token Print a fresh access token
|
|
7985
|
+
status Can this workspace optimize?
|
|
7986
|
+
optimize [<file>... | -] [--channel email|linkedin]
|
|
7987
|
+
[--evidence off|workspace] [--on-unsupported flag|remove]
|
|
7988
|
+
[--tries] [--context <file.json>]
|
|
7989
|
+
keys create --name <name> [--preset read-only|agent|internal|admin]
|
|
7990
|
+
[--expires 30d|90d|365d] [--no-browser]
|
|
7813
7991
|
keys list
|
|
7814
7992
|
keys revoke <id|prefix> [--yes] [--no-browser]
|
|
7815
7993
|
workspace list | workspace use <profile>
|
|
7816
7994
|
install claude-code|codex|cursor [--print] [--force]
|
|
7817
|
-
skills add <publisher>/<skill> [-y] [-g] [--agents] [--force]
|
|
7818
|
-
|
|
7995
|
+
skills add <publisher>/<skill> [-y] [-g] [--agents] [--force]
|
|
7996
|
+
[--accept-warning] [--site <origin>]
|
|
7997
|
+
In a terminal it asks one question: this project (Enter) or
|
|
7998
|
+
every project.
|
|
7819
7999
|
-y add without asking, to this project
|
|
7820
8000
|
-g every project (~/.claude/skills), not just this one
|
|
7821
8001
|
--agents also .agents/skills, for Codex, Cursor and other agents
|
|
@@ -7825,14 +8005,19 @@ Commands:
|
|
|
7825
8005
|
after printing the warning. Without it, a terminal asks
|
|
7826
8006
|
(Cancel or Install anyway); -y, --json or no terminal
|
|
7827
8007
|
stops (exit 9)
|
|
8008
|
+
help Show this help
|
|
7828
8009
|
|
|
7829
8010
|
Global options:
|
|
7830
|
-
--profile <name> Use a saved profile (default: AMDAHL_PROFILE, then the
|
|
7831
|
-
|
|
7832
|
-
--api-
|
|
8011
|
+
--profile <name> Use a saved profile (default: AMDAHL_PROFILE, then the
|
|
8012
|
+
default profile)
|
|
8013
|
+
--api-url <url> API host (default: AMDAHL_API_URL, then the profile,
|
|
8014
|
+
then https://app.amdahl.ai)
|
|
8015
|
+
--api-key <key> Use an API key for this command (prefer AMDAHL_KEY: argv
|
|
8016
|
+
is visible to other users)
|
|
7833
8017
|
--json Data on stdout as JSON, messages on stderr
|
|
7834
8018
|
--no-color No colour (NO_COLOR is honoured too)
|
|
7835
|
-
--verbose Say more: files written, the profile, where the sign-in
|
|
8019
|
+
--verbose Say more: files written, the profile, where the sign-in
|
|
8020
|
+
is stored
|
|
7836
8021
|
-h, --help Show this help
|
|
7837
8022
|
-v, --version Show the version
|
|
7838
8023
|
`;
|
|
@@ -8031,7 +8216,7 @@ async function send(url, init) {
|
|
|
8031
8216
|
return await fetch(url, init);
|
|
8032
8217
|
} catch (err) {
|
|
8033
8218
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
8034
|
-
throw new CliError("network_error", `Could not reach ${new URL(url).host}: ${message2}`, EXIT.network);
|
|
8219
|
+
throw new CliError("network_error", `Could not reach ${new URL(url).host}: ${message2}`, EXIT.network, NETWORK_HINT);
|
|
8035
8220
|
}
|
|
8036
8221
|
}
|
|
8037
8222
|
async function discover(apiUrl) {
|
|
@@ -8142,7 +8327,7 @@ function signInAgainLine(reason, url) {
|
|
|
8142
8327
|
function noBusinessMessage(consoleUrl, mayCreate, reason) {
|
|
8143
8328
|
switch (reason) {
|
|
8144
8329
|
case void 0:
|
|
8145
|
-
return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run
|
|
8330
|
+
return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run \`${amdahlCommand("login")}\` again.` : `You're signed in, but you don't have access yet. Amdahl is in beta: join the waitlist at ${consoleUrl}/new, then run \`${amdahlCommand("login")}\` again once you're in.`;
|
|
8146
8331
|
case "domain_has_workspace":
|
|
8147
8332
|
return `Your company already has an Amdahl workspace. Ask its admins to let you in at ${consoleUrl}, then try again.`;
|
|
8148
8333
|
case "invite_only":
|
|
@@ -8178,8 +8363,8 @@ async function startLoopback(io, input) {
|
|
|
8178
8363
|
let refused = null;
|
|
8179
8364
|
let settle;
|
|
8180
8365
|
let fail;
|
|
8181
|
-
const result = new Promise((
|
|
8182
|
-
settle =
|
|
8366
|
+
const result = new Promise((resolve2, reject) => {
|
|
8367
|
+
settle = resolve2;
|
|
8183
8368
|
fail = reject;
|
|
8184
8369
|
});
|
|
8185
8370
|
result.catch(() => {
|
|
@@ -8195,7 +8380,7 @@ async function startLoopback(io, input) {
|
|
|
8195
8380
|
res.end(page(body));
|
|
8196
8381
|
};
|
|
8197
8382
|
if (url.searchParams.get("state") !== input.state) {
|
|
8198
|
-
send2(400,
|
|
8383
|
+
send2(400, `<p>This sign-in did not match the one your terminal started. Run <code>${amdahlCommand("login")}</code> again.</p>`);
|
|
8199
8384
|
fail(new CliError("state_mismatch", "The sign-in response did not match this login (state mismatch).", EXIT.general));
|
|
8200
8385
|
return;
|
|
8201
8386
|
}
|
|
@@ -8225,7 +8410,7 @@ async function startLoopback(io, input) {
|
|
|
8225
8410
|
settle({ kind: "error", error: "invalid_request", description: "The callback carried no code." });
|
|
8226
8411
|
}
|
|
8227
8412
|
});
|
|
8228
|
-
await new Promise((
|
|
8413
|
+
await new Promise((resolve2, reject) => {
|
|
8229
8414
|
server.once(
|
|
8230
8415
|
"error",
|
|
8231
8416
|
(err) => reject(
|
|
@@ -8237,12 +8422,12 @@ async function startLoopback(io, input) {
|
|
|
8237
8422
|
)
|
|
8238
8423
|
)
|
|
8239
8424
|
);
|
|
8240
|
-
server.listen(input.port, "127.0.0.1", () =>
|
|
8425
|
+
server.listen(input.port, "127.0.0.1", () => resolve2());
|
|
8241
8426
|
});
|
|
8242
8427
|
const port = server.address().port;
|
|
8243
8428
|
const timer = setTimeout(() => {
|
|
8244
8429
|
fail(
|
|
8245
|
-
refused ? new CliError("no_business", noBusinessMessage(input.consoleUrl, false, refused), EXIT.noWorkspace) : new CliError("login_timeout", "Timed out waiting for the browser sign-in.", EXIT.general,
|
|
8430
|
+
refused ? new CliError("no_business", noBusinessMessage(input.consoleUrl, false, refused), EXIT.noWorkspace) : new CliError("login_timeout", "Timed out waiting for the browser sign-in.", EXIT.general, `Run \`${amdahlCommand("login")}\` again.`)
|
|
8246
8431
|
);
|
|
8247
8432
|
}, input.timeoutMs);
|
|
8248
8433
|
timer.unref?.();
|
|
@@ -8269,7 +8454,7 @@ function consoleUrlFor(io, apiUrl) {
|
|
|
8269
8454
|
}
|
|
8270
8455
|
|
|
8271
8456
|
// src/secrets.ts
|
|
8272
|
-
import { chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, rmSync, statSync } from "fs";
|
|
8457
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, rmSync, statSync as statSync2 } from "fs";
|
|
8273
8458
|
import { join as join2 } from "path";
|
|
8274
8459
|
var KEYCHAIN_SERVICE = "amdahl-cli";
|
|
8275
8460
|
function keychainAccount(apiUrl, profile) {
|
|
@@ -8362,7 +8547,7 @@ function fileStore(io, note) {
|
|
|
8362
8547
|
const checkMode = () => {
|
|
8363
8548
|
if (checked || io.platform === "win32" || !existsSync2(path)) return;
|
|
8364
8549
|
checked = true;
|
|
8365
|
-
if ((
|
|
8550
|
+
if ((statSync2(path).mode & 63) === 0) return;
|
|
8366
8551
|
try {
|
|
8367
8552
|
chmodSync2(path, 384);
|
|
8368
8553
|
} catch {
|
|
@@ -8371,7 +8556,7 @@ function fileStore(io, note) {
|
|
|
8371
8556
|
return;
|
|
8372
8557
|
}
|
|
8373
8558
|
io.stderr(
|
|
8374
|
-
`Warning: ${path} was readable by other users on this machine. It is private now; to be safe, run
|
|
8559
|
+
`Warning: ${path} was readable by other users on this machine. It is private now; to be safe, run \`${amdahlCommand("logout")}\` and sign in again.
|
|
8375
8560
|
`
|
|
8376
8561
|
);
|
|
8377
8562
|
};
|
|
@@ -8503,7 +8688,7 @@ async function requireOAuthCredential(ctx) {
|
|
|
8503
8688
|
"oauth_required",
|
|
8504
8689
|
`This command needs your own sign-in, but a credential from ${winner === "flag" ? "--api-key" : winner} takes precedence.`,
|
|
8505
8690
|
EXIT.forbidden,
|
|
8506
|
-
|
|
8691
|
+
`Unset it and run \`${amdahlCommand("login")}\`.`
|
|
8507
8692
|
);
|
|
8508
8693
|
}
|
|
8509
8694
|
return profileCredential(ctx);
|
|
@@ -8511,20 +8696,20 @@ async function requireOAuthCredential(ctx) {
|
|
|
8511
8696
|
async function profileCredential(ctx) {
|
|
8512
8697
|
const { profile, profileName } = ctx;
|
|
8513
8698
|
if (!profile || !profileName) {
|
|
8514
|
-
throw new CliError("not_signed_in",
|
|
8699
|
+
throw new CliError("not_signed_in", notSignedIn(), EXIT.unauthenticated);
|
|
8515
8700
|
}
|
|
8516
8701
|
if (normaliseApiUrl(profile.api_url) !== ctx.apiUrl) {
|
|
8517
8702
|
throw new CliError(
|
|
8518
8703
|
"profile_host_mismatch",
|
|
8519
8704
|
`Profile "${profileName}" is signed in to ${profile.api_url}, not ${ctx.apiUrl}.`,
|
|
8520
8705
|
EXIT.usage,
|
|
8521
|
-
|
|
8706
|
+
`Drop --api-url / AMDAHL_API_URL, or run \`${amdahlCommand("login")}\` against that host.`
|
|
8522
8707
|
);
|
|
8523
8708
|
}
|
|
8524
8709
|
const account = keychainAccount(profile.api_url, profileName);
|
|
8525
8710
|
const store = ctx.secrets();
|
|
8526
8711
|
const secrets = await store.get(account);
|
|
8527
|
-
if (!secrets) throw new CliError("not_signed_in",
|
|
8712
|
+
if (!secrets) throw new CliError("not_signed_in", notSignedIn(), EXIT.unauthenticated);
|
|
8528
8713
|
const left = Date.parse(secrets.access_expires_at) - ctx.io.now();
|
|
8529
8714
|
if (Number.isFinite(left) && left >= 6e4) {
|
|
8530
8715
|
return { source: "profile", token: secrets.access_token, profile: profileName };
|
|
@@ -8536,7 +8721,7 @@ async function profileCredential(ctx) {
|
|
|
8536
8721
|
"invalid_grant",
|
|
8537
8722
|
"Your sign-in has expired.",
|
|
8538
8723
|
EXIT.unauthenticated,
|
|
8539
|
-
|
|
8724
|
+
`Run \`${amdahlCommand("login")}\` again.`
|
|
8540
8725
|
);
|
|
8541
8726
|
}
|
|
8542
8727
|
await store.set(account, refreshed);
|
|
@@ -8557,16 +8742,16 @@ function errorDetails(body) {
|
|
|
8557
8742
|
}
|
|
8558
8743
|
function toCliError(err) {
|
|
8559
8744
|
if (err instanceof CliError) return err;
|
|
8560
|
-
if (err instanceof PromptCancelled) return new CliError("cancelled", "Stopped.", EXIT.cancelled);
|
|
8745
|
+
if (err instanceof PromptCancelled) return new CliError("cancelled", "Stopped. Nothing changed.", EXIT.cancelled);
|
|
8561
8746
|
if (err instanceof AmdahlApiError) {
|
|
8562
8747
|
const exit = exitForHttp(err.statusCode, err.errorCode);
|
|
8563
8748
|
const details = errorDetails(err.responseBody);
|
|
8564
8749
|
const retry = details.retry_after_seconds;
|
|
8565
|
-
const hint = exit === EXIT.unauthenticated ?
|
|
8750
|
+
const hint = exit === EXIT.unauthenticated ? `Run \`${amdahlCommand("login")}\`, or check AMDAHL_KEY.` : typeof retry === "number" ? `Try again in ${retry} seconds.` : void 0;
|
|
8566
8751
|
return new CliError(err.errorCode ?? `http_${err.statusCode}`, err.message, exit, hint);
|
|
8567
8752
|
}
|
|
8568
8753
|
const message2 = err instanceof Error ? err.message : String(err);
|
|
8569
|
-
return new CliError("network_error", `Could not reach the Amdahl API: ${message2}`, EXIT.network);
|
|
8754
|
+
return new CliError("network_error", `Could not reach the Amdahl API: ${message2}`, EXIT.network, NETWORK_HINT);
|
|
8570
8755
|
}
|
|
8571
8756
|
async function call(fn) {
|
|
8572
8757
|
try {
|
|
@@ -8668,13 +8853,15 @@ async function browserSignIn(ctx) {
|
|
|
8668
8853
|
`);
|
|
8669
8854
|
};
|
|
8670
8855
|
let cancelled;
|
|
8856
|
+
let fallbackLine = null;
|
|
8671
8857
|
if (flags["no-browser"]) {
|
|
8672
8858
|
showLink("Open this URL in a browser to sign in:");
|
|
8673
8859
|
} else {
|
|
8674
8860
|
out.info("Opening your browser\u2026");
|
|
8675
8861
|
if (!ctx.nonInteractive && io.stderrIsTTY) {
|
|
8676
8862
|
const fallback = supportsHyperlinks(io.env, out.color) ? hyperlink("Open the sign-in page", url) : "Press Enter for the link.";
|
|
8677
|
-
|
|
8863
|
+
fallbackLine = `Browser didn't open? ${fallback}`;
|
|
8864
|
+
out.info(fallbackLine);
|
|
8678
8865
|
cancelled = new Promise((_, reject) => {
|
|
8679
8866
|
stopKeys = io.listenForEnter(
|
|
8680
8867
|
() => showLink("Open this link to sign in:"),
|
|
@@ -8688,6 +8875,7 @@ async function browserSignIn(ctx) {
|
|
|
8688
8875
|
}
|
|
8689
8876
|
const callback = await (cancelled ? Promise.race([loopback.result, cancelled]) : loopback.result);
|
|
8690
8877
|
stopKeys();
|
|
8878
|
+
if (fallbackLine && !linkShown && callback.kind === "code") io.stderr(eraseUp(screenLines(fallbackLine, io.columns())));
|
|
8691
8879
|
linkShown = true;
|
|
8692
8880
|
if (callback.kind === "no_business") {
|
|
8693
8881
|
throw new CliError("no_business", noBusinessMessage(consoleUrl, callback.mayCreate, callback.reason), EXIT.noWorkspace);
|
|
@@ -8719,6 +8907,8 @@ async function browserSignIn(ctx) {
|
|
|
8719
8907
|
};
|
|
8720
8908
|
ctx.config.default_profile = name;
|
|
8721
8909
|
ctx.saveConfig();
|
|
8910
|
+
ctx.profileName = name;
|
|
8911
|
+
ctx.profile = ctx.config.profiles[name] ?? null;
|
|
8722
8912
|
return { profile: name, me, workspace: ws };
|
|
8723
8913
|
} finally {
|
|
8724
8914
|
stopKeys();
|
|
@@ -8728,6 +8918,10 @@ async function browserSignIn(ctx) {
|
|
|
8728
8918
|
function signedInLine(ctx, { profile, me, workspace }) {
|
|
8729
8919
|
return `Signed in as ${printable(me.email ?? me.user_id)} to ${ctx.out.bold(printable(workspace.name))} (${printable(workspace.slug)}). Profile: ${profile}.`;
|
|
8730
8920
|
}
|
|
8921
|
+
function loginNextLine() {
|
|
8922
|
+
return `Next: run \`${amdahlCommand("optimize")}\` and paste an email,
|
|
8923
|
+
or \`${amdahlCommand()}\` to add Amdahl to Claude Code, Cursor or Codex.`;
|
|
8924
|
+
}
|
|
8731
8925
|
async function login(ctx) {
|
|
8732
8926
|
const signedIn = await browserSignIn(ctx);
|
|
8733
8927
|
const { profile, me, workspace } = signedIn;
|
|
@@ -8740,7 +8934,8 @@ async function login(ctx) {
|
|
|
8740
8934
|
scopes: me.scopes,
|
|
8741
8935
|
api_url: ctx.apiUrl
|
|
8742
8936
|
},
|
|
8743
|
-
signedInLine(ctx, signedIn)
|
|
8937
|
+
`${signedInLine(ctx, signedIn)}
|
|
8938
|
+
${loginNextLine()}`
|
|
8744
8939
|
);
|
|
8745
8940
|
return EXIT.ok;
|
|
8746
8941
|
}
|
|
@@ -8827,8 +9022,8 @@ async function authToken(ctx) {
|
|
|
8827
9022
|
}
|
|
8828
9023
|
|
|
8829
9024
|
// src/commands/install.ts
|
|
8830
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
8831
|
-
import { dirname, join as join4 } from "path";
|
|
9025
|
+
import { chmodSync as chmodSync3, copyFileSync, existsSync as existsSync3, lstatSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readlinkSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
9026
|
+
import { basename, dirname, join as join4, resolve } from "path";
|
|
8832
9027
|
|
|
8833
9028
|
// src/skill.ts
|
|
8834
9029
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -8882,22 +9077,140 @@ async function onPath(ctx, command) {
|
|
|
8882
9077
|
return res.code === 0;
|
|
8883
9078
|
}
|
|
8884
9079
|
var CLIENT_SIGNS = {
|
|
8885
|
-
"claude-code": { command: "claude",
|
|
8886
|
-
codex: { command: "codex",
|
|
8887
|
-
cursor: { command: "cursor",
|
|
9080
|
+
"claude-code": { command: "claude", paths: [".claude", ".claude.json"] },
|
|
9081
|
+
codex: { command: "codex", paths: [".codex"] },
|
|
9082
|
+
cursor: { command: "cursor", paths: [".cursor"] }
|
|
8888
9083
|
};
|
|
8889
9084
|
async function detectClients(ctx) {
|
|
8890
9085
|
const found = await Promise.all(
|
|
8891
9086
|
CLIENTS.map(async (client) => {
|
|
8892
|
-
const { command,
|
|
8893
|
-
return existsSync3(join4(ctx.io.homedir,
|
|
9087
|
+
const { command, paths } = CLIENT_SIGNS[client];
|
|
9088
|
+
return paths.some((p) => existsSync3(join4(ctx.io.homedir, p))) || await onPath(ctx, command);
|
|
8894
9089
|
})
|
|
8895
9090
|
);
|
|
8896
9091
|
return Object.fromEntries(CLIENTS.map((client, i) => [client, found[i]]));
|
|
8897
9092
|
}
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
9093
|
+
function claudeConfigPath(ctx) {
|
|
9094
|
+
return join4(ctx.io.env.CLAUDE_CONFIG_DIR || ctx.io.homedir, ".claude.json");
|
|
9095
|
+
}
|
|
9096
|
+
var routes = /* @__PURE__ */ new WeakMap();
|
|
9097
|
+
function claudeRoute(ctx) {
|
|
9098
|
+
let route = routes.get(ctx);
|
|
9099
|
+
if (!route) {
|
|
9100
|
+
route = findClaude(ctx).then((command) => command ? { kind: "command", command } : { kind: "file", path: claudeConfigPath(ctx) });
|
|
9101
|
+
routes.set(ctx, route);
|
|
9102
|
+
}
|
|
9103
|
+
return route;
|
|
9104
|
+
}
|
|
9105
|
+
async function findClaude(ctx) {
|
|
9106
|
+
const { io } = ctx;
|
|
9107
|
+
if (await onPath(ctx, "claude")) return "claude";
|
|
9108
|
+
const win = io.platform === "win32";
|
|
9109
|
+
const local = [join4(io.homedir, ".local", "bin", win ? "claude.exe" : "claude"), join4(io.homedir, ".claude", "local", win ? "claude.exe" : "claude")];
|
|
9110
|
+
const system = win ? [] : ["/opt/homebrew/bin/claude", "/usr/local/bin/claude"];
|
|
9111
|
+
const found = [...local, ...system].find((path) => io.isExecutable(path));
|
|
9112
|
+
if (found) return found;
|
|
9113
|
+
if (win) return null;
|
|
9114
|
+
const prefix2 = io.env.npm_config_global_prefix || await npmGlobalPrefix(ctx);
|
|
9115
|
+
const npmClaude = prefix2 ? join4(prefix2, "bin", "claude") : null;
|
|
9116
|
+
return npmClaude && io.isExecutable(npmClaude) ? npmClaude : null;
|
|
9117
|
+
}
|
|
9118
|
+
async function npmGlobalPrefix(ctx) {
|
|
9119
|
+
const res = await ctx.io.run("npm", ["prefix", "-g"]);
|
|
9120
|
+
const prefix2 = res.code === 0 ? res.stdout.trim() : "";
|
|
9121
|
+
return prefix2 || null;
|
|
9122
|
+
}
|
|
9123
|
+
async function claudeHasAmdahl(ctx, command, mcpUrl) {
|
|
9124
|
+
let answer = claudeAnswers.get(ctx);
|
|
9125
|
+
if (!answer) {
|
|
9126
|
+
answer = ctx.io.run(command, ["mcp", "get", "amdahl"]).then((res) => res.code === 0 && res.stdout.split("\n").some((line) => line.trim() === `URL: ${mcpUrl}`));
|
|
9127
|
+
claudeAnswers.set(ctx, answer);
|
|
9128
|
+
}
|
|
9129
|
+
return answer;
|
|
9130
|
+
}
|
|
9131
|
+
var claudeAnswers = /* @__PURE__ */ new WeakMap();
|
|
9132
|
+
var CONFIG_HINT = "Fix the file, or move it away, then run the same command again.";
|
|
9133
|
+
function readJsonObject(path) {
|
|
9134
|
+
if (!existsSync3(path)) return { config: {}, existed: false };
|
|
9135
|
+
const raw = readFileSync3(path, "utf8");
|
|
9136
|
+
if (!raw.trim()) return { config: {}, existed: true };
|
|
9137
|
+
let config;
|
|
9138
|
+
try {
|
|
9139
|
+
config = JSON.parse(raw);
|
|
9140
|
+
} catch {
|
|
9141
|
+
throw new CliError("invalid_config", `${path} is not valid JSON; not changing it.`, EXIT.general, CONFIG_HINT);
|
|
9142
|
+
}
|
|
9143
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
9144
|
+
throw new CliError("invalid_config", `${path} does not hold a JSON object; not changing it.`, EXIT.general, CONFIG_HINT);
|
|
9145
|
+
}
|
|
9146
|
+
return { config, existed: true };
|
|
9147
|
+
}
|
|
9148
|
+
function readMcpConfig(path) {
|
|
9149
|
+
const { config, existed } = readJsonObject(path);
|
|
9150
|
+
const servers = config.mcpServers;
|
|
9151
|
+
if (servers === void 0) return { config, servers: {}, existed };
|
|
9152
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
9153
|
+
throw new CliError("invalid_config", `The "mcpServers" in ${path} is not a JSON object; not changing it.`, EXIT.general, CONFIG_HINT);
|
|
9154
|
+
}
|
|
9155
|
+
return { config, servers, existed };
|
|
9156
|
+
}
|
|
9157
|
+
function claudeFileHasAmdahl(path, mcpUrl) {
|
|
9158
|
+
try {
|
|
9159
|
+
const servers = readMcpConfig(path).servers;
|
|
9160
|
+
return servers.amdahl?.url === mcpUrl;
|
|
9161
|
+
} catch {
|
|
9162
|
+
return false;
|
|
9163
|
+
}
|
|
9164
|
+
}
|
|
9165
|
+
function writeTarget(path) {
|
|
9166
|
+
let target = path;
|
|
9167
|
+
for (let hops = 0; hops < 40; hops++) {
|
|
9168
|
+
let link;
|
|
9169
|
+
try {
|
|
9170
|
+
link = lstatSync(target).isSymbolicLink();
|
|
9171
|
+
} catch {
|
|
9172
|
+
return target;
|
|
9173
|
+
}
|
|
9174
|
+
if (!link) return existsSync3(target) ? realpathSync(target) : target;
|
|
9175
|
+
target = resolve(dirname(target), readlinkSync(target));
|
|
9176
|
+
}
|
|
9177
|
+
throw new CliError("invalid_config", `${path} is a chain of more than 40 symbolic links; not changing it.`, EXIT.general, CONFIG_HINT);
|
|
9178
|
+
}
|
|
9179
|
+
function writeFileAtomic(path, text2) {
|
|
9180
|
+
const target = writeTarget(path);
|
|
9181
|
+
const mode = existsSync3(target) ? statSync3(target).mode & 511 : 384;
|
|
9182
|
+
mkdirSync2(dirname(target), { recursive: true });
|
|
9183
|
+
const tmp = join4(dirname(target), `.${basename(target)}.amdahl-${process.pid}.tmp`);
|
|
9184
|
+
try {
|
|
9185
|
+
writeFileSync2(tmp, text2, { mode });
|
|
9186
|
+
chmodSync3(tmp, mode);
|
|
9187
|
+
renameSync2(tmp, target);
|
|
9188
|
+
} catch (err) {
|
|
9189
|
+
rmSync2(tmp, { force: true });
|
|
9190
|
+
throw err;
|
|
9191
|
+
}
|
|
9192
|
+
}
|
|
9193
|
+
function writeClaudeConfig(ctx, path, mcpUrl, command) {
|
|
9194
|
+
return updateConfig(path, () => {
|
|
9195
|
+
const { config, servers, existed } = readMcpConfig(path);
|
|
9196
|
+
const data = { ok: true, client: "claude-code", action: "unchanged", path, command };
|
|
9197
|
+
if (servers.amdahl?.url === mcpUrl) {
|
|
9198
|
+
return { data, human: `${path} already points Claude Code at ${mcpUrl}; nothing changed.`, notes: [] };
|
|
9199
|
+
}
|
|
9200
|
+
config.mcpServers = { ...servers, amdahl: { type: "http", url: mcpUrl } };
|
|
9201
|
+
const backup = `${writeTarget(path)}.amdahl-backup`;
|
|
9202
|
+
ctx.io.uninterruptible(() => {
|
|
9203
|
+
if (existed) copyFileSync(writeTarget(path), backup);
|
|
9204
|
+
writeFileAtomic(path, `${JSON.stringify(config, null, 2)}
|
|
9205
|
+
`);
|
|
9206
|
+
});
|
|
9207
|
+
const saved = existed ? ` The file as it was is saved at ${backup}.` : "";
|
|
9208
|
+
return {
|
|
9209
|
+
data: { ...data, action: "wrote", ...existed ? { backup } : {} },
|
|
9210
|
+
human: `Added Amdahl to Claude Code for all your projects, in ${path} (\`claude\` was not found).${saved} Run /mcp in Claude Code to sign in. If amdahl is not listed there, restart Claude Code first.`,
|
|
9211
|
+
notes: []
|
|
9212
|
+
};
|
|
9213
|
+
});
|
|
8901
9214
|
}
|
|
8902
9215
|
function updateConfig(path, fn) {
|
|
8903
9216
|
try {
|
|
@@ -8908,12 +9221,13 @@ function updateConfig(path, fn) {
|
|
|
8908
9221
|
throw new CliError("write_failed", `Could not update ${path} (${reason}).`, EXIT.general);
|
|
8909
9222
|
}
|
|
8910
9223
|
}
|
|
8911
|
-
async function
|
|
8912
|
-
const skill = skillPath(client, ctx.io.homedir);
|
|
8913
|
-
const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
|
|
8914
|
-
if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
|
|
9224
|
+
async function hasAmdahl(ctx, client) {
|
|
8915
9225
|
const mcpUrl = `${ctx.apiUrl}/mcp`;
|
|
8916
|
-
if (client === "claude-code")
|
|
9226
|
+
if (client === "claude-code") {
|
|
9227
|
+
if (claudeFileHasAmdahl(claudeConfigPath(ctx), mcpUrl)) return true;
|
|
9228
|
+
const route = await claudeRoute(ctx);
|
|
9229
|
+
return route.kind === "command" && await claudeHasAmdahl(ctx, route.command, mcpUrl);
|
|
9230
|
+
}
|
|
8917
9231
|
if (client === "codex") {
|
|
8918
9232
|
const path = codexConfigPath(ctx.io.homedir);
|
|
8919
9233
|
return existsSync3(path) && codexHasAmdahl(readFileSync3(path, "utf8"));
|
|
@@ -8925,6 +9239,12 @@ async function isInstalled(ctx, client) {
|
|
|
8925
9239
|
return false;
|
|
8926
9240
|
}
|
|
8927
9241
|
}
|
|
9242
|
+
async function isInstalled(ctx, client) {
|
|
9243
|
+
const skill = skillPath(client, ctx.io.homedir);
|
|
9244
|
+
const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
|
|
9245
|
+
if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
|
|
9246
|
+
return hasAmdahl(ctx, client);
|
|
9247
|
+
}
|
|
8928
9248
|
async function installClient(ctx, client, print) {
|
|
8929
9249
|
const mcp = await installMcp(ctx, client, print);
|
|
8930
9250
|
return { mcp, skill: installSkill(ctx, client, print) };
|
|
@@ -8971,14 +9291,14 @@ async function installMcp(ctx, client, print) {
|
|
|
8971
9291
|
if (client === "claude-code") {
|
|
8972
9292
|
const args = ["mcp", "add", "--scope", "user", "--transport", "http", "amdahl", mcpUrl];
|
|
8973
9293
|
const command = `claude ${args.join(" ")}`;
|
|
8974
|
-
if (print
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
if (await claudeHasAmdahl(ctx, mcpUrl)) {
|
|
9294
|
+
if (print) return { data: { ok: true, client, action: "printed", command }, human: command, notes: [] };
|
|
9295
|
+
const route = await claudeRoute(ctx);
|
|
9296
|
+
if (route.kind === "file") return writeClaudeConfig(ctx, route.path, mcpUrl, command);
|
|
9297
|
+
if (claudeFileHasAmdahl(claudeConfigPath(ctx), mcpUrl) || await claudeHasAmdahl(ctx, route.command, mcpUrl)) {
|
|
8979
9298
|
return { data: { ok: true, client, action: "unchanged", command }, human: `Claude Code already has Amdahl at ${mcpUrl}; nothing changed.`, notes: [] };
|
|
8980
9299
|
}
|
|
8981
|
-
|
|
9300
|
+
claudeAnswers.delete(ctx);
|
|
9301
|
+
const res = await io.run(route.command, args);
|
|
8982
9302
|
if (res.code !== 0) {
|
|
8983
9303
|
throw new CliError("install_failed", `\`${command}\` failed: ${res.stderr.trim() || `exit ${res.code}`}`, EXIT.general);
|
|
8984
9304
|
}
|
|
@@ -8996,9 +9316,8 @@ ${block}`, notes: [] };
|
|
|
8996
9316
|
if (codexHasAmdahl(current)) {
|
|
8997
9317
|
return { data: { ok: true, client, action: "unchanged", path: path2 }, human: `${path2} already has [mcp_servers.amdahl]; nothing changed.`, notes: [] };
|
|
8998
9318
|
}
|
|
8999
|
-
mkdirSync2(dirname(path2), { recursive: true });
|
|
9000
9319
|
const sep2 = current.length === 0 ? "" : current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
9001
|
-
|
|
9320
|
+
writeFileAtomic(path2, `${current}${sep2}${block}`);
|
|
9002
9321
|
return { data: { ok: true, client, action: "wrote", path: path2 }, human: `Added Amdahl to ${path2}. Run \`codex mcp login amdahl\` to sign in.`, notes: [] };
|
|
9003
9322
|
});
|
|
9004
9323
|
}
|
|
@@ -9009,28 +9328,33 @@ ${block}`, notes: [] };
|
|
|
9009
9328
|
${JSON.stringify({ mcpServers: { amdahl: entry } }, null, 2)}`, notes: [] };
|
|
9010
9329
|
}
|
|
9011
9330
|
return updateConfig(path, () => {
|
|
9012
|
-
|
|
9013
|
-
if (existsSync3(path)) {
|
|
9014
|
-
const raw = readFileSync3(path, "utf8");
|
|
9015
|
-
if (raw.trim()) {
|
|
9016
|
-
try {
|
|
9017
|
-
config = JSON.parse(raw);
|
|
9018
|
-
} catch {
|
|
9019
|
-
throw new CliError("invalid_config", `${path} is not valid JSON; not changing it.`, EXIT.general);
|
|
9020
|
-
}
|
|
9021
|
-
}
|
|
9022
|
-
}
|
|
9023
|
-
const servers = config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {};
|
|
9331
|
+
const { config, servers } = readMcpConfig(path);
|
|
9024
9332
|
if (JSON.stringify(servers.amdahl) === JSON.stringify(entry)) {
|
|
9025
9333
|
return { data: { ok: true, client, action: "unchanged", path }, human: `${path} already points at ${mcpUrl}; nothing changed.`, notes: [] };
|
|
9026
9334
|
}
|
|
9027
9335
|
config.mcpServers = { ...servers, amdahl: entry };
|
|
9028
|
-
|
|
9029
|
-
writeFileSync2(path, `${JSON.stringify(config, null, 2)}
|
|
9336
|
+
writeFileAtomic(path, `${JSON.stringify(config, null, 2)}
|
|
9030
9337
|
`);
|
|
9031
9338
|
return { data: { ok: true, client, action: "wrote", path }, human: `Added Amdahl to ${path}. Cursor asks you to sign in on first use.`, notes: [] };
|
|
9032
9339
|
});
|
|
9033
9340
|
}
|
|
9341
|
+
async function preflight(ctx, client) {
|
|
9342
|
+
let path = null;
|
|
9343
|
+
if (client === "cursor") path = cursorConfigPath(ctx.io.homedir);
|
|
9344
|
+
if (client === "claude-code") {
|
|
9345
|
+
const route = await claudeRoute(ctx);
|
|
9346
|
+
if (route.kind === "file") path = route.path;
|
|
9347
|
+
}
|
|
9348
|
+
if (!path) return null;
|
|
9349
|
+
try {
|
|
9350
|
+
readMcpConfig(path);
|
|
9351
|
+
return null;
|
|
9352
|
+
} catch (err) {
|
|
9353
|
+
if (err instanceof CliError) return err;
|
|
9354
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
9355
|
+
return new CliError("write_failed", `Could not read ${path} (${reason}).`, EXIT.general);
|
|
9356
|
+
}
|
|
9357
|
+
}
|
|
9034
9358
|
|
|
9035
9359
|
// src/keygen.ts
|
|
9036
9360
|
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
@@ -9098,7 +9422,7 @@ async function waitForDecision(ctx, client, req) {
|
|
|
9098
9422
|
}
|
|
9099
9423
|
async function keysCreate(ctx) {
|
|
9100
9424
|
const name = str(ctx.flags.name)?.trim();
|
|
9101
|
-
if (!name) throw usageError("--name is required.",
|
|
9425
|
+
if (!name) throw usageError("--name is required.", `Example: ${amdahlCommand('keys create --name "CI pipeline"')}`);
|
|
9102
9426
|
if (name.length > 80) throw usageError("--name must be at most 80 characters.");
|
|
9103
9427
|
const preset = str(ctx.flags.preset) ?? "read-only";
|
|
9104
9428
|
const bundle = PRESET_BUNDLES[preset];
|
|
@@ -9168,7 +9492,7 @@ function findKey(keys, ref) {
|
|
|
9168
9492
|
const matches = keys.filter((k) => ref.length >= 8 && k.key_prefix.startsWith(ref));
|
|
9169
9493
|
if (matches.length === 1 && matches[0]) return matches[0];
|
|
9170
9494
|
if (matches.length > 1) throw usageError(`"${ref}" matches ${matches.length} keys. Use the key id.`);
|
|
9171
|
-
throw new CliError("key_not_found", `No active key matches "${ref}".`, EXIT.general,
|
|
9495
|
+
throw new CliError("key_not_found", `No active key matches "${ref}".`, EXIT.general, `Run \`${amdahlCommand("keys list")}\`.`);
|
|
9172
9496
|
}
|
|
9173
9497
|
async function keysRevoke(ctx, args) {
|
|
9174
9498
|
const ref = args[0];
|
|
@@ -9204,6 +9528,7 @@ async function keysRevoke(ctx, args) {
|
|
|
9204
9528
|
|
|
9205
9529
|
// src/commands/optimize.ts
|
|
9206
9530
|
import { readFileSync as readFileSync4 } from "fs";
|
|
9531
|
+
var NEW_USER = "New to Amdahl? The sign-in sets up your free workspace.";
|
|
9207
9532
|
var OPTIMIZE_TIMEOUT_MS = 18e4;
|
|
9208
9533
|
var CHANNELS = ["email", "linkedin"];
|
|
9209
9534
|
var EVIDENCE = ["off", "workspace"];
|
|
@@ -9348,10 +9673,86 @@ function whatAmdahlDid(result) {
|
|
|
9348
9673
|
const kept = new Set([...lines].sort((a, b) => a.rank - b.rank).slice(0, MAX_LINES));
|
|
9349
9674
|
return lines.filter((l) => kept.has(l)).map((l) => l.line);
|
|
9350
9675
|
}
|
|
9676
|
+
var PASTE_PROMPT = "Paste an email or LinkedIn message, then press Enter on an empty line:";
|
|
9677
|
+
var VERSION_RULE = "\u2500\u2500 Amdahl's version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
|
|
9678
|
+
function nextTimeLine(ctx) {
|
|
9679
|
+
const file = amdahlCommand("optimize draft.txt");
|
|
9680
|
+
return ctx.io.platform === "darwin" ? `Next time: ${file},
|
|
9681
|
+
or pbpaste | ${amdahlCommand("optimize -")}` : `Next time: ${file}`;
|
|
9682
|
+
}
|
|
9683
|
+
function requestOptions(ctx) {
|
|
9684
|
+
const { flags } = ctx;
|
|
9685
|
+
const base = {};
|
|
9686
|
+
const channel = oneOf("channel", str(flags.channel), CHANNELS);
|
|
9687
|
+
const evidence = oneOf("evidence", str(flags.evidence), EVIDENCE);
|
|
9688
|
+
const onUnsupported = oneOf("on-unsupported", str(flags["on-unsupported"]), ON_UNSUPPORTED);
|
|
9689
|
+
if (channel) base.channel = channel;
|
|
9690
|
+
if (evidence) base.evidence = evidence;
|
|
9691
|
+
if (onUnsupported) base.on_unsupported = onUnsupported;
|
|
9692
|
+
if (flags.tries === true) base.include_tries = true;
|
|
9693
|
+
const contextPath = str(flags.context);
|
|
9694
|
+
if (contextPath) base.context = readContext(contextPath);
|
|
9695
|
+
return base;
|
|
9696
|
+
}
|
|
9697
|
+
async function optimizeOne(ctx, credential, message2, base, file) {
|
|
9698
|
+
try {
|
|
9699
|
+
return await withTimeout(
|
|
9700
|
+
apiClient(ctx, credential).request("/api/platform/v1/messages/optimize", {
|
|
9701
|
+
method: "POST",
|
|
9702
|
+
body: { message: message2, ...base }
|
|
9703
|
+
}),
|
|
9704
|
+
OPTIMIZE_TIMEOUT_MS,
|
|
9705
|
+
file
|
|
9706
|
+
);
|
|
9707
|
+
} catch (err) {
|
|
9708
|
+
throw toCliError(err);
|
|
9709
|
+
}
|
|
9710
|
+
}
|
|
9711
|
+
async function credentialOrSignIn(ctx) {
|
|
9712
|
+
try {
|
|
9713
|
+
return await resolveCredential(ctx);
|
|
9714
|
+
} catch (err) {
|
|
9715
|
+
if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
|
|
9716
|
+
}
|
|
9717
|
+
ctx.out.info(`Sign in first, then paste your message.
|
|
9718
|
+
${NEW_USER}`);
|
|
9719
|
+
const signedIn = await browserSignIn(ctx);
|
|
9720
|
+
ctx.out.info(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
|
|
9721
|
+
return resolveCredential(ctx);
|
|
9722
|
+
}
|
|
9723
|
+
async function optimizePasted(ctx) {
|
|
9724
|
+
const { io, out } = ctx;
|
|
9725
|
+
const base = requestOptions(ctx);
|
|
9726
|
+
const credential = await credentialOrSignIn(ctx);
|
|
9727
|
+
const message2 = await io.readPaste(PASTE_PROMPT);
|
|
9728
|
+
if (!message2.trim()) {
|
|
9729
|
+
out.info(`Nothing pasted, so nothing was sent.
|
|
9730
|
+
${nextTimeLine(ctx)}`);
|
|
9731
|
+
return EXIT.ok;
|
|
9732
|
+
}
|
|
9733
|
+
out.info("Amdahl is rewriting it. This can take a minute.");
|
|
9734
|
+
const result = await optimizeOne(ctx, credential, message2, base, "your message");
|
|
9735
|
+
if (result.ok === false) {
|
|
9736
|
+
if (out.json) out.result({ ok: false, file: "-", result });
|
|
9737
|
+
out.info(`Not optimized (${String(result.reason ?? "unknown")}): ${String(result.detail ?? "")}`);
|
|
9738
|
+
return EXIT.general;
|
|
9739
|
+
}
|
|
9740
|
+
if (out.json) {
|
|
9741
|
+
out.result({ ok: true, file: "-", result });
|
|
9742
|
+
return EXIT.ok;
|
|
9743
|
+
}
|
|
9744
|
+
out.info(VERSION_RULE);
|
|
9745
|
+
if (result.unchanged === true) out.info("No rewrite beat your draft; it is returned unchanged.");
|
|
9746
|
+
out.result({}, String(result.message ?? ""));
|
|
9747
|
+
const did = whatAmdahlDid(result);
|
|
9748
|
+
out.info([...did, nextTimeLine(ctx)].join("\n"));
|
|
9749
|
+
return EXIT.ok;
|
|
9750
|
+
}
|
|
9351
9751
|
async function optimize(ctx, args) {
|
|
9352
|
-
const { io, out
|
|
9752
|
+
const { io, out } = ctx;
|
|
9353
9753
|
let files = args;
|
|
9354
9754
|
if (files.length === 0) {
|
|
9755
|
+
if (io.stdinIsTTY && !ctx.nonInteractive) return optimizePasted(ctx);
|
|
9355
9756
|
if (io.stdinIsTTY) {
|
|
9356
9757
|
throw usageError(
|
|
9357
9758
|
"Give a file to optimize, or pipe the message on stdin.",
|
|
@@ -9361,16 +9762,7 @@ async function optimize(ctx, args) {
|
|
|
9361
9762
|
files = ["-"];
|
|
9362
9763
|
}
|
|
9363
9764
|
if (files.filter((f) => f === "-").length > 1) throw usageError("Stdin (-) can be read only once.");
|
|
9364
|
-
const base =
|
|
9365
|
-
const channel = oneOf("channel", str(flags.channel), CHANNELS);
|
|
9366
|
-
const evidence = oneOf("evidence", str(flags.evidence), EVIDENCE);
|
|
9367
|
-
const onUnsupported = oneOf("on-unsupported", str(flags["on-unsupported"]), ON_UNSUPPORTED);
|
|
9368
|
-
if (channel) base.channel = channel;
|
|
9369
|
-
if (evidence) base.evidence = evidence;
|
|
9370
|
-
if (onUnsupported) base.on_unsupported = onUnsupported;
|
|
9371
|
-
if (flags.tries === true) base.include_tries = true;
|
|
9372
|
-
const contextPath = str(flags.context);
|
|
9373
|
-
if (contextPath) base.context = readContext(contextPath);
|
|
9765
|
+
const base = requestOptions(ctx);
|
|
9374
9766
|
const inputs = [];
|
|
9375
9767
|
for (const file of files) {
|
|
9376
9768
|
let message2;
|
|
@@ -9383,23 +9775,10 @@ async function optimize(ctx, args) {
|
|
|
9383
9775
|
inputs.push({ file, message: message2 });
|
|
9384
9776
|
}
|
|
9385
9777
|
const credential = await resolveCredential(ctx);
|
|
9386
|
-
const client = apiClient(ctx, credential);
|
|
9387
9778
|
const results = [];
|
|
9388
9779
|
let exit = EXIT.ok;
|
|
9389
9780
|
for (const { file, message: message2 } of inputs) {
|
|
9390
|
-
|
|
9391
|
-
try {
|
|
9392
|
-
result = await withTimeout(
|
|
9393
|
-
client.request("/api/platform/v1/messages/optimize", {
|
|
9394
|
-
method: "POST",
|
|
9395
|
-
body: { message: message2, ...base }
|
|
9396
|
-
}),
|
|
9397
|
-
OPTIMIZE_TIMEOUT_MS,
|
|
9398
|
-
file
|
|
9399
|
-
);
|
|
9400
|
-
} catch (err) {
|
|
9401
|
-
throw toCliError(err);
|
|
9402
|
-
}
|
|
9781
|
+
const result = await optimizeOne(ctx, credential, message2, base, file);
|
|
9403
9782
|
const ok = result.ok !== false;
|
|
9404
9783
|
results.push({ ok, file, result });
|
|
9405
9784
|
if (!ok) {
|
|
@@ -9426,7 +9805,7 @@ import { dirname as dirname3, join as join6, relative, sep } from "path";
|
|
|
9426
9805
|
|
|
9427
9806
|
// src/commands/skills-install.ts
|
|
9428
9807
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
9429
|
-
import { chmodSync as
|
|
9808
|
+
import { chmodSync as chmodSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
9430
9809
|
import { dirname as dirname2, join as join5 } from "path";
|
|
9431
9810
|
function skillDirs(targets, name) {
|
|
9432
9811
|
const dirs = [];
|
|
@@ -9453,7 +9832,7 @@ function writeFailure(path, err, written = []) {
|
|
|
9453
9832
|
function sameFolder(dir, archive) {
|
|
9454
9833
|
let top;
|
|
9455
9834
|
try {
|
|
9456
|
-
top =
|
|
9835
|
+
top = lstatSync2(dir);
|
|
9457
9836
|
} catch (err) {
|
|
9458
9837
|
if (err.code === "ENOENT") return null;
|
|
9459
9838
|
throw writeFailure(dir, err);
|
|
@@ -9472,7 +9851,7 @@ function sameFolder(dir, archive) {
|
|
|
9472
9851
|
const file = want.get(path);
|
|
9473
9852
|
if (!file) return false;
|
|
9474
9853
|
const full = join5(dir, path);
|
|
9475
|
-
if (process.platform !== "win32" && (
|
|
9854
|
+
if (process.platform !== "win32" && (lstatSync2(full).mode & 73 ? 493 : 420) !== file.mode) return false;
|
|
9476
9855
|
if (!readFileSync5(full).equals(file.bytes)) return false;
|
|
9477
9856
|
} else {
|
|
9478
9857
|
return false;
|
|
@@ -9491,7 +9870,7 @@ function refuseLinks(dir, file, written = []) {
|
|
|
9491
9870
|
for (const part of [dirname2(dirname2(dir)), dirname2(dir), dir, ...file ? [file] : []]) {
|
|
9492
9871
|
let stat;
|
|
9493
9872
|
try {
|
|
9494
|
-
stat =
|
|
9873
|
+
stat = lstatSync2(part);
|
|
9495
9874
|
} catch (err) {
|
|
9496
9875
|
if (err.code === "ENOENT") return;
|
|
9497
9876
|
throw writeFailure(path, err, written);
|
|
@@ -9518,7 +9897,7 @@ function planInstall(dirs, payload) {
|
|
|
9518
9897
|
refuseLinks(dir, path);
|
|
9519
9898
|
let current;
|
|
9520
9899
|
try {
|
|
9521
|
-
current =
|
|
9900
|
+
current = lstatSync2(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
|
|
9522
9901
|
} catch (err) {
|
|
9523
9902
|
throw writeFailure(path, err);
|
|
9524
9903
|
}
|
|
@@ -9543,7 +9922,7 @@ function stage(target, name, archive, staged) {
|
|
|
9543
9922
|
s.createdParent = mkdirSync3(dirname2(target), { recursive: true }) ?? void 0;
|
|
9544
9923
|
refuseLinks(target);
|
|
9545
9924
|
s.temp = mkdtempSync(join5(dirname2(target), `.${name}.amdahl-tmp-`));
|
|
9546
|
-
|
|
9925
|
+
chmodSync4(s.temp, 493);
|
|
9547
9926
|
for (const dir of archive.dirs) mkdirSync3(join5(s.temp, dir));
|
|
9548
9927
|
for (const file of archive.files) writeFileSync3(join5(s.temp, file.path), file.bytes, { flag: "wx", mode: file.mode });
|
|
9549
9928
|
}
|
|
@@ -9551,15 +9930,15 @@ function rollBack(staged) {
|
|
|
9551
9930
|
for (const s of [...staged].reverse()) {
|
|
9552
9931
|
try {
|
|
9553
9932
|
if (s.swapped) {
|
|
9554
|
-
|
|
9555
|
-
if (s.backup)
|
|
9933
|
+
rmSync3(s.target, { recursive: true, force: true });
|
|
9934
|
+
if (s.backup) renameSync3(s.backup, s.target);
|
|
9556
9935
|
} else if (s.backup) {
|
|
9557
|
-
|
|
9936
|
+
renameSync3(s.backup, s.target);
|
|
9558
9937
|
}
|
|
9559
9938
|
} catch {
|
|
9560
9939
|
}
|
|
9561
|
-
if (s.temp)
|
|
9562
|
-
if (s.createdParent)
|
|
9940
|
+
if (s.temp) rmSync3(s.temp, { recursive: true, force: true });
|
|
9941
|
+
if (s.createdParent) rmSync3(s.createdParent, { recursive: true, force: true });
|
|
9563
9942
|
}
|
|
9564
9943
|
}
|
|
9565
9944
|
function writeFolders(plan, name, archive) {
|
|
@@ -9575,9 +9954,9 @@ function writeFolders(plan, name, archive) {
|
|
|
9575
9954
|
current = s.target;
|
|
9576
9955
|
if (plan.find((p) => p.path === s.target)?.action === "replaced") {
|
|
9577
9956
|
s.backup = `${s.temp}.old`;
|
|
9578
|
-
|
|
9957
|
+
renameSync3(s.target, s.backup);
|
|
9579
9958
|
}
|
|
9580
|
-
|
|
9959
|
+
renameSync3(s.temp, s.target);
|
|
9581
9960
|
s.swapped = true;
|
|
9582
9961
|
}
|
|
9583
9962
|
} catch (err) {
|
|
@@ -9585,7 +9964,7 @@ function writeFolders(plan, name, archive) {
|
|
|
9585
9964
|
if (err instanceof CliError) throw err;
|
|
9586
9965
|
throw writeFailure(current, err);
|
|
9587
9966
|
}
|
|
9588
|
-
for (const s of staged) if (s.backup)
|
|
9967
|
+
for (const s of staged) if (s.backup) rmSync3(s.backup, { recursive: true, force: true });
|
|
9589
9968
|
}
|
|
9590
9969
|
function writeFiles(plan, body) {
|
|
9591
9970
|
const written = [];
|
|
@@ -9597,9 +9976,9 @@ function writeFiles(plan, body) {
|
|
|
9597
9976
|
mkdirSync3(dirname2(p.path), { recursive: true });
|
|
9598
9977
|
refuseLinks(dirname2(p.path), p.path, written);
|
|
9599
9978
|
writeFileSync3(temp, body, { flag: "wx", mode: 420 });
|
|
9600
|
-
|
|
9979
|
+
renameSync3(temp, p.path);
|
|
9601
9980
|
} catch (err) {
|
|
9602
|
-
|
|
9981
|
+
rmSync3(temp, { force: true });
|
|
9603
9982
|
if (err instanceof CliError) throw err;
|
|
9604
9983
|
throw writeFailure(p.path, err, written);
|
|
9605
9984
|
}
|
|
@@ -9704,16 +10083,124 @@ var SHOWN = [
|
|
|
9704
10083
|
{ client: "cursor", name: "Cursor" },
|
|
9705
10084
|
{ client: "codex", name: "Codex" }
|
|
9706
10085
|
];
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
10086
|
+
var QUESTION = "Where should Amdahl work?";
|
|
10087
|
+
var TERMINAL = "This terminal";
|
|
10088
|
+
var FOUND_TAG = "found on your machine";
|
|
10089
|
+
var CONNECT_URL = "https://amdahl.ai/connect";
|
|
10090
|
+
var NONE_FOUND = `We didn't find Claude Code, Cursor or Codex on this computer.
|
|
10091
|
+
Using the Claude or ChatGPT app? Connect it at ${CONNECT_URL}`;
|
|
10092
|
+
var MENU_HINT = "\u2191\u2193 move \xB7 Space tick/untick \xB7 Enter continue";
|
|
10093
|
+
var ASK = '"Use Amdahl to make this sound less like AI"';
|
|
10094
|
+
var SIGN_IN = {
|
|
10095
|
+
"Claude Code": "If it's open, quit and reopen it. Type /mcp, pick amdahl, sign in.",
|
|
10096
|
+
// Cursor reads ~/.cursor/mcp.json at start, and lists its MCP servers under Customize > MCPs.
|
|
10097
|
+
Cursor: "If it's open, quit and reopen it. In Customize > MCPs, sign in to amdahl."
|
|
10098
|
+
};
|
|
10099
|
+
var TRY_IN = {
|
|
10100
|
+
"Claude Code": `Paste an email and say: ${ASK}`,
|
|
10101
|
+
Cursor: `In chat, paste an email, say: ${ASK}`,
|
|
10102
|
+
Codex: `In codex, paste an email, say: ${ASK}`
|
|
10103
|
+
};
|
|
10104
|
+
var CODEX_TRY = `Run codex, paste an email and say: ${ASK}`;
|
|
10105
|
+
var CODEX_LOGIN = "codex mcp login amdahl";
|
|
10106
|
+
function toolScreen(name, already) {
|
|
10107
|
+
return [
|
|
10108
|
+
already ? `\u2713 ${name} already has Amdahl.` : `\u2713 Added Amdahl to ${name}.`,
|
|
10109
|
+
`One step left, in ${name}:`,
|
|
10110
|
+
` 1. ${SIGN_IN[name] ?? ""}`,
|
|
10111
|
+
` 2. ${TRY_IN[name] ?? ""}`
|
|
10112
|
+
];
|
|
10113
|
+
}
|
|
10114
|
+
function allSetLines(names) {
|
|
10115
|
+
const [only] = names;
|
|
10116
|
+
if (only === void 0) return [];
|
|
10117
|
+
const tryHere2 = `Want to try it here instead? Run: ${amdahlCommand("optimize")}`;
|
|
10118
|
+
if (names.length === 1) {
|
|
10119
|
+
const done = {
|
|
10120
|
+
"Claude Code": ["If you've already run /mcp, you're done.", "If not: quit and reopen Claude Code, type /mcp, pick amdahl, sign in."],
|
|
10121
|
+
Cursor: ["If you've already signed in under Customize > MCPs, you're done.", "If not: quit and reopen Cursor, then sign in to amdahl there."],
|
|
10122
|
+
Codex: [`If you've already run ${CODEX_LOGIN}, you're done.`, `If not, run it now in this terminal.`]
|
|
10123
|
+
};
|
|
10124
|
+
return [`Amdahl is in ${only}. You're all set.`, `Paste an email there and say: ${ASK}`, ...done[only] ?? [], tryHere2];
|
|
10125
|
+
}
|
|
10126
|
+
const step = {
|
|
10127
|
+
"Claude Code": "quit and reopen it, type /mcp, pick amdahl, sign in",
|
|
10128
|
+
Cursor: "quit and reopen it, then in Customize > MCPs, sign in to amdahl",
|
|
10129
|
+
Codex: `run ${CODEX_LOGIN}`
|
|
10130
|
+
};
|
|
10131
|
+
return [
|
|
10132
|
+
`Amdahl is in ${joinNames(names)}. You're all set.`,
|
|
10133
|
+
`Paste an email in ${names.length === 2 ? "either" : "any of them"} and say: ${ASK}`,
|
|
10134
|
+
"If you've already signed each one in, you're done. If not:",
|
|
10135
|
+
...names.map((n) => ` ${n}: ${step[n] ?? ""}`),
|
|
10136
|
+
tryHere2
|
|
10137
|
+
];
|
|
10138
|
+
}
|
|
10139
|
+
function nothingPickedLines() {
|
|
10140
|
+
return [
|
|
10141
|
+
"Nothing picked, so nothing changed.",
|
|
10142
|
+
`To try it here, run ${amdahlCommand()} again and tick "${TERMINAL}".`,
|
|
10143
|
+
`Using the Claude or ChatGPT app? Connect it at ${CONNECT_URL}`
|
|
10144
|
+
];
|
|
10145
|
+
}
|
|
10146
|
+
function wrapLine(line, columns) {
|
|
10147
|
+
if (!columns || [...line].length <= columns) return [line];
|
|
10148
|
+
const lead = /^\s*(?:\d+\.\s+)?/.exec(line)?.[0] ?? "";
|
|
10149
|
+
const indent = " ".repeat(lead.length);
|
|
10150
|
+
const lines = [];
|
|
10151
|
+
let current = lead;
|
|
10152
|
+
let words = 0;
|
|
10153
|
+
for (const word of line.slice(lead.length).split(" ")) {
|
|
10154
|
+
if (words > 0 && [...`${current} ${word}`].length > columns) {
|
|
10155
|
+
lines.push(current);
|
|
10156
|
+
current = indent + word;
|
|
10157
|
+
} else current = words > 0 ? `${current} ${word}` : current + word;
|
|
10158
|
+
words++;
|
|
10159
|
+
}
|
|
10160
|
+
lines.push(current);
|
|
10161
|
+
return lines;
|
|
10162
|
+
}
|
|
10163
|
+
var AGENT_ASK = `paste an email and say ${ASK}`;
|
|
10164
|
+
function finishLines(names, inClaudeCode = false) {
|
|
10165
|
+
const [only] = names;
|
|
10166
|
+
if (only === void 0) return [];
|
|
10167
|
+
if (names.length === 1) {
|
|
10168
|
+
const step2 = {
|
|
10169
|
+
"Claude Code": inClaudeCode ? "Before that, restart Claude Code and run /mcp once to connect Amdahl." : "Before that, run /mcp once in Claude Code to connect Amdahl.",
|
|
10170
|
+
Cursor: "Before that, sign in when Cursor asks, the first time you use Amdahl.",
|
|
10171
|
+
Codex: `Before that, run \`${CODEX_LOGIN}\` once to connect Amdahl.`
|
|
10172
|
+
};
|
|
10173
|
+
return [`Try it: ${AGENT_ASK} in ${only}.`, step2[only] ?? ""];
|
|
10174
|
+
}
|
|
10175
|
+
const step = {
|
|
10176
|
+
"Claude Code": inClaudeCode ? "restart it, then run /mcp" : "run /mcp",
|
|
10177
|
+
Cursor: "sign in when it asks, on first use",
|
|
10178
|
+
Codex: `run \`${CODEX_LOGIN}\``
|
|
10179
|
+
};
|
|
10180
|
+
return [`Try it: ${AGENT_ASK} in any of them.`, "Before that, connect each one once:", ...names.map((n) => ` ${n}: ${step[n] ?? ""}`)];
|
|
10181
|
+
}
|
|
10182
|
+
var isSet = (value) => Boolean(value) && value !== "false" && value !== "0";
|
|
10183
|
+
function ciVariable(env) {
|
|
10184
|
+
const named = ["CI", "JENKINS_URL", "TF_BUILD", "CODEBUILD_BUILD_ID", "TEAMCITY_VERSION", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI"].find((name) => isSet(env[name]));
|
|
10185
|
+
if (named) return named;
|
|
10186
|
+
return isSet(env.BUILD_NUMBER) && isSet(env.JENKINS_HOME) ? "BUILD_NUMBER with JENKINS_HOME" : null;
|
|
9712
10187
|
}
|
|
9713
|
-
function
|
|
10188
|
+
function setupMode(io) {
|
|
9714
10189
|
const { env } = io;
|
|
9715
|
-
const ci =
|
|
9716
|
-
|
|
10190
|
+
const ci = ciVariable(env);
|
|
10191
|
+
if (ci) return { kind: "usage", reason: `${ci} is set` };
|
|
10192
|
+
if (env.AMDAHL_NO_PROMPT === "1") return { kind: "usage", reason: "AMDAHL_NO_PROMPT=1 is set" };
|
|
10193
|
+
if (env.CLAUDECODE || env.CURSOR_AGENT) return { kind: "agent" };
|
|
10194
|
+
return io.stdinIsTTY && io.stdoutIsTTY && io.stderrIsTTY ? { kind: "menu" } : { kind: "agent" };
|
|
10195
|
+
}
|
|
10196
|
+
function installLine() {
|
|
10197
|
+
return `To set up a client, run \`${amdahlCommand("install claude-code")}\` (or cursor, codex).`;
|
|
10198
|
+
}
|
|
10199
|
+
function noSetupLine(reason) {
|
|
10200
|
+
return `Setup did not run: ${reason}.
|
|
10201
|
+
${installLine()}
|
|
10202
|
+
|
|
10203
|
+
`;
|
|
9717
10204
|
}
|
|
9718
10205
|
async function signIn(ctx) {
|
|
9719
10206
|
let credential;
|
|
@@ -9731,83 +10218,178 @@ async function signIn(ctx) {
|
|
|
9731
10218
|
return credential.source === "profile" ? { kind: "out" } : { kind: "rejected", source: credential.source };
|
|
9732
10219
|
}
|
|
9733
10220
|
}
|
|
9734
|
-
|
|
10221
|
+
function reportError(io, err) {
|
|
10222
|
+
io.stderr(`Error: ${err.message}
|
|
10223
|
+
${err.hint ? `${err.hint}
|
|
10224
|
+
` : ""}`);
|
|
10225
|
+
}
|
|
10226
|
+
async function installEach(ctx, tools) {
|
|
10227
|
+
let exit = EXIT.ok;
|
|
10228
|
+
const ready = [];
|
|
10229
|
+
for (const tool of tools) {
|
|
10230
|
+
let steps;
|
|
10231
|
+
try {
|
|
10232
|
+
steps = await installClient(ctx, tool.client, false);
|
|
10233
|
+
} catch (raw) {
|
|
10234
|
+
const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${tool.name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
|
|
10235
|
+
reportError(ctx.io, err);
|
|
10236
|
+
if (exit === EXIT.ok) exit = err.exit;
|
|
10237
|
+
continue;
|
|
10238
|
+
}
|
|
10239
|
+
const detail = [steps.mcp.human, ...steps.mcp.notes, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n");
|
|
10240
|
+
ready.push({ ...tool, already: steps.mcp.data.action === "unchanged", detail });
|
|
10241
|
+
}
|
|
10242
|
+
return { ready, exit };
|
|
10243
|
+
}
|
|
10244
|
+
function enterOrSkip(io) {
|
|
10245
|
+
return new Promise((resolve2) => {
|
|
10246
|
+
const stop = io.listenForEnter(
|
|
10247
|
+
() => {
|
|
10248
|
+
stop();
|
|
10249
|
+
resolve2(true);
|
|
10250
|
+
},
|
|
10251
|
+
() => resolve2(false)
|
|
10252
|
+
);
|
|
10253
|
+
});
|
|
10254
|
+
}
|
|
10255
|
+
async function codexScreen(ctx, tool, say, detail, lead) {
|
|
10256
|
+
say(tool.already ? "\u2713 Codex already has Amdahl." : "\u2713 Added Amdahl to Codex.");
|
|
10257
|
+
detail(tool.detail);
|
|
10258
|
+
for (const line of lead) say(line);
|
|
10259
|
+
if (!await onPath(ctx, "codex")) {
|
|
10260
|
+
say("One step left, in your terminal:");
|
|
10261
|
+
say(` 1. Run: ${CODEX_LOGIN}`);
|
|
10262
|
+
say(` 2. ${TRY_IN.Codex ?? ""}`);
|
|
10263
|
+
return;
|
|
10264
|
+
}
|
|
10265
|
+
say("Press Enter to sign Codex in to Amdahl (opens your browser).");
|
|
10266
|
+
if (!await enterOrSkip(ctx.io)) {
|
|
10267
|
+
say(`Skipped. To connect it later, run: ${CODEX_LOGIN}`);
|
|
10268
|
+
say(CODEX_TRY);
|
|
10269
|
+
return;
|
|
10270
|
+
}
|
|
10271
|
+
const code = await ctx.io.runInteractive("codex", ["mcp", "login", "amdahl"]);
|
|
10272
|
+
if (code === 0) say("\u2713 Codex is connected.");
|
|
10273
|
+
else {
|
|
10274
|
+
say("");
|
|
10275
|
+
say(`Codex didn't finish signing in. To try again, run: ${CODEX_LOGIN}`);
|
|
10276
|
+
}
|
|
10277
|
+
say(CODEX_TRY);
|
|
10278
|
+
}
|
|
10279
|
+
async function setup(ctx, mode) {
|
|
9735
10280
|
const { io, out } = ctx;
|
|
9736
10281
|
const verbose = ctx.flags.verbose === true;
|
|
9737
|
-
const say = (line) =>
|
|
10282
|
+
const say = (line) => {
|
|
10283
|
+
for (const part of mode === "menu" ? wrapLine(line, io.columns()) : [line]) io.stdout(`${part}
|
|
9738
10284
|
`);
|
|
10285
|
+
};
|
|
9739
10286
|
const detail = (text2) => {
|
|
9740
|
-
if (verbose && text2)
|
|
10287
|
+
if (verbose && text2) io.stdout(`${text2.replace(/^/gm, " ")}
|
|
10288
|
+
`);
|
|
9741
10289
|
};
|
|
9742
10290
|
say(`${out.bold("Amdahl")} \xB7 predictive ML for GTM`);
|
|
9743
10291
|
const found = await detectClients(ctx);
|
|
9744
10292
|
const detected = SHOWN.filter((c) => found[c.client]);
|
|
9745
|
-
|
|
10293
|
+
if (mode === "agent") {
|
|
10294
|
+
const running = SHOWN.filter((c) => c.client === "claude-code" && io.env.CLAUDECODE || c.client === "cursor" && io.env.CURSOR_AGENT);
|
|
10295
|
+
const targets = running.length > 0 ? running : detected;
|
|
10296
|
+
if (targets.length === 0) {
|
|
10297
|
+
io.stderr(`Didn't find Claude Code, Cursor or Codex on this computer, so nothing changed.
|
|
10298
|
+
${installLine()}
|
|
10299
|
+
`);
|
|
10300
|
+
return EXIT.general;
|
|
10301
|
+
}
|
|
10302
|
+
const { ready, exit: exit2 } = await installEach(ctx, targets);
|
|
10303
|
+
for (const tool of ready) {
|
|
10304
|
+
say(tool.already ? `\u2713 ${tool.name} already has Amdahl` : `\u2713 Added to ${tool.name}`);
|
|
10305
|
+
detail(tool.detail);
|
|
10306
|
+
}
|
|
10307
|
+
for (const line of finishLines(ready.map((t) => t.name), Boolean(io.env.CLAUDECODE))) say(line);
|
|
10308
|
+
return exit2;
|
|
10309
|
+
}
|
|
9746
10310
|
if (detected.length > 0) {
|
|
9747
|
-
const
|
|
9748
|
-
if (
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
if (unreachable || who?.kind === "in") {
|
|
9757
|
-
say(`You're set up in ${joinNames(detected.map((c) => c.name))}.${unreachable ? " (Could not reach Amdahl to check your sign-in.)" : ""}`);
|
|
9758
|
-
say(tryItLine(detected[0]?.name ?? ""));
|
|
9759
|
-
return EXIT.ok;
|
|
9760
|
-
}
|
|
10311
|
+
const there = await Promise.all(detected.map((c) => hasAmdahl(ctx, c.client)));
|
|
10312
|
+
if (there.every(Boolean)) {
|
|
10313
|
+
for (const tool of detected) {
|
|
10314
|
+
if (await isInstalled(ctx, tool.client)) continue;
|
|
10315
|
+
const { ready } = await installEach(ctx, [tool]);
|
|
10316
|
+
for (const r of ready) detail(r.detail);
|
|
10317
|
+
}
|
|
10318
|
+
for (const line of allSetLines(detected.map((c) => c.name))) say(line);
|
|
10319
|
+
return EXIT.ok;
|
|
9761
10320
|
}
|
|
9762
10321
|
}
|
|
9763
|
-
const
|
|
9764
|
-
|
|
9765
|
-
SHOWN.map((c) => c.
|
|
9766
|
-
|
|
9767
|
-
|
|
10322
|
+
const rows = [...SHOWN.map((c) => c.name), TERMINAL];
|
|
10323
|
+
const picks = await io.multiSelect(QUESTION, rows, {
|
|
10324
|
+
ticked: [...SHOWN.map((c) => found[c.client]), false],
|
|
10325
|
+
tags: [...SHOWN.map((c) => found[c.client] ? FOUND_TAG : void 0), void 0],
|
|
10326
|
+
note: detected.length === 0 ? NONE_FOUND : void 0,
|
|
10327
|
+
// With no tool here, the marker starts on "This terminal": Space, then Enter.
|
|
10328
|
+
initial: detected.length === 0 ? SHOWN.length : void 0,
|
|
10329
|
+
hint: MENU_HINT,
|
|
10330
|
+
color: out.color
|
|
10331
|
+
});
|
|
9768
10332
|
if (picks.length === 0) {
|
|
9769
|
-
|
|
10333
|
+
for (const line of nothingPickedLines()) say(line);
|
|
9770
10334
|
return EXIT.ok;
|
|
9771
10335
|
}
|
|
9772
|
-
|
|
10336
|
+
const here = picks.includes(SHOWN.length);
|
|
10337
|
+
let exit = EXIT.ok;
|
|
10338
|
+
const chosen = [];
|
|
10339
|
+
for (const pick of picks.flatMap((i) => SHOWN[i] ?? [])) {
|
|
10340
|
+
const err = await preflight(ctx, pick.client);
|
|
10341
|
+
if (!err) {
|
|
10342
|
+
chosen.push(pick);
|
|
10343
|
+
continue;
|
|
10344
|
+
}
|
|
10345
|
+
reportError(io, err);
|
|
10346
|
+
if (exit === EXIT.ok) exit = err.exit;
|
|
10347
|
+
}
|
|
10348
|
+
const done = await installEach(ctx, chosen);
|
|
10349
|
+
if (exit === EXIT.ok) exit = done.exit;
|
|
10350
|
+
let lead = [NEW_USER];
|
|
10351
|
+
for (const [i, tool] of done.ready.entries()) {
|
|
10352
|
+
if (i > 0) say("");
|
|
10353
|
+
if (tool.client === "codex") {
|
|
10354
|
+
await codexScreen(ctx, tool, say, detail, lead);
|
|
10355
|
+
} else {
|
|
10356
|
+
const [first, ...steps] = toolScreen(tool.name, tool.already);
|
|
10357
|
+
say(first ?? "");
|
|
10358
|
+
detail(tool.detail);
|
|
10359
|
+
for (const line of [...steps, ...lead]) say(line);
|
|
10360
|
+
}
|
|
10361
|
+
lead = [];
|
|
10362
|
+
}
|
|
10363
|
+
if (!here) return exit;
|
|
10364
|
+
if (done.ready.length > 0) say("");
|
|
10365
|
+
try {
|
|
10366
|
+
const code = await tryHere(ctx, say, lead);
|
|
10367
|
+
return exit === EXIT.ok ? code : exit;
|
|
10368
|
+
} catch (err) {
|
|
10369
|
+
if (err instanceof PromptCancelled && done.ready.length > 0) {
|
|
10370
|
+
throw new CliError("cancelled", `Stopped. Amdahl is still set up in ${joinNames(done.ready.map((t) => t.name))}.`, EXIT.cancelled);
|
|
10371
|
+
}
|
|
10372
|
+
throw err;
|
|
10373
|
+
}
|
|
10374
|
+
}
|
|
10375
|
+
async function tryHere(ctx, say, lead) {
|
|
10376
|
+
const { out } = ctx;
|
|
10377
|
+
const verbose = ctx.flags.verbose === true;
|
|
10378
|
+
const who = await signIn(ctx);
|
|
9773
10379
|
if (who.kind === "in") {
|
|
9774
10380
|
say(`\u2713 Signed in \xB7 ${who.workspace}`);
|
|
9775
|
-
|
|
10381
|
+
if (verbose) say(` Signed in as ${who.email}.`);
|
|
9776
10382
|
} else if (who.kind === "out") {
|
|
10383
|
+
for (const line of lead) say(line);
|
|
9777
10384
|
const signedIn = await browserSignIn(ctx);
|
|
9778
10385
|
say(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
|
|
9779
|
-
|
|
10386
|
+
if (verbose) say(` ${signedInLine(ctx, signedIn)}`);
|
|
9780
10387
|
} else {
|
|
9781
10388
|
const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
|
|
9782
|
-
out.info(`Amdahl rejected the key in ${name}
|
|
9783
|
-
|
|
9784
|
-
let exit = EXIT.ok;
|
|
9785
|
-
let ready = null;
|
|
9786
|
-
for (const { client, name } of picks.flatMap((i) => SHOWN[i] ?? [])) {
|
|
9787
|
-
let steps;
|
|
9788
|
-
try {
|
|
9789
|
-
steps = await installClient(ctx, client, false);
|
|
9790
|
-
} catch (raw) {
|
|
9791
|
-
const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
|
|
9792
|
-
io.stderr(`Error: ${err.message}
|
|
9793
|
-
${err.hint ? `${err.hint}
|
|
9794
|
-
` : ""}`);
|
|
9795
|
-
if (exit === EXIT.ok) exit = err.exit;
|
|
9796
|
-
continue;
|
|
9797
|
-
}
|
|
9798
|
-
const action = steps.mcp.data.action;
|
|
9799
|
-
if (action === "printed") {
|
|
9800
|
-
say(`! ${name}: \`claude\` is not on your PATH. Once it is, run:
|
|
9801
|
-
${String(steps.mcp.data.command)}`);
|
|
9802
|
-
} else {
|
|
9803
|
-
say(action === "unchanged" ? `\u2713 ${name} already has Amdahl` : `\u2713 Added to ${name}`);
|
|
9804
|
-
ready ??= { name, connect: action !== "unchanged" };
|
|
9805
|
-
}
|
|
9806
|
-
const mcpDetail = action === "printed" ? [] : [steps.mcp.human, ...steps.mcp.notes];
|
|
9807
|
-
detail([...mcpDetail, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n"));
|
|
10389
|
+
out.info(`Amdahl rejected the key in ${name}, so a message can't be tried here. Fix it, or unset it and run \`${amdahlCommand("login")}\`.`);
|
|
10390
|
+
return EXIT.unauthenticated;
|
|
9808
10391
|
}
|
|
9809
|
-
|
|
9810
|
-
return exit;
|
|
10392
|
+
return optimizePasted(ctx);
|
|
9811
10393
|
}
|
|
9812
10394
|
|
|
9813
10395
|
// src/commands/skills.ts
|
|
@@ -9935,8 +10517,8 @@ function readTar(tar) {
|
|
|
9935
10517
|
throw new ArchiveError("its tar is not ustar");
|
|
9936
10518
|
}
|
|
9937
10519
|
const name = field(header, 0, 100);
|
|
9938
|
-
const
|
|
9939
|
-
let path =
|
|
10520
|
+
const prefix2 = field(header, 345, 155);
|
|
10521
|
+
let path = prefix2 ? `${prefix2}/${name}` : name;
|
|
9940
10522
|
const type = String.fromCharCode(header[156]);
|
|
9941
10523
|
const size = octal(header, 124, 12);
|
|
9942
10524
|
const mode = octal(header, 100, 8);
|
|
@@ -10357,9 +10939,9 @@ ${done}`);
|
|
|
10357
10939
|
|
|
10358
10940
|
// src/commands/workspace-status.ts
|
|
10359
10941
|
var BLOCKER_FIX = {
|
|
10360
|
-
missing_scope:
|
|
10361
|
-
role_too_low: "Your role cannot optimize. Ask a workspace admin to raise it.",
|
|
10362
|
-
quota_exhausted: "The monthly optimize quota is used up."
|
|
10942
|
+
missing_scope: () => `This credential cannot optimize. Use a Customer agent key, or run \`${amdahlCommand("login")}\` again.`,
|
|
10943
|
+
role_too_low: () => "Your role cannot optimize. Ask a workspace admin to raise it.",
|
|
10944
|
+
quota_exhausted: () => "The monthly optimize quota is used up."
|
|
10363
10945
|
};
|
|
10364
10946
|
function describe(status2) {
|
|
10365
10947
|
const lines = [
|
|
@@ -10371,8 +10953,9 @@ function describe(status2) {
|
|
|
10371
10953
|
if (opt.quota) {
|
|
10372
10954
|
lines.push(`Quota: ${opt.quota.used} of ${opt.quota.limit} used, resets ${opt.quota.resets_at}`);
|
|
10373
10955
|
}
|
|
10374
|
-
|
|
10375
|
-
|
|
10956
|
+
const fix = opt.blocker ? BLOCKER_FIX[opt.blocker] : void 0;
|
|
10957
|
+
if (!opt.allowed && fix) {
|
|
10958
|
+
lines.push(` ${fix()}`);
|
|
10376
10959
|
}
|
|
10377
10960
|
const conns = status2.connections;
|
|
10378
10961
|
if ("omitted" in conns) {
|
|
@@ -10401,7 +10984,7 @@ async function workspaceList(ctx) {
|
|
|
10401
10984
|
email: p.email,
|
|
10402
10985
|
api_url: p.api_url
|
|
10403
10986
|
}));
|
|
10404
|
-
const human = profiles.length === 0 ?
|
|
10987
|
+
const human = profiles.length === 0 ? `No profiles. Run \`${amdahlCommand("login")}\`.` : profiles.map(
|
|
10405
10988
|
(p) => `${p.name === config.default_profile ? "*" : " "} ${p.name} ${p.workspace.name} ${p.email ?? ""} ${p.api_url}`
|
|
10406
10989
|
).join("\n");
|
|
10407
10990
|
ctx.out.result({ ok: true, default: config.default_profile, profiles }, human);
|
|
@@ -10411,7 +10994,7 @@ async function workspaceUse(ctx, args) {
|
|
|
10411
10994
|
const name = args[0];
|
|
10412
10995
|
if (!name || args.length > 1) throw usageError("Usage: amdahl workspace use <profile>");
|
|
10413
10996
|
if (!ctx.config.profiles[name]) {
|
|
10414
|
-
throw usageError(`No profile named "${name}".`,
|
|
10997
|
+
throw usageError(`No profile named "${name}".`, `Run \`${amdahlCommand("workspace list")}\` to see your profiles.`);
|
|
10415
10998
|
}
|
|
10416
10999
|
ctx.config.default_profile = name;
|
|
10417
11000
|
ctx.saveConfig();
|
|
@@ -10420,7 +11003,7 @@ async function workspaceUse(ctx, args) {
|
|
|
10420
11003
|
}
|
|
10421
11004
|
|
|
10422
11005
|
// src/version.ts
|
|
10423
|
-
var VERSION = "0.3.
|
|
11006
|
+
var VERSION = "0.3.5";
|
|
10424
11007
|
|
|
10425
11008
|
// src/main.ts
|
|
10426
11009
|
var COMMANDS = {
|
|
@@ -10438,8 +11021,9 @@ var COMMANDS = {
|
|
|
10438
11021
|
install,
|
|
10439
11022
|
"skills add": skillsAdd
|
|
10440
11023
|
};
|
|
10441
|
-
var NO_ARGS = /* @__PURE__ */ new Set(["login", "logout", "whoami", "auth token", "status", "keys create", "keys list", "workspace list"]);
|
|
11024
|
+
var NO_ARGS = /* @__PURE__ */ new Set(["setup", "login", "logout", "whoami", "auth token", "status", "keys create", "keys list", "workspace list"]);
|
|
10442
11025
|
async function runCli(argv, io) {
|
|
11026
|
+
setCommandName(io);
|
|
10443
11027
|
const json = argv.includes("--json");
|
|
10444
11028
|
let out = createOutput(io, json, argv.includes("--no-color"));
|
|
10445
11029
|
try {
|
|
@@ -10450,18 +11034,21 @@ async function runCli(argv, io) {
|
|
|
10450
11034
|
`);
|
|
10451
11035
|
return EXIT.ok;
|
|
10452
11036
|
}
|
|
10453
|
-
if (parsed.flags.help) {
|
|
11037
|
+
if (parsed.flags.help || parsed.command === "help") {
|
|
10454
11038
|
io.stdout(USAGE);
|
|
10455
11039
|
return EXIT.ok;
|
|
10456
11040
|
}
|
|
10457
|
-
if (
|
|
10458
|
-
if (argv.every((a) => a === "--verbose") && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
|
|
10459
|
-
io.stderr(USAGE);
|
|
10460
|
-
return EXIT.usage;
|
|
10461
|
-
}
|
|
10462
|
-
if (NO_ARGS.has(parsed.command) && parsed.args.length > 0) {
|
|
11041
|
+
if (NO_ARGS.has(parsed.command ?? "") && parsed.args.length > 0) {
|
|
10463
11042
|
throw new CliError("usage", `Unexpected argument: ${parsed.args[0]}`, EXIT.usage);
|
|
10464
11043
|
}
|
|
11044
|
+
if (!parsed.command || parsed.command === "setup") {
|
|
11045
|
+
const mode = parsed.flags.json ? { kind: "usage", reason: "it has no --json output" } : setupMode(io);
|
|
11046
|
+
if (mode.kind === "usage") {
|
|
11047
|
+
io.stderr(`${noSetupLine(mode.reason)}${USAGE}`);
|
|
11048
|
+
return EXIT.usage;
|
|
11049
|
+
}
|
|
11050
|
+
return await setup(createCtx(io, out, parsed.flags), mode.kind);
|
|
11051
|
+
}
|
|
10465
11052
|
const handler = COMMANDS[parsed.command];
|
|
10466
11053
|
if (!handler) throw new CliError("usage", `Unknown command ${parsed.command}`, EXIT.usage);
|
|
10467
11054
|
const ctx = createCtx(io, out, parsed.flags);
|