@amdahl/cli 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -5
- package/dist/amdahl.js +662 -342
- package/package.json +1 -1
package/dist/amdahl.js
CHANGED
|
@@ -7371,26 +7371,25 @@ import { createInterface } from "readline";
|
|
|
7371
7371
|
// src/select.ts
|
|
7372
7372
|
import { emitKeypressEvents } from "readline";
|
|
7373
7373
|
var HINT = "(Use the arrow keys, then Enter)";
|
|
7374
|
+
var MULTI_HINT = "(Use the arrow keys, Space to tick or untick, then Enter)";
|
|
7375
|
+
var OFF = "\x1B[0m";
|
|
7374
7376
|
var HIDE_CURSOR = "\x1B[?25l";
|
|
7375
7377
|
var SHOW_CURSOR = "\x1B[?25h";
|
|
7376
7378
|
var CLEAR_LINE = "\x1B[2K";
|
|
7377
7379
|
var CLEAR_DOWN = "\x1B[0J";
|
|
7378
7380
|
var up = (n) => `\x1B[${n}A`;
|
|
7379
|
-
function
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
const off = "\x1B[0m";
|
|
7383
|
-
const row = (i) => i === current ? `${on}> ${choices[i]}${off}` : ` ${choices[i]}`;
|
|
7384
|
-
const draw = () => choices.map((_, i) => `${CLEAR_LINE}\r${row(i)}
|
|
7385
|
-
`).join("") + `${CLEAR_LINE}\r${HINT}
|
|
7381
|
+
function runMenu(input, output, question, hint, rows, onKey) {
|
|
7382
|
+
const draw = () => rows().map((r) => `${CLEAR_LINE}\r${r}
|
|
7383
|
+
`).join("") + `${CLEAR_LINE}\r${hint}
|
|
7386
7384
|
`;
|
|
7385
|
+
const height = rows().length;
|
|
7387
7386
|
const wasRaw = input.isRaw === true;
|
|
7388
7387
|
return new Promise((resolve, reject) => {
|
|
7389
7388
|
let done = false;
|
|
7390
7389
|
const finish = (outcome) => {
|
|
7391
7390
|
if (done) return;
|
|
7392
7391
|
done = true;
|
|
7393
|
-
input.removeListener("keypress",
|
|
7392
|
+
input.removeListener("keypress", onKeypress);
|
|
7394
7393
|
input.removeListener("error", onError);
|
|
7395
7394
|
process.removeListener("SIGINT", onSigint);
|
|
7396
7395
|
try {
|
|
@@ -7399,7 +7398,7 @@ function selectMenu(input, output, question, choices, options = {}) {
|
|
|
7399
7398
|
}
|
|
7400
7399
|
input.pause();
|
|
7401
7400
|
if ("pick" in outcome) {
|
|
7402
|
-
output.write(`${up(
|
|
7401
|
+
output.write(`${up(height + 2)}\r${CLEAR_DOWN}${question} ${outcome.summary}
|
|
7403
7402
|
${SHOW_CURSOR}`);
|
|
7404
7403
|
resolve(outcome.pick);
|
|
7405
7404
|
} else {
|
|
@@ -7407,17 +7406,12 @@ ${SHOW_CURSOR}`);
|
|
|
7407
7406
|
reject(outcome.error);
|
|
7408
7407
|
}
|
|
7409
7408
|
};
|
|
7410
|
-
const
|
|
7409
|
+
const onKeypress = (text, key) => {
|
|
7411
7410
|
const name = key?.name ?? text;
|
|
7412
7411
|
if (key?.ctrl && (name === "c" || name === "d")) return finish({ error: new PromptCancelled() });
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
if (
|
|
7416
|
-
else if (name === "down" || name === "j") next = (current + 1) % choices.length;
|
|
7417
|
-
else if (text && /^[1-9]$/.test(text) && Number(text) <= choices.length) next = Number(text) - 1;
|
|
7418
|
-
if (next === current) return;
|
|
7419
|
-
current = next;
|
|
7420
|
-
output.write(`${up(choices.length + 1)}${draw()}`);
|
|
7412
|
+
const result = onKey(text, name);
|
|
7413
|
+
if (result === "redraw") output.write(`${up(height + 1)}${draw()}`);
|
|
7414
|
+
else if (result) finish(result);
|
|
7421
7415
|
};
|
|
7422
7416
|
const onError = (err) => finish({ error: err });
|
|
7423
7417
|
const onSigint = () => finish({ error: new PromptCancelled() });
|
|
@@ -7426,7 +7420,7 @@ ${SHOW_CURSOR}`);
|
|
|
7426
7420
|
input.setRawMode?.(true);
|
|
7427
7421
|
output.write(`${HIDE_CURSOR}${question}
|
|
7428
7422
|
${draw()}`);
|
|
7429
|
-
input.on("keypress",
|
|
7423
|
+
input.on("keypress", onKeypress);
|
|
7430
7424
|
input.on("error", onError);
|
|
7431
7425
|
process.on("SIGINT", onSigint);
|
|
7432
7426
|
input.resume();
|
|
@@ -7435,6 +7429,55 @@ ${draw()}`);
|
|
|
7435
7429
|
}
|
|
7436
7430
|
});
|
|
7437
7431
|
}
|
|
7432
|
+
function marker(options) {
|
|
7433
|
+
return options.color ? options.trueColor ? "\x1B[1;38;2;253;75;3m" : "\x1B[1;38;5;202m" : "\x1B[1m";
|
|
7434
|
+
}
|
|
7435
|
+
function move(current, count, text, name) {
|
|
7436
|
+
if (name === "up" || name === "k") return (current - 1 + count) % count;
|
|
7437
|
+
if (name === "down" || name === "j") return (current + 1) % count;
|
|
7438
|
+
if (text && /^[1-9]$/.test(text) && Number(text) <= count) return Number(text) - 1;
|
|
7439
|
+
return null;
|
|
7440
|
+
}
|
|
7441
|
+
function selectMenu(input, output, question, choices, options = {}) {
|
|
7442
|
+
let current = Math.min(Math.max(options.initial ?? 0, 0), choices.length - 1);
|
|
7443
|
+
const on = marker(options);
|
|
7444
|
+
const rows = () => choices.map((c, i) => i === current ? `${on}> ${c}${OFF}` : ` ${c}`);
|
|
7445
|
+
return runMenu(input, output, question, HINT, rows, (text, name) => {
|
|
7446
|
+
if (name === "return" || name === "enter") return { pick: current, summary: choices[current] ?? "" };
|
|
7447
|
+
const next = move(current, choices.length, text, name);
|
|
7448
|
+
if (next === null || next === current) return null;
|
|
7449
|
+
current = next;
|
|
7450
|
+
return "redraw";
|
|
7451
|
+
});
|
|
7452
|
+
}
|
|
7453
|
+
function multiSelectMenu(input, output, question, choices, options = {}) {
|
|
7454
|
+
let current = 0;
|
|
7455
|
+
const ticked = choices.map((_, i) => options.ticked?.[i] === true);
|
|
7456
|
+
const on = marker(options);
|
|
7457
|
+
const rows = () => choices.map((c, i) => {
|
|
7458
|
+
const row = `[${ticked[i] ? "x" : " "}] ${c}`;
|
|
7459
|
+
return i === current ? `${on}> ${row}${OFF}` : ` ${row}`;
|
|
7460
|
+
});
|
|
7461
|
+
return runMenu(input, output, question, MULTI_HINT, rows, (text, name) => {
|
|
7462
|
+
if (name === "return" || name === "enter") {
|
|
7463
|
+
const picks = choices.flatMap((_, i) => ticked[i] ? [i] : []);
|
|
7464
|
+
return { pick: picks, summary: picks.map((i) => choices[i]).join(", ") || "none" };
|
|
7465
|
+
}
|
|
7466
|
+
if (name === "space") {
|
|
7467
|
+
ticked[current] = !ticked[current];
|
|
7468
|
+
return "redraw";
|
|
7469
|
+
}
|
|
7470
|
+
if (text && /^[1-9]$/.test(text) && Number(text) <= choices.length) {
|
|
7471
|
+
current = Number(text) - 1;
|
|
7472
|
+
ticked[current] = !ticked[current];
|
|
7473
|
+
return "redraw";
|
|
7474
|
+
}
|
|
7475
|
+
const next = move(current, choices.length, text, name);
|
|
7476
|
+
if (next === null || next === current) return null;
|
|
7477
|
+
current = next;
|
|
7478
|
+
return "redraw";
|
|
7479
|
+
});
|
|
7480
|
+
}
|
|
7438
7481
|
|
|
7439
7482
|
// src/io.ts
|
|
7440
7483
|
var PromptCancelled = class extends Error {
|
|
@@ -7516,11 +7559,15 @@ function realIo() {
|
|
|
7516
7559
|
...options,
|
|
7517
7560
|
trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "")
|
|
7518
7561
|
}),
|
|
7562
|
+
multiSelect: (question, choices, options) => multiSelectMenu(process.stdin, process.stderr, question, choices, {
|
|
7563
|
+
...options,
|
|
7564
|
+
trueColor: /^(truecolor|24bit)$/i.test(process.env.COLORTERM ?? "")
|
|
7565
|
+
}),
|
|
7519
7566
|
uninterruptible,
|
|
7520
7567
|
run: runProcess,
|
|
7521
7568
|
openUrl: async (url) => {
|
|
7522
7569
|
const [cmd, args] = openerFor(process.platform, url);
|
|
7523
|
-
await runProcess(cmd, args);
|
|
7570
|
+
return (await runProcess(cmd, args)).code === 0;
|
|
7524
7571
|
},
|
|
7525
7572
|
homedir: homedir(),
|
|
7526
7573
|
cwd: process.cwd(),
|
|
@@ -7528,7 +7575,8 @@ function realIo() {
|
|
|
7528
7575
|
hostname: hostname(),
|
|
7529
7576
|
now: () => Date.now(),
|
|
7530
7577
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
7531
|
-
loginTimeoutMs:
|
|
7578
|
+
loginTimeoutMs: 9e5,
|
|
7579
|
+
browserGraceMs: 5e3,
|
|
7532
7580
|
fetchTimeoutMs: 15e3
|
|
7533
7581
|
};
|
|
7534
7582
|
}
|
|
@@ -7607,7 +7655,8 @@ var GLOBAL_OPTIONS = {
|
|
|
7607
7655
|
json: { type: "boolean" },
|
|
7608
7656
|
"no-color": { type: "boolean" },
|
|
7609
7657
|
help: { type: "boolean", short: "h" },
|
|
7610
|
-
version: { type: "boolean", short: "v" }
|
|
7658
|
+
version: { type: "boolean", short: "v" },
|
|
7659
|
+
verbose: { type: "boolean" }
|
|
7611
7660
|
};
|
|
7612
7661
|
var COMMAND_OPTIONS = {
|
|
7613
7662
|
login: {
|
|
@@ -7704,6 +7753,9 @@ function strictParse(argv, options) {
|
|
|
7704
7753
|
var USAGE = `Usage: amdahl <command> [options]
|
|
7705
7754
|
|
|
7706
7755
|
What you can do:
|
|
7756
|
+
amdahl Set up Claude Code, Cursor or Codex, in a terminal: it asks
|
|
7757
|
+
which, signs you in, and runs install for each one
|
|
7758
|
+
(--verbose shows the files and profile it used)
|
|
7707
7759
|
amdahl optimize draft.md Rewrite an outbound email or LinkedIn message
|
|
7708
7760
|
amdahl status Check that this workspace is set up to optimize
|
|
7709
7761
|
amdahl keys create --name ci Make an API key for a server or CI
|
|
@@ -7744,6 +7796,7 @@ Global options:
|
|
|
7744
7796
|
--api-key <key> Use an API key for this command (prefer AMDAHL_KEY: argv is visible to other users)
|
|
7745
7797
|
--json Data on stdout as JSON, messages on stderr
|
|
7746
7798
|
--no-color No colour (NO_COLOR is honoured too)
|
|
7799
|
+
--verbose Say more: files written, the profile, where the sign-in is stored
|
|
7747
7800
|
-h, --help Show this help
|
|
7748
7801
|
-v, --version Show the version
|
|
7749
7802
|
`;
|
|
@@ -8038,6 +8091,46 @@ function authorizeUrl(endpoints, input) {
|
|
|
8038
8091
|
const query = params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
8039
8092
|
return `${endpoints.authorization_endpoint}?${query}`;
|
|
8040
8093
|
}
|
|
8094
|
+
var SIGN_IN_AGAIN = {
|
|
8095
|
+
not_work_email: { why: "Amdahl needs your work email.", label: "Sign in with your work email" },
|
|
8096
|
+
subdomain_email: { why: "Use your main company email.", label: "Sign in again" },
|
|
8097
|
+
email_unconfirmed: { why: "Check your inbox to confirm your email, then try again.", label: "Try again" }
|
|
8098
|
+
};
|
|
8099
|
+
function signInAgainLine(reason, url) {
|
|
8100
|
+
const again = SIGN_IN_AGAIN[reason];
|
|
8101
|
+
return again ? `${again.why} ${again.label}:
|
|
8102
|
+
|
|
8103
|
+
${url}
|
|
8104
|
+
` : "";
|
|
8105
|
+
}
|
|
8106
|
+
function noBusinessMessage(consoleUrl, mayCreate, reason) {
|
|
8107
|
+
switch (reason) {
|
|
8108
|
+
case void 0:
|
|
8109
|
+
return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run \`amdahl 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 \`amdahl login\` again once you're in.`;
|
|
8110
|
+
case "domain_has_workspace":
|
|
8111
|
+
return `Your company already has an Amdahl workspace. Ask its admins to let you in at ${consoleUrl}, then try again.`;
|
|
8112
|
+
case "invite_only":
|
|
8113
|
+
return "Your company's Amdahl workspace is invite-only. Ask a colleague in it to invite you, then try again.";
|
|
8114
|
+
case "not_work_email":
|
|
8115
|
+
case "subdomain_email":
|
|
8116
|
+
case "email_unconfirmed": {
|
|
8117
|
+
const again = SIGN_IN_AGAIN[reason];
|
|
8118
|
+
return `${again.why} Run this again, then choose "${again.label}" in the browser.`;
|
|
8119
|
+
}
|
|
8120
|
+
case "daily_cap":
|
|
8121
|
+
return "Amdahl isn't taking more new workspaces today. Try again tomorrow.";
|
|
8122
|
+
case "paused":
|
|
8123
|
+
return "New workspaces are paused for now. Try again later.";
|
|
8124
|
+
case "domain_used":
|
|
8125
|
+
return "Your company already used its free workspace. Email hello@amdahl.ai to get access.";
|
|
8126
|
+
default:
|
|
8127
|
+
return "Amdahl couldn't create your workspace just now. Try again in a few minutes.";
|
|
8128
|
+
}
|
|
8129
|
+
}
|
|
8130
|
+
function signInAgainUrl(authorize) {
|
|
8131
|
+
const url = new URL(authorize);
|
|
8132
|
+
return `${url.origin}/login?redirect=${encodeURIComponent(`${url.pathname}${url.search}`)}`;
|
|
8133
|
+
}
|
|
8041
8134
|
function html(text) {
|
|
8042
8135
|
return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
8043
8136
|
}
|
|
@@ -8045,6 +8138,8 @@ function page(body) {
|
|
|
8045
8138
|
return `<!doctype html><html><head><meta charset="utf-8"><title>Amdahl CLI</title></head><body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;line-height:1.5">${body}</body></html>`;
|
|
8046
8139
|
}
|
|
8047
8140
|
async function startLoopback(io, input) {
|
|
8141
|
+
let authorize = null;
|
|
8142
|
+
let refused = null;
|
|
8048
8143
|
let settle;
|
|
8049
8144
|
let fail;
|
|
8050
8145
|
const result = new Promise((resolve, reject) => {
|
|
@@ -8071,9 +8166,18 @@ async function startLoopback(io, input) {
|
|
|
8071
8166
|
const error = url.searchParams.get("error");
|
|
8072
8167
|
const code = url.searchParams.get("code");
|
|
8073
8168
|
if (error === "no_business") {
|
|
8074
|
-
const
|
|
8075
|
-
|
|
8076
|
-
|
|
8169
|
+
const mayCreate = url.searchParams.get("may_create") === "true";
|
|
8170
|
+
const reason = url.searchParams.get("reason") ?? void 0;
|
|
8171
|
+
const again = reason ? SIGN_IN_AGAIN[reason] : void 0;
|
|
8172
|
+
if (again && authorize) {
|
|
8173
|
+
refused = reason;
|
|
8174
|
+
const retry = signInAgainUrl(authorize);
|
|
8175
|
+
send2(200, `<p>${html(again.why)}</p><p><a href="${html(retry)}">${html(again.label)} \u2192</a></p>`);
|
|
8176
|
+
input.onSignInAgain?.(reason, retry);
|
|
8177
|
+
return;
|
|
8178
|
+
}
|
|
8179
|
+
send2(200, `<p>${html(noBusinessMessage(input.consoleUrl, mayCreate, reason))}</p>`);
|
|
8180
|
+
settle({ kind: "no_business", mayCreate, reason });
|
|
8077
8181
|
} else if (error) {
|
|
8078
8182
|
send2(400, `<p>Sign-in failed: ${html(url.searchParams.get("error_description") ?? error)}</p>`);
|
|
8079
8183
|
settle({ kind: "error", error, description: url.searchParams.get("error_description") ?? void 0 });
|
|
@@ -8101,12 +8205,17 @@ async function startLoopback(io, input) {
|
|
|
8101
8205
|
});
|
|
8102
8206
|
const port = server.address().port;
|
|
8103
8207
|
const timer = setTimeout(() => {
|
|
8104
|
-
fail(
|
|
8208
|
+
fail(
|
|
8209
|
+
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 `amdahl login` again.")
|
|
8210
|
+
);
|
|
8105
8211
|
}, input.timeoutMs);
|
|
8106
8212
|
timer.unref?.();
|
|
8107
8213
|
return {
|
|
8108
8214
|
redirectUri: `http://127.0.0.1:${port}/callback`,
|
|
8109
8215
|
result,
|
|
8216
|
+
setAuthorizeUrl: (url) => {
|
|
8217
|
+
authorize = url;
|
|
8218
|
+
},
|
|
8110
8219
|
close: () => {
|
|
8111
8220
|
clearTimeout(timer);
|
|
8112
8221
|
server.closeAllConnections?.();
|
|
@@ -8124,7 +8233,7 @@ function consoleUrlFor(io, apiUrl) {
|
|
|
8124
8233
|
}
|
|
8125
8234
|
|
|
8126
8235
|
// src/secrets.ts
|
|
8127
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
|
|
8236
|
+
import { chmodSync as chmodSync2, existsSync as existsSync2, readFileSync as readFileSync2, rmSync, statSync } from "fs";
|
|
8128
8237
|
import { join as join2 } from "path";
|
|
8129
8238
|
var KEYCHAIN_SERVICE = "amdahl-cli";
|
|
8130
8239
|
function keychainAccount(apiUrl, profile) {
|
|
@@ -8211,10 +8320,28 @@ function readCredentials(path) {
|
|
|
8211
8320
|
return { version: 1, accounts: {} };
|
|
8212
8321
|
}
|
|
8213
8322
|
}
|
|
8214
|
-
function fileStore(io,
|
|
8323
|
+
function fileStore(io, note) {
|
|
8215
8324
|
const path = credentialsPath(io);
|
|
8325
|
+
let checked = false;
|
|
8326
|
+
const checkMode = () => {
|
|
8327
|
+
if (checked || io.platform === "win32" || !existsSync2(path)) return;
|
|
8328
|
+
checked = true;
|
|
8329
|
+
if ((statSync(path).mode & 63) === 0) return;
|
|
8330
|
+
try {
|
|
8331
|
+
chmodSync2(path, 384);
|
|
8332
|
+
} catch {
|
|
8333
|
+
io.stderr(`Warning: ${path} can be read by other users on this machine, and this CLI cannot change that. Run \`chmod 600 ${path}\` as its owner.
|
|
8334
|
+
`);
|
|
8335
|
+
return;
|
|
8336
|
+
}
|
|
8337
|
+
io.stderr(
|
|
8338
|
+
`Warning: ${path} was readable by other users on this machine. It is private now; to be safe, run \`amdahl logout\` and sign in again.
|
|
8339
|
+
`
|
|
8340
|
+
);
|
|
8341
|
+
};
|
|
8216
8342
|
return {
|
|
8217
8343
|
async get(account) {
|
|
8344
|
+
checkMode();
|
|
8218
8345
|
return asSecrets(readCredentials(path).accounts[account]);
|
|
8219
8346
|
},
|
|
8220
8347
|
async set(account, secrets) {
|
|
@@ -8222,11 +8349,9 @@ function fileStore(io, warn) {
|
|
|
8222
8349
|
const file = readCredentials(path);
|
|
8223
8350
|
file.accounts[account] = secrets;
|
|
8224
8351
|
writePrivateJson(path, file);
|
|
8225
|
-
if (!existed &&
|
|
8226
|
-
io.stderr(
|
|
8227
|
-
|
|
8228
|
-
`
|
|
8229
|
-
);
|
|
8352
|
+
if (!existed && note) {
|
|
8353
|
+
io.stderr(`No OS keychain here, so your sign-in is stored in ${path} (readable only by you).
|
|
8354
|
+
`);
|
|
8230
8355
|
}
|
|
8231
8356
|
},
|
|
8232
8357
|
async delete(account) {
|
|
@@ -8238,7 +8363,7 @@ function fileStore(io, warn) {
|
|
|
8238
8363
|
}
|
|
8239
8364
|
};
|
|
8240
8365
|
}
|
|
8241
|
-
function createSecretStore(io) {
|
|
8366
|
+
function createSecretStore(io, options = {}) {
|
|
8242
8367
|
const forced = io.env.AMDAHL_CREDENTIAL_STORE;
|
|
8243
8368
|
if (forced && forced !== "keychain" && forced !== "file") {
|
|
8244
8369
|
throw new CliError(
|
|
@@ -8265,7 +8390,7 @@ function createSecretStore(io) {
|
|
|
8265
8390
|
}
|
|
8266
8391
|
};
|
|
8267
8392
|
}
|
|
8268
|
-
const file = fileStore(io, true);
|
|
8393
|
+
const file = fileStore(io, options.verbose === true);
|
|
8269
8394
|
if (!keychain) return file;
|
|
8270
8395
|
return {
|
|
8271
8396
|
async get(account) {
|
|
@@ -8317,7 +8442,7 @@ function createCtx(io, out, flags) {
|
|
|
8317
8442
|
profile,
|
|
8318
8443
|
apiUrl: normaliseApiUrl(rawApi),
|
|
8319
8444
|
nonInteractive: !io.stdinIsTTY || io.env.AMDAHL_NO_PROMPT === "1",
|
|
8320
|
-
secrets: () => store ??= createSecretStore(io),
|
|
8445
|
+
secrets: () => store ??= createSecretStore(io, { verbose: flags.verbose === true }),
|
|
8321
8446
|
saveConfig: () => saveConfig(io, config)
|
|
8322
8447
|
};
|
|
8323
8448
|
}
|
|
@@ -8415,15 +8540,45 @@ async function call(fn) {
|
|
|
8415
8540
|
}
|
|
8416
8541
|
}
|
|
8417
8542
|
|
|
8543
|
+
// src/output.ts
|
|
8544
|
+
var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
8545
|
+
function printable(text) {
|
|
8546
|
+
return text.replace(CONTROL_RE, "");
|
|
8547
|
+
}
|
|
8548
|
+
function createOutput(io, json, noColor) {
|
|
8549
|
+
const color = !noColor && !io.env.NO_COLOR && io.env.TERM !== "dumb" && io.stderrIsTTY && !json;
|
|
8550
|
+
return {
|
|
8551
|
+
json,
|
|
8552
|
+
color,
|
|
8553
|
+
result(data, human) {
|
|
8554
|
+
if (json) {
|
|
8555
|
+
io.stdout(`${JSON.stringify(data)}
|
|
8556
|
+
`);
|
|
8557
|
+
} else if (human !== void 0) {
|
|
8558
|
+
io.stdout(human.endsWith("\n") ? human : `${human}
|
|
8559
|
+
`);
|
|
8560
|
+
}
|
|
8561
|
+
},
|
|
8562
|
+
info(text) {
|
|
8563
|
+
io.stderr(text.endsWith("\n") ? text : `${text}
|
|
8564
|
+
`);
|
|
8565
|
+
},
|
|
8566
|
+
event(data) {
|
|
8567
|
+
if (json) io.stderr(`${JSON.stringify(data)}
|
|
8568
|
+
`);
|
|
8569
|
+
},
|
|
8570
|
+
bold(text) {
|
|
8571
|
+
return color ? `\x1B[1m${text}\x1B[22m` : text;
|
|
8572
|
+
}
|
|
8573
|
+
};
|
|
8574
|
+
}
|
|
8575
|
+
|
|
8418
8576
|
// src/commands/auth.ts
|
|
8419
8577
|
var PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
8420
|
-
function noBusinessMessage(consoleUrl, mayCreate) {
|
|
8421
|
-
return mayCreate ? `You're signed in, but you don't have a workspace yet. Create one at ${consoleUrl}/new, then run \`amdahl 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 \`amdahl login\` again once you're in.`;
|
|
8422
|
-
}
|
|
8423
8578
|
function tokenPreview(token) {
|
|
8424
8579
|
return `${token.slice(0, 10)}...`;
|
|
8425
8580
|
}
|
|
8426
|
-
async function
|
|
8581
|
+
async function browserSignIn(ctx) {
|
|
8427
8582
|
const { io, out, flags } = ctx;
|
|
8428
8583
|
const workspace = str(flags.workspace);
|
|
8429
8584
|
const rawPort = str(flags.port);
|
|
@@ -8438,35 +8593,52 @@ async function login(ctx) {
|
|
|
8438
8593
|
const endpoints = await discover(ctx.apiUrl);
|
|
8439
8594
|
const pkce = createPkce();
|
|
8440
8595
|
const consoleUrl = consoleUrlFor(io, ctx.apiUrl);
|
|
8596
|
+
let timer;
|
|
8597
|
+
let linkShown = false;
|
|
8441
8598
|
const loopback = await startLoopback(io, {
|
|
8442
8599
|
port,
|
|
8443
8600
|
state: pkce.state,
|
|
8444
8601
|
consoleUrl,
|
|
8445
|
-
timeoutMs: io.loginTimeoutMs
|
|
8602
|
+
timeoutMs: io.loginTimeoutMs,
|
|
8603
|
+
// The page carries the way on; the authorize link would only sign in as the same address again.
|
|
8604
|
+
onSignInAgain: (reason, retry) => {
|
|
8605
|
+
clearTimeout(timer);
|
|
8606
|
+
linkShown = true;
|
|
8607
|
+
out.info(signInAgainLine(reason, retry));
|
|
8608
|
+
}
|
|
8446
8609
|
});
|
|
8447
8610
|
try {
|
|
8448
8611
|
const url = authorizeUrl(endpoints, { redirectUri: loopback.redirectUri, pkce, workspace });
|
|
8612
|
+
loopback.setAuthorizeUrl(url);
|
|
8449
8613
|
out.event({ event: "open_url", url });
|
|
8450
|
-
|
|
8451
|
-
|
|
8614
|
+
const showLink = (lead) => {
|
|
8615
|
+
if (linkShown) return;
|
|
8616
|
+
linkShown = true;
|
|
8617
|
+
out.info(`${lead}
|
|
8452
8618
|
|
|
8453
8619
|
${url}
|
|
8454
8620
|
`);
|
|
8621
|
+
};
|
|
8622
|
+
if (flags["no-browser"]) {
|
|
8623
|
+
showLink("Open this URL in a browser to sign in:");
|
|
8455
8624
|
} else {
|
|
8456
|
-
out.info(
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8625
|
+
out.info("Opening your browser\u2026");
|
|
8626
|
+
void io.openUrl(url).then((opened) => {
|
|
8627
|
+
if (!opened) showLink("Your browser did not open. Open this link to sign in:");
|
|
8628
|
+
});
|
|
8629
|
+
timer = setTimeout(() => showLink("Finish in your browser, or open this link:"), io.browserGraceMs);
|
|
8630
|
+
timer.unref?.();
|
|
8461
8631
|
}
|
|
8462
8632
|
const callback = await loopback.result;
|
|
8633
|
+
clearTimeout(timer);
|
|
8634
|
+
linkShown = true;
|
|
8463
8635
|
if (callback.kind === "no_business") {
|
|
8464
|
-
throw new CliError("no_business", noBusinessMessage(consoleUrl, callback.mayCreate), EXIT.noWorkspace);
|
|
8636
|
+
throw new CliError("no_business", noBusinessMessage(consoleUrl, callback.mayCreate, callback.reason), EXIT.noWorkspace);
|
|
8465
8637
|
}
|
|
8466
8638
|
if (callback.kind === "error") {
|
|
8467
8639
|
throw new CliError(
|
|
8468
8640
|
callback.error,
|
|
8469
|
-
`Sign-in failed: ${callback.description ?? callback.error}`,
|
|
8641
|
+
`Sign-in failed: ${printable(callback.description ?? callback.error)}`,
|
|
8470
8642
|
callback.error === "access_denied" ? EXIT.forbidden : EXIT.general
|
|
8471
8643
|
);
|
|
8472
8644
|
}
|
|
@@ -8490,22 +8662,31 @@ async function login(ctx) {
|
|
|
8490
8662
|
};
|
|
8491
8663
|
ctx.config.default_profile = name;
|
|
8492
8664
|
ctx.saveConfig();
|
|
8493
|
-
|
|
8494
|
-
{
|
|
8495
|
-
ok: true,
|
|
8496
|
-
profile: name,
|
|
8497
|
-
user: { id: me.user_id, email: me.email ?? null },
|
|
8498
|
-
workspace: ws,
|
|
8499
|
-
scopes: me.scopes,
|
|
8500
|
-
api_url: ctx.apiUrl
|
|
8501
|
-
},
|
|
8502
|
-
`Signed in as ${me.email ?? me.user_id} to ${out.bold(ws.name)} (${ws.slug}). Profile: ${name}.`
|
|
8503
|
-
);
|
|
8504
|
-
return EXIT.ok;
|
|
8665
|
+
return { profile: name, me, workspace: ws };
|
|
8505
8666
|
} finally {
|
|
8667
|
+
clearTimeout(timer);
|
|
8506
8668
|
loopback.close();
|
|
8507
8669
|
}
|
|
8508
8670
|
}
|
|
8671
|
+
function signedInLine(ctx, { profile, me, workspace }) {
|
|
8672
|
+
return `Signed in as ${printable(me.email ?? me.user_id)} to ${ctx.out.bold(printable(workspace.name))} (${printable(workspace.slug)}). Profile: ${profile}.`;
|
|
8673
|
+
}
|
|
8674
|
+
async function login(ctx) {
|
|
8675
|
+
const signedIn = await browserSignIn(ctx);
|
|
8676
|
+
const { profile, me, workspace } = signedIn;
|
|
8677
|
+
ctx.out.result(
|
|
8678
|
+
{
|
|
8679
|
+
ok: true,
|
|
8680
|
+
profile,
|
|
8681
|
+
user: { id: me.user_id, email: me.email ?? null },
|
|
8682
|
+
workspace,
|
|
8683
|
+
scopes: me.scopes,
|
|
8684
|
+
api_url: ctx.apiUrl
|
|
8685
|
+
},
|
|
8686
|
+
signedInLine(ctx, signedIn)
|
|
8687
|
+
);
|
|
8688
|
+
return EXIT.ok;
|
|
8689
|
+
}
|
|
8509
8690
|
async function logout(ctx) {
|
|
8510
8691
|
const { config, out } = ctx;
|
|
8511
8692
|
const names = ctx.flags.all ? Object.keys(config.profiles) : ctx.profileName && config.profiles[ctx.profileName] ? [ctx.profileName] : [];
|
|
@@ -8623,6 +8804,12 @@ function skillPath(client, home) {
|
|
|
8623
8804
|
|
|
8624
8805
|
// src/commands/install.ts
|
|
8625
8806
|
var CLIENTS = ["claude-code", "codex", "cursor"];
|
|
8807
|
+
function codexConfigPath(home) {
|
|
8808
|
+
return join4(home, ".codex", "config.toml");
|
|
8809
|
+
}
|
|
8810
|
+
function cursorConfigPath(home) {
|
|
8811
|
+
return join4(home, ".cursor", "mcp.json");
|
|
8812
|
+
}
|
|
8626
8813
|
function codexBlock(mcpUrl) {
|
|
8627
8814
|
return `[mcp_servers.amdahl]
|
|
8628
8815
|
url = "${mcpUrl}"
|
|
@@ -8637,14 +8824,60 @@ async function onPath(ctx, command) {
|
|
|
8637
8824
|
const res = await ctx.io.run(probe, [command]);
|
|
8638
8825
|
return res.code === 0;
|
|
8639
8826
|
}
|
|
8827
|
+
var CLIENT_SIGNS = {
|
|
8828
|
+
"claude-code": { command: "claude", dir: ".claude" },
|
|
8829
|
+
codex: { command: "codex", dir: ".codex" },
|
|
8830
|
+
cursor: { command: "cursor", dir: ".cursor" }
|
|
8831
|
+
};
|
|
8832
|
+
async function detectClients(ctx) {
|
|
8833
|
+
const found = await Promise.all(
|
|
8834
|
+
CLIENTS.map(async (client) => {
|
|
8835
|
+
const { command, dir } = CLIENT_SIGNS[client];
|
|
8836
|
+
return existsSync3(join4(ctx.io.homedir, dir)) || await onPath(ctx, command);
|
|
8837
|
+
})
|
|
8838
|
+
);
|
|
8839
|
+
return Object.fromEntries(CLIENTS.map((client, i) => [client, found[i]]));
|
|
8840
|
+
}
|
|
8841
|
+
async function claudeHasAmdahl(ctx, mcpUrl) {
|
|
8842
|
+
const res = await ctx.io.run("claude", ["mcp", "get", "amdahl"]);
|
|
8843
|
+
return res.code === 0 && res.stdout.split("\n").some((line) => line.trim() === `URL: ${mcpUrl}`);
|
|
8844
|
+
}
|
|
8845
|
+
function updateConfig(path, fn) {
|
|
8846
|
+
try {
|
|
8847
|
+
return fn();
|
|
8848
|
+
} catch (err) {
|
|
8849
|
+
if (err instanceof CliError) throw err;
|
|
8850
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
8851
|
+
throw new CliError("write_failed", `Could not update ${path} (${reason}).`, EXIT.general);
|
|
8852
|
+
}
|
|
8853
|
+
}
|
|
8854
|
+
async function isInstalled(ctx, client) {
|
|
8855
|
+
const skill = skillPath(client, ctx.io.homedir);
|
|
8856
|
+
const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
|
|
8857
|
+
if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
|
|
8858
|
+
const mcpUrl = `${ctx.apiUrl}/mcp`;
|
|
8859
|
+
if (client === "claude-code") return await onPath(ctx, "claude") && await claudeHasAmdahl(ctx, mcpUrl);
|
|
8860
|
+
if (client === "codex") {
|
|
8861
|
+
const path = codexConfigPath(ctx.io.homedir);
|
|
8862
|
+
return existsSync3(path) && codexHasAmdahl(readFileSync3(path, "utf8"));
|
|
8863
|
+
}
|
|
8864
|
+
try {
|
|
8865
|
+
const config = JSON.parse(readFileSync3(cursorConfigPath(ctx.io.homedir), "utf8"));
|
|
8866
|
+
return JSON.stringify(config.mcpServers?.amdahl) === JSON.stringify({ url: mcpUrl });
|
|
8867
|
+
} catch {
|
|
8868
|
+
return false;
|
|
8869
|
+
}
|
|
8870
|
+
}
|
|
8871
|
+
async function installClient(ctx, client, print) {
|
|
8872
|
+
const mcp = await installMcp(ctx, client, print);
|
|
8873
|
+
return { mcp, skill: installSkill(ctx, client, print) };
|
|
8874
|
+
}
|
|
8640
8875
|
async function install(ctx, args) {
|
|
8641
8876
|
const client = args[0];
|
|
8642
8877
|
if (!client || args.length > 1 || !CLIENTS.includes(client)) {
|
|
8643
8878
|
throw usageError(`Usage: amdahl install ${CLIENTS.join("|")} [--print] [--force]`);
|
|
8644
8879
|
}
|
|
8645
|
-
const
|
|
8646
|
-
const mcp = await installMcp(ctx, client, print);
|
|
8647
|
-
const skill = installSkill(ctx, client, print);
|
|
8880
|
+
const { mcp, skill } = await installClient(ctx, client, ctx.flags.print === true);
|
|
8648
8881
|
const human = [mcp.human.replace(/\n$/, ""), skill.human].filter(Boolean).join("\n");
|
|
8649
8882
|
ctx.out.result({ ...mcp.data, skill: skill.data }, human);
|
|
8650
8883
|
for (const note of [...mcp.notes, ...skill.notes]) ctx.out.info(note);
|
|
@@ -8685,6 +8918,9 @@ async function installMcp(ctx, client, print) {
|
|
|
8685
8918
|
const notes = print ? [] : ["`claude` is not on your PATH. Run the command above once it is installed."];
|
|
8686
8919
|
return { data: { ok: true, client, action: "printed", command }, human: command, notes };
|
|
8687
8920
|
}
|
|
8921
|
+
if (await claudeHasAmdahl(ctx, mcpUrl)) {
|
|
8922
|
+
return { data: { ok: true, client, action: "unchanged", command }, human: `Claude Code already has Amdahl at ${mcpUrl}; nothing changed.`, notes: [] };
|
|
8923
|
+
}
|
|
8688
8924
|
const res = await io.run("claude", args);
|
|
8689
8925
|
if (res.code !== 0) {
|
|
8690
8926
|
throw new CliError("install_failed", `\`${command}\` failed: ${res.stderr.trim() || `exit ${res.code}`}`, EXIT.general);
|
|
@@ -8692,47 +8928,51 @@ async function installMcp(ctx, client, print) {
|
|
|
8692
8928
|
return { data: { ok: true, client, action: "ran", command }, human: `Added Amdahl to Claude Code for all your projects. Run /mcp in Claude Code to sign in. If amdahl is not listed there, restart Claude Code first.`, notes: [] };
|
|
8693
8929
|
}
|
|
8694
8930
|
if (client === "codex") {
|
|
8695
|
-
const path2 =
|
|
8931
|
+
const path2 = codexConfigPath(io.homedir);
|
|
8696
8932
|
const block = codexBlock(mcpUrl);
|
|
8697
8933
|
if (print) {
|
|
8698
8934
|
return { data: { ok: true, client, action: "printed", path: path2 }, human: `# ${path2}
|
|
8699
8935
|
${block}`, notes: [] };
|
|
8700
8936
|
}
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8937
|
+
return updateConfig(path2, () => {
|
|
8938
|
+
const current = existsSync3(path2) ? readFileSync3(path2, "utf8") : "";
|
|
8939
|
+
if (codexHasAmdahl(current)) {
|
|
8940
|
+
return { data: { ok: true, client, action: "unchanged", path: path2 }, human: `${path2} already has [mcp_servers.amdahl]; nothing changed.`, notes: [] };
|
|
8941
|
+
}
|
|
8942
|
+
mkdirSync2(dirname(path2), { recursive: true });
|
|
8943
|
+
const sep2 = current.length === 0 ? "" : current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
8944
|
+
writeFileSync2(path2, `${current}${sep2}${block}`);
|
|
8945
|
+
return { data: { ok: true, client, action: "wrote", path: path2 }, human: `Added Amdahl to ${path2}. Run \`codex mcp login amdahl\` to sign in.`, notes: [] };
|
|
8946
|
+
});
|
|
8709
8947
|
}
|
|
8710
|
-
const path =
|
|
8948
|
+
const path = cursorConfigPath(io.homedir);
|
|
8711
8949
|
const entry = { url: mcpUrl };
|
|
8712
8950
|
if (print) {
|
|
8713
8951
|
return { data: { ok: true, client, action: "printed", path }, human: `# ${path}
|
|
8714
8952
|
${JSON.stringify({ mcpServers: { amdahl: entry } }, null, 2)}`, notes: [] };
|
|
8715
8953
|
}
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8954
|
+
return updateConfig(path, () => {
|
|
8955
|
+
let config = {};
|
|
8956
|
+
if (existsSync3(path)) {
|
|
8957
|
+
const raw = readFileSync3(path, "utf8");
|
|
8958
|
+
if (raw.trim()) {
|
|
8959
|
+
try {
|
|
8960
|
+
config = JSON.parse(raw);
|
|
8961
|
+
} catch {
|
|
8962
|
+
throw new CliError("invalid_config", `${path} is not valid JSON; not changing it.`, EXIT.general);
|
|
8963
|
+
}
|
|
8724
8964
|
}
|
|
8725
8965
|
}
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
writeFileSync2(path, `${JSON.stringify(config, null, 2)}
|
|
8966
|
+
const servers = config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {};
|
|
8967
|
+
if (JSON.stringify(servers.amdahl) === JSON.stringify(entry)) {
|
|
8968
|
+
return { data: { ok: true, client, action: "unchanged", path }, human: `${path} already points at ${mcpUrl}; nothing changed.`, notes: [] };
|
|
8969
|
+
}
|
|
8970
|
+
config.mcpServers = { ...servers, amdahl: entry };
|
|
8971
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
8972
|
+
writeFileSync2(path, `${JSON.stringify(config, null, 2)}
|
|
8734
8973
|
`);
|
|
8735
|
-
|
|
8974
|
+
return { data: { ok: true, client, action: "wrote", path }, human: `Added Amdahl to ${path}. Cursor asks you to sign in on first use.`, notes: [] };
|
|
8975
|
+
});
|
|
8736
8976
|
}
|
|
8737
8977
|
|
|
8738
8978
|
// src/keygen.ts
|
|
@@ -9008,260 +9248,43 @@ async function optimize(ctx, args) {
|
|
|
9008
9248
|
return exit;
|
|
9009
9249
|
}
|
|
9010
9250
|
|
|
9011
|
-
// src/commands/skills.ts
|
|
9012
|
-
|
|
9013
|
-
import {
|
|
9251
|
+
// src/commands/skills-guide.ts
|
|
9252
|
+
import { existsSync as existsSync4 } from "fs";
|
|
9253
|
+
import { dirname as dirname3, join as join6, relative, sep } from "path";
|
|
9014
9254
|
|
|
9015
|
-
// src/
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9255
|
+
// src/commands/skills-install.ts
|
|
9256
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
9257
|
+
import { chmodSync as chmodSync3, lstatSync, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
9258
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
9259
|
+
function skillDirs(targets, name) {
|
|
9260
|
+
const dirs = [];
|
|
9261
|
+
if (targets.claude) dirs.push(join5(targets.base, ".claude", "skills", name));
|
|
9262
|
+
if (targets.agents) dirs.push(join5(targets.base, ".agents", "skills", name));
|
|
9263
|
+
return dirs;
|
|
9019
9264
|
}
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
`);
|
|
9029
|
-
} else if (human !== void 0) {
|
|
9030
|
-
io.stdout(human.endsWith("\n") ? human : `${human}
|
|
9031
|
-
`);
|
|
9032
|
-
}
|
|
9033
|
-
},
|
|
9034
|
-
info(text) {
|
|
9035
|
-
io.stderr(text.endsWith("\n") ? text : `${text}
|
|
9036
|
-
`);
|
|
9037
|
-
},
|
|
9038
|
-
event(data) {
|
|
9039
|
-
if (json) io.stderr(`${JSON.stringify(data)}
|
|
9040
|
-
`);
|
|
9041
|
-
},
|
|
9042
|
-
bold(text) {
|
|
9043
|
-
return color ? `\x1B[1m${text}\x1B[22m` : text;
|
|
9044
|
-
}
|
|
9045
|
-
};
|
|
9265
|
+
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
9266
|
+
function plainReason(err) {
|
|
9267
|
+
const code = err.code;
|
|
9268
|
+
if (code === "EACCES" || code === "EPERM") return "you do not have permission to write there";
|
|
9269
|
+
if (code === "ENOSPC") return "the disk is full";
|
|
9270
|
+
if (code === "EROFS") return "that disk is read-only";
|
|
9271
|
+
if (code === "ENOTDIR" || code === "EEXIST") return "a file is in the way";
|
|
9272
|
+
return message(err);
|
|
9046
9273
|
}
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
var BLOCK = 512;
|
|
9054
|
-
var FILE_MODES = /* @__PURE__ */ new Set([420, 493]);
|
|
9055
|
-
var CONTROL_RE2 = /[\u0000-\u001f\u007f-\u009f]/;
|
|
9056
|
-
var ArchiveError = class extends Error {
|
|
9057
|
-
tooLarge;
|
|
9058
|
-
constructor(message2, tooLarge = false) {
|
|
9059
|
-
super(message2);
|
|
9060
|
-
this.name = "ArchiveError";
|
|
9061
|
-
this.tooLarge = tooLarge;
|
|
9062
|
-
}
|
|
9063
|
-
};
|
|
9064
|
-
var crcTable = null;
|
|
9065
|
-
function crc32(data) {
|
|
9066
|
-
if (!crcTable) {
|
|
9067
|
-
crcTable = new Uint32Array(256);
|
|
9068
|
-
for (let n = 0; n < 256; n += 1) {
|
|
9069
|
-
let c = n;
|
|
9070
|
-
for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
9071
|
-
crcTable[n] = c >>> 0;
|
|
9072
|
-
}
|
|
9073
|
-
}
|
|
9074
|
-
let crc = 4294967295;
|
|
9075
|
-
for (let i = 0; i < data.length; i += 1) crc = crcTable[(crc ^ data[i]) & 255] ^ crc >>> 8;
|
|
9076
|
-
return (crc ^ 4294967295) >>> 0;
|
|
9274
|
+
var leftBehind = (written) => written.length > 0 ? `It wrote ${written.join(" and ")} first.` : "Nothing was written.";
|
|
9275
|
+
function writeFailure(path, err, written = []) {
|
|
9276
|
+
const code = err.code;
|
|
9277
|
+
const left = leftBehind(written);
|
|
9278
|
+
const fix = code === "EACCES" || code === "EPERM" ? " Run the command from a folder you can write to." : "";
|
|
9279
|
+
return new CliError("write_failed", `Could not write ${path}: ${plainReason(err)}`, EXIT.general, `${left}${fix}`);
|
|
9077
9280
|
}
|
|
9078
|
-
function
|
|
9079
|
-
|
|
9080
|
-
if (gz[2] !== 8) throw new ArchiveError("its gzip method is not deflate");
|
|
9081
|
-
const flags = gz[3];
|
|
9082
|
-
if (flags & 224) throw new ArchiveError("its gzip header sets reserved flags");
|
|
9083
|
-
let at = 10;
|
|
9084
|
-
if (flags & 4) at += 2 + gz.readUInt16LE(Math.min(at, gz.length - 2));
|
|
9085
|
-
for (const flag of [8, 16]) {
|
|
9086
|
-
if (!(flags & flag)) continue;
|
|
9087
|
-
const end = gz.indexOf(0, at);
|
|
9088
|
-
if (end < 0) throw new ArchiveError("its gzip header is cut short");
|
|
9089
|
-
at = end + 1;
|
|
9090
|
-
}
|
|
9091
|
-
if (flags & 2) at += 2;
|
|
9092
|
-
if (at > gz.length - 8) throw new ArchiveError("its gzip header is cut short");
|
|
9093
|
-
let tar;
|
|
9094
|
-
let used;
|
|
9281
|
+
function sameFolder(dir, archive) {
|
|
9282
|
+
let top;
|
|
9095
9283
|
try {
|
|
9096
|
-
|
|
9097
|
-
tar = out.buffer;
|
|
9098
|
-
used = out.engine.bytesWritten;
|
|
9284
|
+
top = lstatSync(dir);
|
|
9099
9285
|
} catch (err) {
|
|
9100
|
-
if (err.code === "
|
|
9101
|
-
|
|
9102
|
-
}
|
|
9103
|
-
throw new ArchiveError(`its gzip data is broken (${err instanceof Error ? err.message : String(err)})`);
|
|
9104
|
-
}
|
|
9105
|
-
const trailer = at + used;
|
|
9106
|
-
if (gz.length - trailer < 8) throw new ArchiveError("its gzip trailer is cut short");
|
|
9107
|
-
if (gz.length - trailer > 8) throw new ArchiveError("it has bytes after its gzip trailer");
|
|
9108
|
-
if (gz.readUInt32LE(trailer) !== crc32(tar)) throw new ArchiveError("its gzip CRC-32 does not match its contents");
|
|
9109
|
-
if (gz.readUInt32LE(trailer + 4) !== tar.length % 2 ** 32) throw new ArchiveError("its gzip length does not match its contents");
|
|
9110
|
-
return tar;
|
|
9111
|
-
}
|
|
9112
|
-
function field(header, start, length) {
|
|
9113
|
-
const raw = header.subarray(start, start + length);
|
|
9114
|
-
const end = raw.indexOf(0);
|
|
9115
|
-
try {
|
|
9116
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(end < 0 ? raw : raw.subarray(0, end));
|
|
9117
|
-
} catch {
|
|
9118
|
-
throw new ArchiveError("it has a path that is not UTF-8");
|
|
9119
|
-
}
|
|
9120
|
-
}
|
|
9121
|
-
function octal(header, start, length) {
|
|
9122
|
-
const text = header.toString("latin1", start, start + length).replace(/[\0 ]+$/, "").replace(/^ +/, "");
|
|
9123
|
-
return /^[0-7]{1,12}$/.test(text) ? parseInt(text, 8) : null;
|
|
9124
|
-
}
|
|
9125
|
-
function checkPath(path) {
|
|
9126
|
-
const shown = JSON.stringify(path);
|
|
9127
|
-
if (path === "") throw new ArchiveError("it has an entry with an empty path");
|
|
9128
|
-
if (path.startsWith("/")) throw new ArchiveError(`it has an absolute path, ${shown}`);
|
|
9129
|
-
if (path.includes("\\")) throw new ArchiveError(`it has a path with a backslash, ${shown}`);
|
|
9130
|
-
if (CONTROL_RE2.test(path)) throw new ArchiveError(`it has a path with a control character, ${shown}`);
|
|
9131
|
-
for (const segment of path.split("/")) {
|
|
9132
|
-
if (segment === "") throw new ArchiveError(`it has a path with an empty segment, ${shown}`);
|
|
9133
|
-
if (segment === ".." || segment === ".") throw new ArchiveError(`it has a path with "${segment}" in it, ${shown}`);
|
|
9134
|
-
}
|
|
9135
|
-
}
|
|
9136
|
-
var TYPE_NAMES = {
|
|
9137
|
-
"1": "a hard link",
|
|
9138
|
-
"2": "a symbolic link",
|
|
9139
|
-
"3": "a character device",
|
|
9140
|
-
"4": "a block device",
|
|
9141
|
-
"6": "a fifo",
|
|
9142
|
-
"7": "a contiguous file",
|
|
9143
|
-
x: "a pax header",
|
|
9144
|
-
g: "a pax global header",
|
|
9145
|
-
L: "a GNU long name",
|
|
9146
|
-
K: "a GNU long link name"
|
|
9147
|
-
};
|
|
9148
|
-
function readTar(tar) {
|
|
9149
|
-
if (tar.length % BLOCK !== 0) throw new ArchiveError("its tar is not a whole number of 512-byte blocks");
|
|
9150
|
-
const files = [];
|
|
9151
|
-
const dirs = [];
|
|
9152
|
-
const seen = /* @__PURE__ */ new Map();
|
|
9153
|
-
let unpacked = 0;
|
|
9154
|
-
let at = 0;
|
|
9155
|
-
for (; ; ) {
|
|
9156
|
-
if (at >= tar.length) throw new ArchiveError("its tar has no end marker");
|
|
9157
|
-
const header = tar.subarray(at, at + BLOCK);
|
|
9158
|
-
if (header.every((b) => b === 0)) {
|
|
9159
|
-
if (!tar.subarray(at).every((b) => b === 0)) throw new ArchiveError("it has data after its tar end marker");
|
|
9160
|
-
break;
|
|
9161
|
-
}
|
|
9162
|
-
let sum = 0;
|
|
9163
|
-
for (let i = 0; i < BLOCK; i += 1) sum += i >= 148 && i < 156 ? 32 : header[i];
|
|
9164
|
-
if (octal(header, 148, 8) !== sum) throw new ArchiveError("it has a tar header with a wrong checksum");
|
|
9165
|
-
if (header.toString("latin1", 257, 263) !== "ustar\0" || header.toString("latin1", 263, 265) !== "00") {
|
|
9166
|
-
throw new ArchiveError("its tar is not ustar");
|
|
9167
|
-
}
|
|
9168
|
-
const name = field(header, 0, 100);
|
|
9169
|
-
const prefix = field(header, 345, 155);
|
|
9170
|
-
let path = prefix ? `${prefix}/${name}` : name;
|
|
9171
|
-
const type = String.fromCharCode(header[156]);
|
|
9172
|
-
const size = octal(header, 124, 12);
|
|
9173
|
-
const mode = octal(header, 100, 8);
|
|
9174
|
-
if (size === null || mode === null) throw new ArchiveError(`it has a tar header for ${JSON.stringify(path)} with a bad size or mode`);
|
|
9175
|
-
const isDir = type === "5";
|
|
9176
|
-
if (!isDir && type !== "0" && type !== "\0") {
|
|
9177
|
-
const what = TYPE_NAMES[type] ?? `an entry of type ${JSON.stringify(type)}`;
|
|
9178
|
-
throw new ArchiveError(`${JSON.stringify(path)} is ${what}; a skill archive holds only files and folders`);
|
|
9179
|
-
}
|
|
9180
|
-
if (isDir && path.endsWith("/")) path = path.slice(0, -1);
|
|
9181
|
-
checkPath(path);
|
|
9182
|
-
const key = path.toLowerCase();
|
|
9183
|
-
const parts = key.split("/");
|
|
9184
|
-
for (let i = 1; i < parts.length; i += 1) {
|
|
9185
|
-
const parent = parts.slice(0, i).join("/");
|
|
9186
|
-
const kind = seen.get(parent);
|
|
9187
|
-
if (kind === "file") throw new ArchiveError(`it puts ${JSON.stringify(path)} inside a file`);
|
|
9188
|
-
if (!kind) seen.set(parent, "implied");
|
|
9189
|
-
}
|
|
9190
|
-
const before = seen.get(key);
|
|
9191
|
-
if (before === "file" || before === "dir" || before === "implied" && !isDir) {
|
|
9192
|
-
throw new ArchiveError(`it lists ${JSON.stringify(path)} twice`);
|
|
9193
|
-
}
|
|
9194
|
-
const start = at + BLOCK;
|
|
9195
|
-
if (isDir) {
|
|
9196
|
-
if (size !== 0) throw new ArchiveError(`its folder ${JSON.stringify(path)} has a size`);
|
|
9197
|
-
seen.set(key, "dir");
|
|
9198
|
-
dirs.push(path);
|
|
9199
|
-
} else {
|
|
9200
|
-
if (!FILE_MODES.has(mode & 4095)) {
|
|
9201
|
-
throw new ArchiveError(`${JSON.stringify(path)} has mode ${(mode & 4095).toString(8).padStart(4, "0")}; a file must be 0644 or 0755`);
|
|
9202
|
-
}
|
|
9203
|
-
if (files.length + 1 > MAX_ARCHIVE_FILES) throw new ArchiveError(`it has more than ${MAX_ARCHIVE_FILES} files`, true);
|
|
9204
|
-
unpacked += size;
|
|
9205
|
-
if (unpacked > MAX_UNPACKED_BYTES) throw new ArchiveError(`its files come to more than ${MAX_UNPACKED_BYTES / 1024 / 1024} MB`, true);
|
|
9206
|
-
if (start + size > tar.length) throw new ArchiveError(`its tar is cut short inside ${JSON.stringify(path)}`);
|
|
9207
|
-
seen.set(key, "file");
|
|
9208
|
-
files.push({ path, bytes: Buffer.from(tar.subarray(start, start + size)), mode: mode & 4095 });
|
|
9209
|
-
}
|
|
9210
|
-
at = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
9211
|
-
}
|
|
9212
|
-
const allDirs = /* @__PURE__ */ new Map();
|
|
9213
|
-
const addDirsOf = (path, self) => {
|
|
9214
|
-
const parts = path.split("/");
|
|
9215
|
-
for (let i = 1; i <= parts.length - (self ? 0 : 1); i += 1) {
|
|
9216
|
-
const dir = parts.slice(0, i).join("/");
|
|
9217
|
-
if (!allDirs.has(dir.toLowerCase())) allDirs.set(dir.toLowerCase(), dir);
|
|
9218
|
-
}
|
|
9219
|
-
};
|
|
9220
|
-
for (const dir of dirs) addDirsOf(dir, true);
|
|
9221
|
-
for (const f of files) addDirsOf(f.path, false);
|
|
9222
|
-
return { files, dirs: [...allDirs.values()].sort((a, b) => a.split("/").length - b.split("/").length) };
|
|
9223
|
-
}
|
|
9224
|
-
function readSkillArchive(gz) {
|
|
9225
|
-
return readTar(gunzipChecked(gz));
|
|
9226
|
-
}
|
|
9227
|
-
|
|
9228
|
-
// src/commands/skills-guide.ts
|
|
9229
|
-
import { existsSync as existsSync4 } from "fs";
|
|
9230
|
-
import { dirname as dirname3, join as join6, relative, sep } from "path";
|
|
9231
|
-
|
|
9232
|
-
// src/commands/skills-install.ts
|
|
9233
|
-
import { randomBytes as randomBytes3 } from "crypto";
|
|
9234
|
-
import { chmodSync as chmodSync2, lstatSync, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
9235
|
-
import { dirname as dirname2, join as join5 } from "path";
|
|
9236
|
-
function skillDirs(targets, name) {
|
|
9237
|
-
const dirs = [];
|
|
9238
|
-
if (targets.claude) dirs.push(join5(targets.base, ".claude", "skills", name));
|
|
9239
|
-
if (targets.agents) dirs.push(join5(targets.base, ".agents", "skills", name));
|
|
9240
|
-
return dirs;
|
|
9241
|
-
}
|
|
9242
|
-
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
9243
|
-
function plainReason(err) {
|
|
9244
|
-
const code = err.code;
|
|
9245
|
-
if (code === "EACCES" || code === "EPERM") return "you do not have permission to write there";
|
|
9246
|
-
if (code === "ENOSPC") return "the disk is full";
|
|
9247
|
-
if (code === "EROFS") return "that disk is read-only";
|
|
9248
|
-
if (code === "ENOTDIR" || code === "EEXIST") return "a file is in the way";
|
|
9249
|
-
return message(err);
|
|
9250
|
-
}
|
|
9251
|
-
var leftBehind = (written) => written.length > 0 ? `It wrote ${written.join(" and ")} first.` : "Nothing was written.";
|
|
9252
|
-
function writeFailure(path, err, written = []) {
|
|
9253
|
-
const code = err.code;
|
|
9254
|
-
const left = leftBehind(written);
|
|
9255
|
-
const fix = code === "EACCES" || code === "EPERM" ? " Run the command from a folder you can write to." : "";
|
|
9256
|
-
return new CliError("write_failed", `Could not write ${path}: ${plainReason(err)}`, EXIT.general, `${left}${fix}`);
|
|
9257
|
-
}
|
|
9258
|
-
function sameFolder(dir, archive) {
|
|
9259
|
-
let top;
|
|
9260
|
-
try {
|
|
9261
|
-
top = lstatSync(dir);
|
|
9262
|
-
} catch (err) {
|
|
9263
|
-
if (err.code === "ENOENT") return null;
|
|
9264
|
-
throw writeFailure(dir, err);
|
|
9286
|
+
if (err.code === "ENOENT") return null;
|
|
9287
|
+
throw writeFailure(dir, err);
|
|
9265
9288
|
}
|
|
9266
9289
|
if (!top.isDirectory()) return false;
|
|
9267
9290
|
const want = new Map(archive.files.map((f) => [f.path, f]));
|
|
@@ -9348,7 +9371,7 @@ function stage(target, name, archive, staged) {
|
|
|
9348
9371
|
s.createdParent = mkdirSync3(dirname2(target), { recursive: true }) ?? void 0;
|
|
9349
9372
|
refuseLinks(target);
|
|
9350
9373
|
s.temp = mkdtempSync(join5(dirname2(target), `.${name}.amdahl-tmp-`));
|
|
9351
|
-
|
|
9374
|
+
chmodSync3(s.temp, 493);
|
|
9352
9375
|
for (const dir of archive.dirs) mkdirSync3(join5(s.temp, dir));
|
|
9353
9376
|
for (const file of archive.files) writeFileSync3(join5(s.temp, file.path), file.bytes, { flag: "wx", mode: file.mode });
|
|
9354
9377
|
}
|
|
@@ -9503,6 +9526,302 @@ function nextStep(ctx, targets, seen) {
|
|
|
9503
9526
|
return `Open a new ${joinNames(names, "or")} chat ${where} to use it.`;
|
|
9504
9527
|
}
|
|
9505
9528
|
|
|
9529
|
+
// src/commands/setup.ts
|
|
9530
|
+
var SHOWN = [
|
|
9531
|
+
{ client: "claude-code", name: "Claude Code" },
|
|
9532
|
+
{ client: "cursor", name: "Cursor" },
|
|
9533
|
+
{ client: "codex", name: "Codex" }
|
|
9534
|
+
];
|
|
9535
|
+
function tryItLine(name, connect = false) {
|
|
9536
|
+
const ask2 = 'say "make this email sound less like AI"';
|
|
9537
|
+
if (connect && name === "Claude Code") return `Try it: in Claude Code, run /mcp once to connect, then ${ask2}`;
|
|
9538
|
+
if (name === "Codex") return `Try it: run \`codex mcp login amdahl\`, then open Codex and ${ask2}`;
|
|
9539
|
+
return `Try it: open ${name} and ${ask2}`;
|
|
9540
|
+
}
|
|
9541
|
+
function shouldSetUp(io) {
|
|
9542
|
+
const { env } = io;
|
|
9543
|
+
const ci = Boolean(env.CI) && env.CI !== "false" && env.CI !== "0";
|
|
9544
|
+
return io.stdinIsTTY && io.stdoutIsTTY && io.stderrIsTTY && env.AMDAHL_NO_PROMPT !== "1" && !ci && !env.CLAUDECODE && !env.CURSOR_AGENT;
|
|
9545
|
+
}
|
|
9546
|
+
async function signIn(ctx) {
|
|
9547
|
+
let credential;
|
|
9548
|
+
try {
|
|
9549
|
+
credential = await resolveCredential(ctx);
|
|
9550
|
+
} catch (err) {
|
|
9551
|
+
if (err instanceof CliError && err.exit === EXIT.unauthenticated) return { kind: "out" };
|
|
9552
|
+
throw err;
|
|
9553
|
+
}
|
|
9554
|
+
try {
|
|
9555
|
+
const me = await call(() => apiClient(ctx, credential).cli.whoami());
|
|
9556
|
+
return { kind: "in", email: printable(me.email ?? me.user_id), workspace: printable(me.workspace?.name ?? me.business_id) };
|
|
9557
|
+
} catch (err) {
|
|
9558
|
+
if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
|
|
9559
|
+
return credential.source === "profile" ? { kind: "out" } : { kind: "rejected", source: credential.source };
|
|
9560
|
+
}
|
|
9561
|
+
}
|
|
9562
|
+
async function setup(ctx) {
|
|
9563
|
+
const { io, out } = ctx;
|
|
9564
|
+
const verbose = ctx.flags.verbose === true;
|
|
9565
|
+
const say = (line) => io.stdout(`${line}
|
|
9566
|
+
`);
|
|
9567
|
+
const detail = (text) => {
|
|
9568
|
+
if (verbose && text) say(text.replace(/^/gm, " "));
|
|
9569
|
+
};
|
|
9570
|
+
say(`${out.bold("Amdahl")} \xB7 predictive ML for GTM`);
|
|
9571
|
+
const found = await detectClients(ctx);
|
|
9572
|
+
const detected = SHOWN.filter((c) => found[c.client]);
|
|
9573
|
+
let who;
|
|
9574
|
+
if (detected.length > 0) {
|
|
9575
|
+
const installed = await Promise.all(detected.map((c) => isInstalled(ctx, c.client)));
|
|
9576
|
+
if (installed.every(Boolean)) {
|
|
9577
|
+
let unreachable = false;
|
|
9578
|
+
try {
|
|
9579
|
+
who = await signIn(ctx);
|
|
9580
|
+
} catch (err) {
|
|
9581
|
+
if (!(err instanceof CliError && err.exit === EXIT.network)) throw err;
|
|
9582
|
+
unreachable = true;
|
|
9583
|
+
}
|
|
9584
|
+
if (unreachable || who?.kind === "in") {
|
|
9585
|
+
say(`You're set up in ${joinNames(detected.map((c) => c.name))}.${unreachable ? " (Could not reach Amdahl to check your sign-in.)" : ""}`);
|
|
9586
|
+
say(tryItLine(detected[0]?.name ?? ""));
|
|
9587
|
+
return EXIT.ok;
|
|
9588
|
+
}
|
|
9589
|
+
}
|
|
9590
|
+
}
|
|
9591
|
+
const picks = await io.multiSelect(
|
|
9592
|
+
"Set up in:",
|
|
9593
|
+
SHOWN.map((c) => c.name),
|
|
9594
|
+
{ ticked: SHOWN.map((c) => found[c.client]), color: out.color }
|
|
9595
|
+
);
|
|
9596
|
+
if (picks.length === 0) {
|
|
9597
|
+
say("Nothing picked, so nothing changed.");
|
|
9598
|
+
return EXIT.ok;
|
|
9599
|
+
}
|
|
9600
|
+
who ??= await signIn(ctx);
|
|
9601
|
+
if (who.kind === "in") {
|
|
9602
|
+
say(`\u2713 Signed in \xB7 ${who.workspace}`);
|
|
9603
|
+
detail(`Signed in as ${who.email}.`);
|
|
9604
|
+
} else if (who.kind === "out") {
|
|
9605
|
+
const signedIn = await browserSignIn(ctx);
|
|
9606
|
+
say(`\u2713 Signed in \xB7 ${printable(signedIn.workspace.name)}`);
|
|
9607
|
+
detail(signedInLine(ctx, signedIn));
|
|
9608
|
+
} else {
|
|
9609
|
+
const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
|
|
9610
|
+
out.info(`Amdahl rejected the key in ${name}. Skipping the browser sign-in, because that key would still be used instead. Fix it, or unset it and run \`amdahl login\`.`);
|
|
9611
|
+
}
|
|
9612
|
+
let exit = EXIT.ok;
|
|
9613
|
+
let ready = null;
|
|
9614
|
+
for (const { client, name } of picks.flatMap((i) => SHOWN[i] ?? [])) {
|
|
9615
|
+
let steps;
|
|
9616
|
+
try {
|
|
9617
|
+
steps = await installClient(ctx, client, false);
|
|
9618
|
+
} catch (raw) {
|
|
9619
|
+
const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
|
|
9620
|
+
io.stderr(`Error: ${err.message}
|
|
9621
|
+
${err.hint ? `${err.hint}
|
|
9622
|
+
` : ""}`);
|
|
9623
|
+
if (exit === EXIT.ok) exit = err.exit;
|
|
9624
|
+
continue;
|
|
9625
|
+
}
|
|
9626
|
+
const action = steps.mcp.data.action;
|
|
9627
|
+
if (action === "printed") {
|
|
9628
|
+
say(`! ${name}: \`claude\` is not on your PATH. Once it is, run:
|
|
9629
|
+
${String(steps.mcp.data.command)}`);
|
|
9630
|
+
} else {
|
|
9631
|
+
say(action === "unchanged" ? `\u2713 ${name} already has Amdahl` : `\u2713 Added to ${name}`);
|
|
9632
|
+
ready ??= { name, connect: action !== "unchanged" };
|
|
9633
|
+
}
|
|
9634
|
+
const mcpDetail = action === "printed" ? [] : [steps.mcp.human, ...steps.mcp.notes];
|
|
9635
|
+
detail([...mcpDetail, steps.skill.human, ...steps.skill.notes].filter(Boolean).join("\n"));
|
|
9636
|
+
}
|
|
9637
|
+
if (ready) say(tryItLine(ready.name, ready.connect));
|
|
9638
|
+
return exit;
|
|
9639
|
+
}
|
|
9640
|
+
|
|
9641
|
+
// src/commands/skills.ts
|
|
9642
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
9643
|
+
import { createHash as createHash4 } from "crypto";
|
|
9644
|
+
|
|
9645
|
+
// src/tar-gz.ts
|
|
9646
|
+
import { inflateRawSync } from "zlib";
|
|
9647
|
+
var MAX_ARCHIVE_FILES = 200;
|
|
9648
|
+
var MAX_UNPACKED_BYTES = 4 * 1024 * 1024;
|
|
9649
|
+
var MAX_TAR_BYTES = MAX_UNPACKED_BYTES + 1024 * 1024;
|
|
9650
|
+
var BLOCK = 512;
|
|
9651
|
+
var FILE_MODES = /* @__PURE__ */ new Set([420, 493]);
|
|
9652
|
+
var CONTROL_RE2 = /[\u0000-\u001f\u007f-\u009f]/;
|
|
9653
|
+
var ArchiveError = class extends Error {
|
|
9654
|
+
tooLarge;
|
|
9655
|
+
constructor(message2, tooLarge = false) {
|
|
9656
|
+
super(message2);
|
|
9657
|
+
this.name = "ArchiveError";
|
|
9658
|
+
this.tooLarge = tooLarge;
|
|
9659
|
+
}
|
|
9660
|
+
};
|
|
9661
|
+
var crcTable = null;
|
|
9662
|
+
function crc32(data) {
|
|
9663
|
+
if (!crcTable) {
|
|
9664
|
+
crcTable = new Uint32Array(256);
|
|
9665
|
+
for (let n = 0; n < 256; n += 1) {
|
|
9666
|
+
let c = n;
|
|
9667
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
9668
|
+
crcTable[n] = c >>> 0;
|
|
9669
|
+
}
|
|
9670
|
+
}
|
|
9671
|
+
let crc = 4294967295;
|
|
9672
|
+
for (let i = 0; i < data.length; i += 1) crc = crcTable[(crc ^ data[i]) & 255] ^ crc >>> 8;
|
|
9673
|
+
return (crc ^ 4294967295) >>> 0;
|
|
9674
|
+
}
|
|
9675
|
+
function gunzipChecked(gz) {
|
|
9676
|
+
if (gz.length < 18 || gz[0] !== 31 || gz[1] !== 139) throw new ArchiveError("it is not gzip");
|
|
9677
|
+
if (gz[2] !== 8) throw new ArchiveError("its gzip method is not deflate");
|
|
9678
|
+
const flags = gz[3];
|
|
9679
|
+
if (flags & 224) throw new ArchiveError("its gzip header sets reserved flags");
|
|
9680
|
+
let at = 10;
|
|
9681
|
+
if (flags & 4) at += 2 + gz.readUInt16LE(Math.min(at, gz.length - 2));
|
|
9682
|
+
for (const flag of [8, 16]) {
|
|
9683
|
+
if (!(flags & flag)) continue;
|
|
9684
|
+
const end = gz.indexOf(0, at);
|
|
9685
|
+
if (end < 0) throw new ArchiveError("its gzip header is cut short");
|
|
9686
|
+
at = end + 1;
|
|
9687
|
+
}
|
|
9688
|
+
if (flags & 2) at += 2;
|
|
9689
|
+
if (at > gz.length - 8) throw new ArchiveError("its gzip header is cut short");
|
|
9690
|
+
let tar;
|
|
9691
|
+
let used;
|
|
9692
|
+
try {
|
|
9693
|
+
const out = inflateRawSync(gz.subarray(at), { info: true, maxOutputLength: MAX_TAR_BYTES });
|
|
9694
|
+
tar = out.buffer;
|
|
9695
|
+
used = out.engine.bytesWritten;
|
|
9696
|
+
} catch (err) {
|
|
9697
|
+
if (err.code === "ERR_BUFFER_TOO_LARGE") {
|
|
9698
|
+
throw new ArchiveError(`it unpacks to more than ${MAX_TAR_BYTES / 1024 / 1024} MB`, true);
|
|
9699
|
+
}
|
|
9700
|
+
throw new ArchiveError(`its gzip data is broken (${err instanceof Error ? err.message : String(err)})`);
|
|
9701
|
+
}
|
|
9702
|
+
const trailer = at + used;
|
|
9703
|
+
if (gz.length - trailer < 8) throw new ArchiveError("its gzip trailer is cut short");
|
|
9704
|
+
if (gz.length - trailer > 8) throw new ArchiveError("it has bytes after its gzip trailer");
|
|
9705
|
+
if (gz.readUInt32LE(trailer) !== crc32(tar)) throw new ArchiveError("its gzip CRC-32 does not match its contents");
|
|
9706
|
+
if (gz.readUInt32LE(trailer + 4) !== tar.length % 2 ** 32) throw new ArchiveError("its gzip length does not match its contents");
|
|
9707
|
+
return tar;
|
|
9708
|
+
}
|
|
9709
|
+
function field(header, start, length) {
|
|
9710
|
+
const raw = header.subarray(start, start + length);
|
|
9711
|
+
const end = raw.indexOf(0);
|
|
9712
|
+
try {
|
|
9713
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(end < 0 ? raw : raw.subarray(0, end));
|
|
9714
|
+
} catch {
|
|
9715
|
+
throw new ArchiveError("it has a path that is not UTF-8");
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9718
|
+
function octal(header, start, length) {
|
|
9719
|
+
const text = header.toString("latin1", start, start + length).replace(/[\0 ]+$/, "").replace(/^ +/, "");
|
|
9720
|
+
return /^[0-7]{1,12}$/.test(text) ? parseInt(text, 8) : null;
|
|
9721
|
+
}
|
|
9722
|
+
function checkPath(path) {
|
|
9723
|
+
const shown = JSON.stringify(path);
|
|
9724
|
+
if (path === "") throw new ArchiveError("it has an entry with an empty path");
|
|
9725
|
+
if (path.startsWith("/")) throw new ArchiveError(`it has an absolute path, ${shown}`);
|
|
9726
|
+
if (path.includes("\\")) throw new ArchiveError(`it has a path with a backslash, ${shown}`);
|
|
9727
|
+
if (CONTROL_RE2.test(path)) throw new ArchiveError(`it has a path with a control character, ${shown}`);
|
|
9728
|
+
for (const segment of path.split("/")) {
|
|
9729
|
+
if (segment === "") throw new ArchiveError(`it has a path with an empty segment, ${shown}`);
|
|
9730
|
+
if (segment === ".." || segment === ".") throw new ArchiveError(`it has a path with "${segment}" in it, ${shown}`);
|
|
9731
|
+
}
|
|
9732
|
+
}
|
|
9733
|
+
var TYPE_NAMES = {
|
|
9734
|
+
"1": "a hard link",
|
|
9735
|
+
"2": "a symbolic link",
|
|
9736
|
+
"3": "a character device",
|
|
9737
|
+
"4": "a block device",
|
|
9738
|
+
"6": "a fifo",
|
|
9739
|
+
"7": "a contiguous file",
|
|
9740
|
+
x: "a pax header",
|
|
9741
|
+
g: "a pax global header",
|
|
9742
|
+
L: "a GNU long name",
|
|
9743
|
+
K: "a GNU long link name"
|
|
9744
|
+
};
|
|
9745
|
+
function readTar(tar) {
|
|
9746
|
+
if (tar.length % BLOCK !== 0) throw new ArchiveError("its tar is not a whole number of 512-byte blocks");
|
|
9747
|
+
const files = [];
|
|
9748
|
+
const dirs = [];
|
|
9749
|
+
const seen = /* @__PURE__ */ new Map();
|
|
9750
|
+
let unpacked = 0;
|
|
9751
|
+
let at = 0;
|
|
9752
|
+
for (; ; ) {
|
|
9753
|
+
if (at >= tar.length) throw new ArchiveError("its tar has no end marker");
|
|
9754
|
+
const header = tar.subarray(at, at + BLOCK);
|
|
9755
|
+
if (header.every((b) => b === 0)) {
|
|
9756
|
+
if (!tar.subarray(at).every((b) => b === 0)) throw new ArchiveError("it has data after its tar end marker");
|
|
9757
|
+
break;
|
|
9758
|
+
}
|
|
9759
|
+
let sum = 0;
|
|
9760
|
+
for (let i = 0; i < BLOCK; i += 1) sum += i >= 148 && i < 156 ? 32 : header[i];
|
|
9761
|
+
if (octal(header, 148, 8) !== sum) throw new ArchiveError("it has a tar header with a wrong checksum");
|
|
9762
|
+
if (header.toString("latin1", 257, 263) !== "ustar\0" || header.toString("latin1", 263, 265) !== "00") {
|
|
9763
|
+
throw new ArchiveError("its tar is not ustar");
|
|
9764
|
+
}
|
|
9765
|
+
const name = field(header, 0, 100);
|
|
9766
|
+
const prefix = field(header, 345, 155);
|
|
9767
|
+
let path = prefix ? `${prefix}/${name}` : name;
|
|
9768
|
+
const type = String.fromCharCode(header[156]);
|
|
9769
|
+
const size = octal(header, 124, 12);
|
|
9770
|
+
const mode = octal(header, 100, 8);
|
|
9771
|
+
if (size === null || mode === null) throw new ArchiveError(`it has a tar header for ${JSON.stringify(path)} with a bad size or mode`);
|
|
9772
|
+
const isDir = type === "5";
|
|
9773
|
+
if (!isDir && type !== "0" && type !== "\0") {
|
|
9774
|
+
const what = TYPE_NAMES[type] ?? `an entry of type ${JSON.stringify(type)}`;
|
|
9775
|
+
throw new ArchiveError(`${JSON.stringify(path)} is ${what}; a skill archive holds only files and folders`);
|
|
9776
|
+
}
|
|
9777
|
+
if (isDir && path.endsWith("/")) path = path.slice(0, -1);
|
|
9778
|
+
checkPath(path);
|
|
9779
|
+
const key = path.toLowerCase();
|
|
9780
|
+
const parts = key.split("/");
|
|
9781
|
+
for (let i = 1; i < parts.length; i += 1) {
|
|
9782
|
+
const parent = parts.slice(0, i).join("/");
|
|
9783
|
+
const kind = seen.get(parent);
|
|
9784
|
+
if (kind === "file") throw new ArchiveError(`it puts ${JSON.stringify(path)} inside a file`);
|
|
9785
|
+
if (!kind) seen.set(parent, "implied");
|
|
9786
|
+
}
|
|
9787
|
+
const before = seen.get(key);
|
|
9788
|
+
if (before === "file" || before === "dir" || before === "implied" && !isDir) {
|
|
9789
|
+
throw new ArchiveError(`it lists ${JSON.stringify(path)} twice`);
|
|
9790
|
+
}
|
|
9791
|
+
const start = at + BLOCK;
|
|
9792
|
+
if (isDir) {
|
|
9793
|
+
if (size !== 0) throw new ArchiveError(`its folder ${JSON.stringify(path)} has a size`);
|
|
9794
|
+
seen.set(key, "dir");
|
|
9795
|
+
dirs.push(path);
|
|
9796
|
+
} else {
|
|
9797
|
+
if (!FILE_MODES.has(mode & 4095)) {
|
|
9798
|
+
throw new ArchiveError(`${JSON.stringify(path)} has mode ${(mode & 4095).toString(8).padStart(4, "0")}; a file must be 0644 or 0755`);
|
|
9799
|
+
}
|
|
9800
|
+
if (files.length + 1 > MAX_ARCHIVE_FILES) throw new ArchiveError(`it has more than ${MAX_ARCHIVE_FILES} files`, true);
|
|
9801
|
+
unpacked += size;
|
|
9802
|
+
if (unpacked > MAX_UNPACKED_BYTES) throw new ArchiveError(`its files come to more than ${MAX_UNPACKED_BYTES / 1024 / 1024} MB`, true);
|
|
9803
|
+
if (start + size > tar.length) throw new ArchiveError(`its tar is cut short inside ${JSON.stringify(path)}`);
|
|
9804
|
+
seen.set(key, "file");
|
|
9805
|
+
files.push({ path, bytes: Buffer.from(tar.subarray(start, start + size)), mode: mode & 4095 });
|
|
9806
|
+
}
|
|
9807
|
+
at = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
9808
|
+
}
|
|
9809
|
+
const allDirs = /* @__PURE__ */ new Map();
|
|
9810
|
+
const addDirsOf = (path, self) => {
|
|
9811
|
+
const parts = path.split("/");
|
|
9812
|
+
for (let i = 1; i <= parts.length - (self ? 0 : 1); i += 1) {
|
|
9813
|
+
const dir = parts.slice(0, i).join("/");
|
|
9814
|
+
if (!allDirs.has(dir.toLowerCase())) allDirs.set(dir.toLowerCase(), dir);
|
|
9815
|
+
}
|
|
9816
|
+
};
|
|
9817
|
+
for (const dir of dirs) addDirsOf(dir, true);
|
|
9818
|
+
for (const f of files) addDirsOf(f.path, false);
|
|
9819
|
+
return { files, dirs: [...allDirs.values()].sort((a, b) => a.split("/").length - b.split("/").length) };
|
|
9820
|
+
}
|
|
9821
|
+
function readSkillArchive(gz) {
|
|
9822
|
+
return readTar(gunzipChecked(gz));
|
|
9823
|
+
}
|
|
9824
|
+
|
|
9506
9825
|
// src/commands/skills-warning.ts
|
|
9507
9826
|
var WARNING_FIELD = "x-amdahl-warning";
|
|
9508
9827
|
var MAX_WARNING_CHARS = 300;
|
|
@@ -9929,7 +10248,7 @@ async function workspaceUse(ctx, args) {
|
|
|
9929
10248
|
}
|
|
9930
10249
|
|
|
9931
10250
|
// src/version.ts
|
|
9932
|
-
var VERSION = "0.
|
|
10251
|
+
var VERSION = "0.3.1";
|
|
9933
10252
|
|
|
9934
10253
|
// src/main.ts
|
|
9935
10254
|
var COMMANDS = {
|
|
@@ -9964,6 +10283,7 @@ async function runCli(argv, io) {
|
|
|
9964
10283
|
return EXIT.ok;
|
|
9965
10284
|
}
|
|
9966
10285
|
if (!parsed.command) {
|
|
10286
|
+
if (argv.every((a) => a === "--verbose") && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
|
|
9967
10287
|
io.stderr(USAGE);
|
|
9968
10288
|
return EXIT.usage;
|
|
9969
10289
|
}
|