@amdahl/cli 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -2
- package/dist/amdahl.js +569 -367
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,10 +56,48 @@ amdahl login
|
|
|
56
56
|
`npx @amdahl/cli ...`. That package ([packages/amdahl](packages/amdahl)) has no
|
|
57
57
|
code of its own: it depends on `@amdahl/cli` at the same version and runs it.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
The quickest way to start is `npx amdahl` with nothing after it, in a
|
|
60
|
+
terminal. It walks you through setup:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Amdahl, predictive ML for GTM
|
|
64
|
+
Found on this machine: Claude Code ✓ Cursor ✓ Codex –
|
|
65
|
+
Set up Amdahl in:
|
|
66
|
+
> [x] Claude Code
|
|
67
|
+
[x] Cursor
|
|
68
|
+
[ ] Codex
|
|
69
|
+
(Use the arrow keys, Space to tick or untick, then Enter)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
It finds Claude Code, Cursor and Codex by their command on your PATH
|
|
73
|
+
(`claude`, `cursor`, `codex`) or their folder in your home folder (`.claude`,
|
|
74
|
+
`.cursor`, `.codex`), and ticks the ones it finds. Enter accepts. If you are
|
|
75
|
+
not signed in, it signs you in through the browser first; otherwise it prints
|
|
76
|
+
`Signed in as <email> (workspace: <name>)`. If the server rejects a key set in
|
|
77
|
+
`AMDAHL_KEY` (or `AMDAHL_API_KEY`, `AMDAHL_ACCESS_TOKEN`, `--api-key`), it says
|
|
78
|
+
so and skips the browser sign-in, since that key would still win. Then it
|
|
79
|
+
runs `amdahl install` for each client you ticked. A client that fails prints
|
|
80
|
+
its error and the rest still run; the exit code is the first failure's. It
|
|
81
|
+
ends with something to try, such as
|
|
82
|
+
`Try it: open Claude Code and say "make this email sound less like AI"`
|
|
83
|
+
(for Codex, it first says to run `codex mcp login amdahl`).
|
|
84
|
+
Run it again once you are set up and it says `You're set up in Claude Code
|
|
85
|
+
and Cursor.` with the same line to try, and asks nothing; if it cannot reach
|
|
86
|
+
Amdahl to check your sign-in, it still says so. Ctrl+C at the question stops
|
|
87
|
+
it with nothing written (exit 130).
|
|
88
|
+
|
|
89
|
+
It only does this for a person at a terminal. When stdin, stdout or stderr
|
|
90
|
+
(where the menu draws) is not a terminal, `CI` is set, an agent runs it (`CLAUDECODE` or `CURSOR_AGENT` is
|
|
91
|
+
set), `AMDAHL_NO_PROMPT=1` is set, or any flag is given (such as `--json`),
|
|
92
|
+
bare `amdahl` prints the usage to stderr and exits 2, as before.
|
|
93
|
+
|
|
94
|
+
To connect one AI client yourself, run `amdahl install claude-code` (or `codex`,
|
|
60
95
|
`cursor`). It adds the Amdahl MCP server and also installs the Amdahl skill,
|
|
61
96
|
a map of everything Amdahl can do, so your agent knows which tool fits each
|
|
62
|
-
job.
|
|
97
|
+
job. If the client already points at Amdahl, `install` leaves that as it is
|
|
98
|
+
(for Claude Code it skips `claude mcp add` when `claude mcp get amdahl`
|
|
99
|
+
shows the same URL) and still installs the skill. If it cannot write the
|
|
100
|
+
client's config file, it exits 1 with `write_failed` and names the file. The skill goes to the client's user skills folder:
|
|
63
101
|
|
|
64
102
|
| client | skill file |
|
|
65
103
|
|---|---|
|
|
@@ -77,6 +115,7 @@ https://docs.amdahl.ai/skills/amdahl/SKILL.md.
|
|
|
77
115
|
## Quick start
|
|
78
116
|
|
|
79
117
|
```sh
|
|
118
|
+
amdahl # in a terminal: guided setup for Claude Code, Cursor and Codex
|
|
80
119
|
amdahl login # sign in through the browser with your console account
|
|
81
120
|
amdahl whoami # the account, workspace and role you are signed in as
|
|
82
121
|
amdahl status # can this workspace optimize, and what blocks it
|
|
@@ -93,6 +132,7 @@ On a server or in CI, skip `login` and set an API key in `AMDAHL_KEY`.
|
|
|
93
132
|
## Commands
|
|
94
133
|
|
|
95
134
|
```
|
|
135
|
+
amdahl (guided setup, in a terminal)
|
|
96
136
|
amdahl login [--workspace <slug>] [--no-browser] [--port <n>] (alias: auth login)
|
|
97
137
|
amdahl logout [--all] (alias: auth logout)
|
|
98
138
|
amdahl whoami [--show-token] (alias: auth status)
|
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,6 +7559,10 @@ 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) => {
|
|
@@ -7704,6 +7751,8 @@ function strictParse(argv, options) {
|
|
|
7704
7751
|
var USAGE = `Usage: amdahl <command> [options]
|
|
7705
7752
|
|
|
7706
7753
|
What you can do:
|
|
7754
|
+
amdahl Set up Claude Code, Cursor or Codex, in a terminal: it asks
|
|
7755
|
+
which, signs you in, and runs install for each one
|
|
7707
7756
|
amdahl optimize draft.md Rewrite an outbound email or LinkedIn message
|
|
7708
7757
|
amdahl status Check that this workspace is set up to optimize
|
|
7709
7758
|
amdahl keys create --name ci Make an API key for a server or CI
|
|
@@ -8623,6 +8672,12 @@ function skillPath(client, home) {
|
|
|
8623
8672
|
|
|
8624
8673
|
// src/commands/install.ts
|
|
8625
8674
|
var CLIENTS = ["claude-code", "codex", "cursor"];
|
|
8675
|
+
function codexConfigPath(home) {
|
|
8676
|
+
return join4(home, ".codex", "config.toml");
|
|
8677
|
+
}
|
|
8678
|
+
function cursorConfigPath(home) {
|
|
8679
|
+
return join4(home, ".cursor", "mcp.json");
|
|
8680
|
+
}
|
|
8626
8681
|
function codexBlock(mcpUrl) {
|
|
8627
8682
|
return `[mcp_servers.amdahl]
|
|
8628
8683
|
url = "${mcpUrl}"
|
|
@@ -8637,6 +8692,50 @@ async function onPath(ctx, command) {
|
|
|
8637
8692
|
const res = await ctx.io.run(probe, [command]);
|
|
8638
8693
|
return res.code === 0;
|
|
8639
8694
|
}
|
|
8695
|
+
var CLIENT_SIGNS = {
|
|
8696
|
+
"claude-code": { command: "claude", dir: ".claude" },
|
|
8697
|
+
codex: { command: "codex", dir: ".codex" },
|
|
8698
|
+
cursor: { command: "cursor", dir: ".cursor" }
|
|
8699
|
+
};
|
|
8700
|
+
async function detectClients(ctx) {
|
|
8701
|
+
const found = await Promise.all(
|
|
8702
|
+
CLIENTS.map(async (client) => {
|
|
8703
|
+
const { command, dir } = CLIENT_SIGNS[client];
|
|
8704
|
+
return existsSync3(join4(ctx.io.homedir, dir)) || await onPath(ctx, command);
|
|
8705
|
+
})
|
|
8706
|
+
);
|
|
8707
|
+
return Object.fromEntries(CLIENTS.map((client, i) => [client, found[i]]));
|
|
8708
|
+
}
|
|
8709
|
+
async function claudeHasAmdahl(ctx, mcpUrl) {
|
|
8710
|
+
const res = await ctx.io.run("claude", ["mcp", "get", "amdahl"]);
|
|
8711
|
+
return res.code === 0 && res.stdout.split("\n").some((line) => line.trim() === `URL: ${mcpUrl}`);
|
|
8712
|
+
}
|
|
8713
|
+
function updateConfig(path, fn) {
|
|
8714
|
+
try {
|
|
8715
|
+
return fn();
|
|
8716
|
+
} catch (err) {
|
|
8717
|
+
if (err instanceof CliError) throw err;
|
|
8718
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
8719
|
+
throw new CliError("write_failed", `Could not update ${path} (${reason}).`, EXIT.general);
|
|
8720
|
+
}
|
|
8721
|
+
}
|
|
8722
|
+
async function isInstalled(ctx, client) {
|
|
8723
|
+
const skill = skillPath(client, ctx.io.homedir);
|
|
8724
|
+
const current = existsSync3(skill) ? readFileSync3(skill, "utf8") : null;
|
|
8725
|
+
if (current === null || current !== SKILL_TEXT && isShippedSkill(current)) return false;
|
|
8726
|
+
const mcpUrl = `${ctx.apiUrl}/mcp`;
|
|
8727
|
+
if (client === "claude-code") return await onPath(ctx, "claude") && await claudeHasAmdahl(ctx, mcpUrl);
|
|
8728
|
+
if (client === "codex") {
|
|
8729
|
+
const path = codexConfigPath(ctx.io.homedir);
|
|
8730
|
+
return existsSync3(path) && codexHasAmdahl(readFileSync3(path, "utf8"));
|
|
8731
|
+
}
|
|
8732
|
+
try {
|
|
8733
|
+
const config = JSON.parse(readFileSync3(cursorConfigPath(ctx.io.homedir), "utf8"));
|
|
8734
|
+
return JSON.stringify(config.mcpServers?.amdahl) === JSON.stringify({ url: mcpUrl });
|
|
8735
|
+
} catch {
|
|
8736
|
+
return false;
|
|
8737
|
+
}
|
|
8738
|
+
}
|
|
8640
8739
|
async function install(ctx, args) {
|
|
8641
8740
|
const client = args[0];
|
|
8642
8741
|
if (!client || args.length > 1 || !CLIENTS.includes(client)) {
|
|
@@ -8685,6 +8784,9 @@ async function installMcp(ctx, client, print) {
|
|
|
8685
8784
|
const notes = print ? [] : ["`claude` is not on your PATH. Run the command above once it is installed."];
|
|
8686
8785
|
return { data: { ok: true, client, action: "printed", command }, human: command, notes };
|
|
8687
8786
|
}
|
|
8787
|
+
if (await claudeHasAmdahl(ctx, mcpUrl)) {
|
|
8788
|
+
return { data: { ok: true, client, action: "unchanged", command }, human: `Claude Code already has Amdahl at ${mcpUrl}; nothing changed.`, notes: [] };
|
|
8789
|
+
}
|
|
8688
8790
|
const res = await io.run("claude", args);
|
|
8689
8791
|
if (res.code !== 0) {
|
|
8690
8792
|
throw new CliError("install_failed", `\`${command}\` failed: ${res.stderr.trim() || `exit ${res.code}`}`, EXIT.general);
|
|
@@ -8692,47 +8794,51 @@ async function installMcp(ctx, client, print) {
|
|
|
8692
8794
|
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
8795
|
}
|
|
8694
8796
|
if (client === "codex") {
|
|
8695
|
-
const path2 =
|
|
8797
|
+
const path2 = codexConfigPath(io.homedir);
|
|
8696
8798
|
const block = codexBlock(mcpUrl);
|
|
8697
8799
|
if (print) {
|
|
8698
8800
|
return { data: { ok: true, client, action: "printed", path: path2 }, human: `# ${path2}
|
|
8699
8801
|
${block}`, notes: [] };
|
|
8700
8802
|
}
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8803
|
+
return updateConfig(path2, () => {
|
|
8804
|
+
const current = existsSync3(path2) ? readFileSync3(path2, "utf8") : "";
|
|
8805
|
+
if (codexHasAmdahl(current)) {
|
|
8806
|
+
return { data: { ok: true, client, action: "unchanged", path: path2 }, human: `${path2} already has [mcp_servers.amdahl]; nothing changed.`, notes: [] };
|
|
8807
|
+
}
|
|
8808
|
+
mkdirSync2(dirname(path2), { recursive: true });
|
|
8809
|
+
const sep2 = current.length === 0 ? "" : current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n";
|
|
8810
|
+
writeFileSync2(path2, `${current}${sep2}${block}`);
|
|
8811
|
+
return { data: { ok: true, client, action: "wrote", path: path2 }, human: `Added Amdahl to ${path2}. Run \`codex mcp login amdahl\` to sign in.`, notes: [] };
|
|
8812
|
+
});
|
|
8709
8813
|
}
|
|
8710
|
-
const path =
|
|
8814
|
+
const path = cursorConfigPath(io.homedir);
|
|
8711
8815
|
const entry = { url: mcpUrl };
|
|
8712
8816
|
if (print) {
|
|
8713
8817
|
return { data: { ok: true, client, action: "printed", path }, human: `# ${path}
|
|
8714
8818
|
${JSON.stringify({ mcpServers: { amdahl: entry } }, null, 2)}`, notes: [] };
|
|
8715
8819
|
}
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8820
|
+
return updateConfig(path, () => {
|
|
8821
|
+
let config = {};
|
|
8822
|
+
if (existsSync3(path)) {
|
|
8823
|
+
const raw = readFileSync3(path, "utf8");
|
|
8824
|
+
if (raw.trim()) {
|
|
8825
|
+
try {
|
|
8826
|
+
config = JSON.parse(raw);
|
|
8827
|
+
} catch {
|
|
8828
|
+
throw new CliError("invalid_config", `${path} is not valid JSON; not changing it.`, EXIT.general);
|
|
8829
|
+
}
|
|
8724
8830
|
}
|
|
8725
8831
|
}
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
writeFileSync2(path, `${JSON.stringify(config, null, 2)}
|
|
8832
|
+
const servers = config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {};
|
|
8833
|
+
if (JSON.stringify(servers.amdahl) === JSON.stringify(entry)) {
|
|
8834
|
+
return { data: { ok: true, client, action: "unchanged", path }, human: `${path} already points at ${mcpUrl}; nothing changed.`, notes: [] };
|
|
8835
|
+
}
|
|
8836
|
+
config.mcpServers = { ...servers, amdahl: entry };
|
|
8837
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
8838
|
+
writeFileSync2(path, `${JSON.stringify(config, null, 2)}
|
|
8734
8839
|
`);
|
|
8735
|
-
|
|
8840
|
+
return { data: { ok: true, client, action: "wrote", path }, human: `Added Amdahl to ${path}. Cursor asks you to sign in on first use.`, notes: [] };
|
|
8841
|
+
});
|
|
8736
8842
|
}
|
|
8737
8843
|
|
|
8738
8844
|
// src/keygen.ts
|
|
@@ -9008,337 +9114,120 @@ async function optimize(ctx, args) {
|
|
|
9008
9114
|
return exit;
|
|
9009
9115
|
}
|
|
9010
9116
|
|
|
9011
|
-
// src/commands/skills.ts
|
|
9012
|
-
|
|
9013
|
-
import {
|
|
9117
|
+
// src/commands/skills-guide.ts
|
|
9118
|
+
import { existsSync as existsSync4 } from "fs";
|
|
9119
|
+
import { dirname as dirname3, join as join6, relative, sep } from "path";
|
|
9014
9120
|
|
|
9015
|
-
// src/
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9121
|
+
// src/commands/skills-install.ts
|
|
9122
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
9123
|
+
import { chmodSync as chmodSync2, lstatSync, mkdirSync as mkdirSync3, mkdtempSync, readdirSync, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
9124
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
9125
|
+
function skillDirs(targets, name) {
|
|
9126
|
+
const dirs = [];
|
|
9127
|
+
if (targets.claude) dirs.push(join5(targets.base, ".claude", "skills", name));
|
|
9128
|
+
if (targets.agents) dirs.push(join5(targets.base, ".agents", "skills", name));
|
|
9129
|
+
return dirs;
|
|
9019
9130
|
}
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9131
|
+
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
9132
|
+
function plainReason(err) {
|
|
9133
|
+
const code = err.code;
|
|
9134
|
+
if (code === "EACCES" || code === "EPERM") return "you do not have permission to write there";
|
|
9135
|
+
if (code === "ENOSPC") return "the disk is full";
|
|
9136
|
+
if (code === "EROFS") return "that disk is read-only";
|
|
9137
|
+
if (code === "ENOTDIR" || code === "EEXIST") return "a file is in the way";
|
|
9138
|
+
return message(err);
|
|
9139
|
+
}
|
|
9140
|
+
var leftBehind = (written) => written.length > 0 ? `It wrote ${written.join(" and ")} first.` : "Nothing was written.";
|
|
9141
|
+
function writeFailure(path, err, written = []) {
|
|
9142
|
+
const code = err.code;
|
|
9143
|
+
const left = leftBehind(written);
|
|
9144
|
+
const fix = code === "EACCES" || code === "EPERM" ? " Run the command from a folder you can write to." : "";
|
|
9145
|
+
return new CliError("write_failed", `Could not write ${path}: ${plainReason(err)}`, EXIT.general, `${left}${fix}`);
|
|
9146
|
+
}
|
|
9147
|
+
function sameFolder(dir, archive) {
|
|
9148
|
+
let top;
|
|
9149
|
+
try {
|
|
9150
|
+
top = lstatSync(dir);
|
|
9151
|
+
} catch (err) {
|
|
9152
|
+
if (err.code === "ENOENT") return null;
|
|
9153
|
+
throw writeFailure(dir, err);
|
|
9154
|
+
}
|
|
9155
|
+
if (!top.isDirectory()) return false;
|
|
9156
|
+
const want = new Map(archive.files.map((f) => [f.path, f]));
|
|
9157
|
+
const wantDirs = new Set(archive.dirs);
|
|
9158
|
+
let found = 0;
|
|
9159
|
+
const walk = (rel) => {
|
|
9160
|
+
for (const entry of readdirSync(join5(dir, rel), { withFileTypes: true })) {
|
|
9161
|
+
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
9162
|
+
if (++found > want.size + wantDirs.size) return false;
|
|
9163
|
+
if (entry.isDirectory()) {
|
|
9164
|
+
if (!wantDirs.has(path) || !walk(path)) return false;
|
|
9165
|
+
} else if (entry.isFile()) {
|
|
9166
|
+
const file = want.get(path);
|
|
9167
|
+
if (!file) return false;
|
|
9168
|
+
const full = join5(dir, path);
|
|
9169
|
+
if (process.platform !== "win32" && (lstatSync(full).mode & 73 ? 493 : 420) !== file.mode) return false;
|
|
9170
|
+
if (!readFileSync5(full).equals(file.bytes)) return false;
|
|
9171
|
+
} else {
|
|
9172
|
+
return false;
|
|
9032
9173
|
}
|
|
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
9174
|
}
|
|
9175
|
+
return true;
|
|
9045
9176
|
};
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
var MAX_ARCHIVE_FILES = 200;
|
|
9051
|
-
var MAX_UNPACKED_BYTES = 4 * 1024 * 1024;
|
|
9052
|
-
var MAX_TAR_BYTES = MAX_UNPACKED_BYTES + 1024 * 1024;
|
|
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;
|
|
9177
|
+
try {
|
|
9178
|
+
return walk("") && found === want.size + wantDirs.size;
|
|
9179
|
+
} catch (err) {
|
|
9180
|
+
throw writeFailure(dir, err);
|
|
9062
9181
|
}
|
|
9063
|
-
}
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9182
|
+
}
|
|
9183
|
+
function refuseLinks(dir, file, written = []) {
|
|
9184
|
+
const path = file ?? dir;
|
|
9185
|
+
for (const part of [dirname2(dirname2(dir)), dirname2(dir), dir, ...file ? [file] : []]) {
|
|
9186
|
+
let stat;
|
|
9187
|
+
try {
|
|
9188
|
+
stat = lstatSync(part);
|
|
9189
|
+
} catch (err) {
|
|
9190
|
+
if (err.code === "ENOENT") return;
|
|
9191
|
+
throw writeFailure(path, err, written);
|
|
9192
|
+
}
|
|
9193
|
+
const why = stat.isSymbolicLink() ? "is a link" : part === file && !stat.isFile() ? "is not a file" : "";
|
|
9194
|
+
if (why) {
|
|
9195
|
+
throw new CliError(
|
|
9196
|
+
"link_in_the_way",
|
|
9197
|
+
`Could not write ${path}: ${part} ${why}.`,
|
|
9198
|
+
EXIT.general,
|
|
9199
|
+
`${leftBehind(written)} Remove it, or install somewhere else.`
|
|
9200
|
+
);
|
|
9072
9201
|
}
|
|
9073
9202
|
}
|
|
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;
|
|
9077
9203
|
}
|
|
9078
|
-
function
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
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;
|
|
9095
|
-
try {
|
|
9096
|
-
const out = inflateRawSync(gz.subarray(at), { info: true, maxOutputLength: MAX_TAR_BYTES });
|
|
9097
|
-
tar = out.buffer;
|
|
9098
|
-
used = out.engine.bytesWritten;
|
|
9099
|
-
} catch (err) {
|
|
9100
|
-
if (err.code === "ERR_BUFFER_TOO_LARGE") {
|
|
9101
|
-
throw new ArchiveError(`it unpacks to more than ${MAX_TAR_BYTES / 1024 / 1024} MB`, true);
|
|
9204
|
+
function planInstall(dirs, payload) {
|
|
9205
|
+
return dirs.map((dir) => {
|
|
9206
|
+
if (payload.type === "archive") {
|
|
9207
|
+
refuseLinks(dir);
|
|
9208
|
+
const same = sameFolder(dir, payload.archive);
|
|
9209
|
+
return { path: dir, action: same === null ? "wrote" : same ? "unchanged" : "replaced" };
|
|
9102
9210
|
}
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9211
|
+
const path = join5(dir, "SKILL.md");
|
|
9212
|
+
refuseLinks(dir, path);
|
|
9213
|
+
let current;
|
|
9214
|
+
try {
|
|
9215
|
+
current = lstatSync(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
|
|
9216
|
+
} catch (err) {
|
|
9217
|
+
throw writeFailure(path, err);
|
|
9218
|
+
}
|
|
9219
|
+
return { path, action: current === null ? "wrote" : current.equals(payload.body) ? "unchanged" : "replaced" };
|
|
9220
|
+
});
|
|
9111
9221
|
}
|
|
9112
|
-
function
|
|
9113
|
-
const
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
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);
|
|
9265
|
-
}
|
|
9266
|
-
if (!top.isDirectory()) return false;
|
|
9267
|
-
const want = new Map(archive.files.map((f) => [f.path, f]));
|
|
9268
|
-
const wantDirs = new Set(archive.dirs);
|
|
9269
|
-
let found = 0;
|
|
9270
|
-
const walk = (rel) => {
|
|
9271
|
-
for (const entry of readdirSync(join5(dir, rel), { withFileTypes: true })) {
|
|
9272
|
-
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
9273
|
-
if (++found > want.size + wantDirs.size) return false;
|
|
9274
|
-
if (entry.isDirectory()) {
|
|
9275
|
-
if (!wantDirs.has(path) || !walk(path)) return false;
|
|
9276
|
-
} else if (entry.isFile()) {
|
|
9277
|
-
const file = want.get(path);
|
|
9278
|
-
if (!file) return false;
|
|
9279
|
-
const full = join5(dir, path);
|
|
9280
|
-
if (process.platform !== "win32" && (lstatSync(full).mode & 73 ? 493 : 420) !== file.mode) return false;
|
|
9281
|
-
if (!readFileSync5(full).equals(file.bytes)) return false;
|
|
9282
|
-
} else {
|
|
9283
|
-
return false;
|
|
9284
|
-
}
|
|
9285
|
-
}
|
|
9286
|
-
return true;
|
|
9287
|
-
};
|
|
9288
|
-
try {
|
|
9289
|
-
return walk("") && found === want.size + wantDirs.size;
|
|
9290
|
-
} catch (err) {
|
|
9291
|
-
throw writeFailure(dir, err);
|
|
9292
|
-
}
|
|
9293
|
-
}
|
|
9294
|
-
function refuseLinks(dir, file, written = []) {
|
|
9295
|
-
const path = file ?? dir;
|
|
9296
|
-
for (const part of [dirname2(dirname2(dir)), dirname2(dir), dir, ...file ? [file] : []]) {
|
|
9297
|
-
let stat;
|
|
9298
|
-
try {
|
|
9299
|
-
stat = lstatSync(part);
|
|
9300
|
-
} catch (err) {
|
|
9301
|
-
if (err.code === "ENOENT") return;
|
|
9302
|
-
throw writeFailure(path, err, written);
|
|
9303
|
-
}
|
|
9304
|
-
const why = stat.isSymbolicLink() ? "is a link" : part === file && !stat.isFile() ? "is not a file" : "";
|
|
9305
|
-
if (why) {
|
|
9306
|
-
throw new CliError(
|
|
9307
|
-
"link_in_the_way",
|
|
9308
|
-
`Could not write ${path}: ${part} ${why}.`,
|
|
9309
|
-
EXIT.general,
|
|
9310
|
-
`${leftBehind(written)} Remove it, or install somewhere else.`
|
|
9311
|
-
);
|
|
9312
|
-
}
|
|
9313
|
-
}
|
|
9314
|
-
}
|
|
9315
|
-
function planInstall(dirs, payload) {
|
|
9316
|
-
return dirs.map((dir) => {
|
|
9317
|
-
if (payload.type === "archive") {
|
|
9318
|
-
refuseLinks(dir);
|
|
9319
|
-
const same = sameFolder(dir, payload.archive);
|
|
9320
|
-
return { path: dir, action: same === null ? "wrote" : same ? "unchanged" : "replaced" };
|
|
9321
|
-
}
|
|
9322
|
-
const path = join5(dir, "SKILL.md");
|
|
9323
|
-
refuseLinks(dir, path);
|
|
9324
|
-
let current;
|
|
9325
|
-
try {
|
|
9326
|
-
current = lstatSync(path, { throwIfNoEntry: false }) ? readFileSync5(path) : null;
|
|
9327
|
-
} catch (err) {
|
|
9328
|
-
throw writeFailure(path, err);
|
|
9329
|
-
}
|
|
9330
|
-
return { path, action: current === null ? "wrote" : current.equals(payload.body) ? "unchanged" : "replaced" };
|
|
9331
|
-
});
|
|
9332
|
-
}
|
|
9333
|
-
function refuseClashes(plan, name, force) {
|
|
9334
|
-
const clashes = plan.filter((p) => p.action === "replaced").map((p) => p.path);
|
|
9335
|
-
if (clashes.length > 0 && !force) {
|
|
9336
|
-
throw new CliError(
|
|
9337
|
-
"skill_exists",
|
|
9338
|
-
`A different ${name} skill is already at ${clashes.join(" and ")}.`,
|
|
9339
|
-
EXIT.general,
|
|
9340
|
-
"Nothing was written. Run again with --force to replace it."
|
|
9341
|
-
);
|
|
9222
|
+
function refuseClashes(plan, name, force) {
|
|
9223
|
+
const clashes = plan.filter((p) => p.action === "replaced").map((p) => p.path);
|
|
9224
|
+
if (clashes.length > 0 && !force) {
|
|
9225
|
+
throw new CliError(
|
|
9226
|
+
"skill_exists",
|
|
9227
|
+
`A different ${name} skill is already at ${clashes.join(" and ")}.`,
|
|
9228
|
+
EXIT.general,
|
|
9229
|
+
"Nothing was written. Run again with --force to replace it."
|
|
9230
|
+
);
|
|
9342
9231
|
}
|
|
9343
9232
|
}
|
|
9344
9233
|
function stage(target, name, archive, staged) {
|
|
@@ -9503,6 +9392,318 @@ function nextStep(ctx, targets, seen) {
|
|
|
9503
9392
|
return `Open a new ${joinNames(names, "or")} chat ${where} to use it.`;
|
|
9504
9393
|
}
|
|
9505
9394
|
|
|
9395
|
+
// src/commands/setup.ts
|
|
9396
|
+
var SHOWN = [
|
|
9397
|
+
{ client: "claude-code", name: "Claude Code" },
|
|
9398
|
+
{ client: "cursor", name: "Cursor" },
|
|
9399
|
+
{ client: "codex", name: "Codex" }
|
|
9400
|
+
];
|
|
9401
|
+
function tryItLine(name) {
|
|
9402
|
+
const ask2 = 'say "make this email sound less like AI"';
|
|
9403
|
+
return name === "Codex" ? `Try it: run \`codex mcp login amdahl\`, then open Codex and ${ask2}` : `Try it: open ${name} and ${ask2}`;
|
|
9404
|
+
}
|
|
9405
|
+
function shouldSetUp(io) {
|
|
9406
|
+
const { env } = io;
|
|
9407
|
+
const ci = Boolean(env.CI) && env.CI !== "false" && env.CI !== "0";
|
|
9408
|
+
return io.stdinIsTTY && io.stdoutIsTTY && io.stderrIsTTY && env.AMDAHL_NO_PROMPT !== "1" && !ci && !env.CLAUDECODE && !env.CURSOR_AGENT;
|
|
9409
|
+
}
|
|
9410
|
+
async function signIn(ctx) {
|
|
9411
|
+
let credential;
|
|
9412
|
+
try {
|
|
9413
|
+
credential = await resolveCredential(ctx);
|
|
9414
|
+
} catch (err) {
|
|
9415
|
+
if (err instanceof CliError && err.exit === EXIT.unauthenticated) return { kind: "out" };
|
|
9416
|
+
throw err;
|
|
9417
|
+
}
|
|
9418
|
+
try {
|
|
9419
|
+
const me = await call(() => apiClient(ctx, credential).cli.whoami());
|
|
9420
|
+
return { kind: "in", email: me.email ?? me.user_id, workspace: me.workspace?.name ?? me.business_id };
|
|
9421
|
+
} catch (err) {
|
|
9422
|
+
if (!(err instanceof CliError && err.exit === EXIT.unauthenticated)) throw err;
|
|
9423
|
+
return credential.source === "profile" ? { kind: "out" } : { kind: "rejected", source: credential.source };
|
|
9424
|
+
}
|
|
9425
|
+
}
|
|
9426
|
+
async function setup(ctx) {
|
|
9427
|
+
const { io, out } = ctx;
|
|
9428
|
+
const say = (line) => io.stdout(`${line}
|
|
9429
|
+
`);
|
|
9430
|
+
say(`${out.bold("Amdahl")}, predictive ML for GTM`);
|
|
9431
|
+
const found = await detectClients(ctx);
|
|
9432
|
+
const detected = SHOWN.filter((c) => found[c.client]);
|
|
9433
|
+
let who;
|
|
9434
|
+
if (detected.length > 0) {
|
|
9435
|
+
const installed = await Promise.all(detected.map((c) => isInstalled(ctx, c.client)));
|
|
9436
|
+
if (installed.every(Boolean)) {
|
|
9437
|
+
let unreachable = false;
|
|
9438
|
+
try {
|
|
9439
|
+
who = await signIn(ctx);
|
|
9440
|
+
} catch (err) {
|
|
9441
|
+
if (!(err instanceof CliError && err.exit === EXIT.network)) throw err;
|
|
9442
|
+
unreachable = true;
|
|
9443
|
+
}
|
|
9444
|
+
if (unreachable || who?.kind === "in") {
|
|
9445
|
+
say(`You're set up in ${joinNames(detected.map((c) => c.name))}.${unreachable ? " (Could not reach Amdahl to check your sign-in.)" : ""}`);
|
|
9446
|
+
say(tryItLine(detected[0]?.name ?? ""));
|
|
9447
|
+
return EXIT.ok;
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9450
|
+
}
|
|
9451
|
+
say(`Found on this machine: ${SHOWN.map((c) => `${c.name} ${found[c.client] ? "\u2713" : "\u2013"}`).join(" ")}`);
|
|
9452
|
+
if (detected.length === 0) {
|
|
9453
|
+
say("None of them found here. Tick the ones you use with Space, then press Enter.");
|
|
9454
|
+
}
|
|
9455
|
+
const picks = await io.multiSelect(
|
|
9456
|
+
"Set up Amdahl in:",
|
|
9457
|
+
SHOWN.map((c) => c.name),
|
|
9458
|
+
{ ticked: SHOWN.map((c) => found[c.client]), color: out.color }
|
|
9459
|
+
);
|
|
9460
|
+
if (picks.length === 0) {
|
|
9461
|
+
say("Nothing picked, so nothing changed. To set one up later: amdahl install claude-code|codex|cursor");
|
|
9462
|
+
return EXIT.ok;
|
|
9463
|
+
}
|
|
9464
|
+
who ??= await signIn(ctx);
|
|
9465
|
+
if (who.kind === "in") say(`Signed in as ${who.email} (workspace: ${who.workspace})`);
|
|
9466
|
+
else if (who.kind === "out") await login(ctx);
|
|
9467
|
+
else {
|
|
9468
|
+
const name = who.source === "flag" ? "--api-key" : who.source.slice("env:".length);
|
|
9469
|
+
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\`.`);
|
|
9470
|
+
}
|
|
9471
|
+
let exit = EXIT.ok;
|
|
9472
|
+
let ready = null;
|
|
9473
|
+
for (const { client, name } of picks.flatMap((i) => SHOWN[i] ?? [])) {
|
|
9474
|
+
try {
|
|
9475
|
+
await install(ctx, [client]);
|
|
9476
|
+
} catch (raw) {
|
|
9477
|
+
const err = raw instanceof CliError ? raw : new CliError("install_failed", `Could not set up ${name}: ${raw instanceof Error ? raw.message : String(raw)}`, EXIT.general);
|
|
9478
|
+
io.stderr(`Error: ${err.message}
|
|
9479
|
+
${err.hint ? `${err.hint}
|
|
9480
|
+
` : ""}`);
|
|
9481
|
+
if (exit === EXIT.ok) exit = err.exit;
|
|
9482
|
+
continue;
|
|
9483
|
+
}
|
|
9484
|
+
if (ready === null && (client !== "claude-code" || await onPath(ctx, "claude"))) ready = name;
|
|
9485
|
+
}
|
|
9486
|
+
if (ready) say(tryItLine(ready));
|
|
9487
|
+
return exit;
|
|
9488
|
+
}
|
|
9489
|
+
|
|
9490
|
+
// src/commands/skills.ts
|
|
9491
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
9492
|
+
import { createHash as createHash4 } from "crypto";
|
|
9493
|
+
|
|
9494
|
+
// src/output.ts
|
|
9495
|
+
var CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
9496
|
+
function printable(text) {
|
|
9497
|
+
return text.replace(CONTROL_RE, "");
|
|
9498
|
+
}
|
|
9499
|
+
function createOutput(io, json, noColor) {
|
|
9500
|
+
const color = !noColor && !io.env.NO_COLOR && io.env.TERM !== "dumb" && io.stderrIsTTY && !json;
|
|
9501
|
+
return {
|
|
9502
|
+
json,
|
|
9503
|
+
color,
|
|
9504
|
+
result(data, human) {
|
|
9505
|
+
if (json) {
|
|
9506
|
+
io.stdout(`${JSON.stringify(data)}
|
|
9507
|
+
`);
|
|
9508
|
+
} else if (human !== void 0) {
|
|
9509
|
+
io.stdout(human.endsWith("\n") ? human : `${human}
|
|
9510
|
+
`);
|
|
9511
|
+
}
|
|
9512
|
+
},
|
|
9513
|
+
info(text) {
|
|
9514
|
+
io.stderr(text.endsWith("\n") ? text : `${text}
|
|
9515
|
+
`);
|
|
9516
|
+
},
|
|
9517
|
+
event(data) {
|
|
9518
|
+
if (json) io.stderr(`${JSON.stringify(data)}
|
|
9519
|
+
`);
|
|
9520
|
+
},
|
|
9521
|
+
bold(text) {
|
|
9522
|
+
return color ? `\x1B[1m${text}\x1B[22m` : text;
|
|
9523
|
+
}
|
|
9524
|
+
};
|
|
9525
|
+
}
|
|
9526
|
+
|
|
9527
|
+
// src/tar-gz.ts
|
|
9528
|
+
import { inflateRawSync } from "zlib";
|
|
9529
|
+
var MAX_ARCHIVE_FILES = 200;
|
|
9530
|
+
var MAX_UNPACKED_BYTES = 4 * 1024 * 1024;
|
|
9531
|
+
var MAX_TAR_BYTES = MAX_UNPACKED_BYTES + 1024 * 1024;
|
|
9532
|
+
var BLOCK = 512;
|
|
9533
|
+
var FILE_MODES = /* @__PURE__ */ new Set([420, 493]);
|
|
9534
|
+
var CONTROL_RE2 = /[\u0000-\u001f\u007f-\u009f]/;
|
|
9535
|
+
var ArchiveError = class extends Error {
|
|
9536
|
+
tooLarge;
|
|
9537
|
+
constructor(message2, tooLarge = false) {
|
|
9538
|
+
super(message2);
|
|
9539
|
+
this.name = "ArchiveError";
|
|
9540
|
+
this.tooLarge = tooLarge;
|
|
9541
|
+
}
|
|
9542
|
+
};
|
|
9543
|
+
var crcTable = null;
|
|
9544
|
+
function crc32(data) {
|
|
9545
|
+
if (!crcTable) {
|
|
9546
|
+
crcTable = new Uint32Array(256);
|
|
9547
|
+
for (let n = 0; n < 256; n += 1) {
|
|
9548
|
+
let c = n;
|
|
9549
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
9550
|
+
crcTable[n] = c >>> 0;
|
|
9551
|
+
}
|
|
9552
|
+
}
|
|
9553
|
+
let crc = 4294967295;
|
|
9554
|
+
for (let i = 0; i < data.length; i += 1) crc = crcTable[(crc ^ data[i]) & 255] ^ crc >>> 8;
|
|
9555
|
+
return (crc ^ 4294967295) >>> 0;
|
|
9556
|
+
}
|
|
9557
|
+
function gunzipChecked(gz) {
|
|
9558
|
+
if (gz.length < 18 || gz[0] !== 31 || gz[1] !== 139) throw new ArchiveError("it is not gzip");
|
|
9559
|
+
if (gz[2] !== 8) throw new ArchiveError("its gzip method is not deflate");
|
|
9560
|
+
const flags = gz[3];
|
|
9561
|
+
if (flags & 224) throw new ArchiveError("its gzip header sets reserved flags");
|
|
9562
|
+
let at = 10;
|
|
9563
|
+
if (flags & 4) at += 2 + gz.readUInt16LE(Math.min(at, gz.length - 2));
|
|
9564
|
+
for (const flag of [8, 16]) {
|
|
9565
|
+
if (!(flags & flag)) continue;
|
|
9566
|
+
const end = gz.indexOf(0, at);
|
|
9567
|
+
if (end < 0) throw new ArchiveError("its gzip header is cut short");
|
|
9568
|
+
at = end + 1;
|
|
9569
|
+
}
|
|
9570
|
+
if (flags & 2) at += 2;
|
|
9571
|
+
if (at > gz.length - 8) throw new ArchiveError("its gzip header is cut short");
|
|
9572
|
+
let tar;
|
|
9573
|
+
let used;
|
|
9574
|
+
try {
|
|
9575
|
+
const out = inflateRawSync(gz.subarray(at), { info: true, maxOutputLength: MAX_TAR_BYTES });
|
|
9576
|
+
tar = out.buffer;
|
|
9577
|
+
used = out.engine.bytesWritten;
|
|
9578
|
+
} catch (err) {
|
|
9579
|
+
if (err.code === "ERR_BUFFER_TOO_LARGE") {
|
|
9580
|
+
throw new ArchiveError(`it unpacks to more than ${MAX_TAR_BYTES / 1024 / 1024} MB`, true);
|
|
9581
|
+
}
|
|
9582
|
+
throw new ArchiveError(`its gzip data is broken (${err instanceof Error ? err.message : String(err)})`);
|
|
9583
|
+
}
|
|
9584
|
+
const trailer = at + used;
|
|
9585
|
+
if (gz.length - trailer < 8) throw new ArchiveError("its gzip trailer is cut short");
|
|
9586
|
+
if (gz.length - trailer > 8) throw new ArchiveError("it has bytes after its gzip trailer");
|
|
9587
|
+
if (gz.readUInt32LE(trailer) !== crc32(tar)) throw new ArchiveError("its gzip CRC-32 does not match its contents");
|
|
9588
|
+
if (gz.readUInt32LE(trailer + 4) !== tar.length % 2 ** 32) throw new ArchiveError("its gzip length does not match its contents");
|
|
9589
|
+
return tar;
|
|
9590
|
+
}
|
|
9591
|
+
function field(header, start, length) {
|
|
9592
|
+
const raw = header.subarray(start, start + length);
|
|
9593
|
+
const end = raw.indexOf(0);
|
|
9594
|
+
try {
|
|
9595
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(end < 0 ? raw : raw.subarray(0, end));
|
|
9596
|
+
} catch {
|
|
9597
|
+
throw new ArchiveError("it has a path that is not UTF-8");
|
|
9598
|
+
}
|
|
9599
|
+
}
|
|
9600
|
+
function octal(header, start, length) {
|
|
9601
|
+
const text = header.toString("latin1", start, start + length).replace(/[\0 ]+$/, "").replace(/^ +/, "");
|
|
9602
|
+
return /^[0-7]{1,12}$/.test(text) ? parseInt(text, 8) : null;
|
|
9603
|
+
}
|
|
9604
|
+
function checkPath(path) {
|
|
9605
|
+
const shown = JSON.stringify(path);
|
|
9606
|
+
if (path === "") throw new ArchiveError("it has an entry with an empty path");
|
|
9607
|
+
if (path.startsWith("/")) throw new ArchiveError(`it has an absolute path, ${shown}`);
|
|
9608
|
+
if (path.includes("\\")) throw new ArchiveError(`it has a path with a backslash, ${shown}`);
|
|
9609
|
+
if (CONTROL_RE2.test(path)) throw new ArchiveError(`it has a path with a control character, ${shown}`);
|
|
9610
|
+
for (const segment of path.split("/")) {
|
|
9611
|
+
if (segment === "") throw new ArchiveError(`it has a path with an empty segment, ${shown}`);
|
|
9612
|
+
if (segment === ".." || segment === ".") throw new ArchiveError(`it has a path with "${segment}" in it, ${shown}`);
|
|
9613
|
+
}
|
|
9614
|
+
}
|
|
9615
|
+
var TYPE_NAMES = {
|
|
9616
|
+
"1": "a hard link",
|
|
9617
|
+
"2": "a symbolic link",
|
|
9618
|
+
"3": "a character device",
|
|
9619
|
+
"4": "a block device",
|
|
9620
|
+
"6": "a fifo",
|
|
9621
|
+
"7": "a contiguous file",
|
|
9622
|
+
x: "a pax header",
|
|
9623
|
+
g: "a pax global header",
|
|
9624
|
+
L: "a GNU long name",
|
|
9625
|
+
K: "a GNU long link name"
|
|
9626
|
+
};
|
|
9627
|
+
function readTar(tar) {
|
|
9628
|
+
if (tar.length % BLOCK !== 0) throw new ArchiveError("its tar is not a whole number of 512-byte blocks");
|
|
9629
|
+
const files = [];
|
|
9630
|
+
const dirs = [];
|
|
9631
|
+
const seen = /* @__PURE__ */ new Map();
|
|
9632
|
+
let unpacked = 0;
|
|
9633
|
+
let at = 0;
|
|
9634
|
+
for (; ; ) {
|
|
9635
|
+
if (at >= tar.length) throw new ArchiveError("its tar has no end marker");
|
|
9636
|
+
const header = tar.subarray(at, at + BLOCK);
|
|
9637
|
+
if (header.every((b) => b === 0)) {
|
|
9638
|
+
if (!tar.subarray(at).every((b) => b === 0)) throw new ArchiveError("it has data after its tar end marker");
|
|
9639
|
+
break;
|
|
9640
|
+
}
|
|
9641
|
+
let sum = 0;
|
|
9642
|
+
for (let i = 0; i < BLOCK; i += 1) sum += i >= 148 && i < 156 ? 32 : header[i];
|
|
9643
|
+
if (octal(header, 148, 8) !== sum) throw new ArchiveError("it has a tar header with a wrong checksum");
|
|
9644
|
+
if (header.toString("latin1", 257, 263) !== "ustar\0" || header.toString("latin1", 263, 265) !== "00") {
|
|
9645
|
+
throw new ArchiveError("its tar is not ustar");
|
|
9646
|
+
}
|
|
9647
|
+
const name = field(header, 0, 100);
|
|
9648
|
+
const prefix = field(header, 345, 155);
|
|
9649
|
+
let path = prefix ? `${prefix}/${name}` : name;
|
|
9650
|
+
const type = String.fromCharCode(header[156]);
|
|
9651
|
+
const size = octal(header, 124, 12);
|
|
9652
|
+
const mode = octal(header, 100, 8);
|
|
9653
|
+
if (size === null || mode === null) throw new ArchiveError(`it has a tar header for ${JSON.stringify(path)} with a bad size or mode`);
|
|
9654
|
+
const isDir = type === "5";
|
|
9655
|
+
if (!isDir && type !== "0" && type !== "\0") {
|
|
9656
|
+
const what = TYPE_NAMES[type] ?? `an entry of type ${JSON.stringify(type)}`;
|
|
9657
|
+
throw new ArchiveError(`${JSON.stringify(path)} is ${what}; a skill archive holds only files and folders`);
|
|
9658
|
+
}
|
|
9659
|
+
if (isDir && path.endsWith("/")) path = path.slice(0, -1);
|
|
9660
|
+
checkPath(path);
|
|
9661
|
+
const key = path.toLowerCase();
|
|
9662
|
+
const parts = key.split("/");
|
|
9663
|
+
for (let i = 1; i < parts.length; i += 1) {
|
|
9664
|
+
const parent = parts.slice(0, i).join("/");
|
|
9665
|
+
const kind = seen.get(parent);
|
|
9666
|
+
if (kind === "file") throw new ArchiveError(`it puts ${JSON.stringify(path)} inside a file`);
|
|
9667
|
+
if (!kind) seen.set(parent, "implied");
|
|
9668
|
+
}
|
|
9669
|
+
const before = seen.get(key);
|
|
9670
|
+
if (before === "file" || before === "dir" || before === "implied" && !isDir) {
|
|
9671
|
+
throw new ArchiveError(`it lists ${JSON.stringify(path)} twice`);
|
|
9672
|
+
}
|
|
9673
|
+
const start = at + BLOCK;
|
|
9674
|
+
if (isDir) {
|
|
9675
|
+
if (size !== 0) throw new ArchiveError(`its folder ${JSON.stringify(path)} has a size`);
|
|
9676
|
+
seen.set(key, "dir");
|
|
9677
|
+
dirs.push(path);
|
|
9678
|
+
} else {
|
|
9679
|
+
if (!FILE_MODES.has(mode & 4095)) {
|
|
9680
|
+
throw new ArchiveError(`${JSON.stringify(path)} has mode ${(mode & 4095).toString(8).padStart(4, "0")}; a file must be 0644 or 0755`);
|
|
9681
|
+
}
|
|
9682
|
+
if (files.length + 1 > MAX_ARCHIVE_FILES) throw new ArchiveError(`it has more than ${MAX_ARCHIVE_FILES} files`, true);
|
|
9683
|
+
unpacked += size;
|
|
9684
|
+
if (unpacked > MAX_UNPACKED_BYTES) throw new ArchiveError(`its files come to more than ${MAX_UNPACKED_BYTES / 1024 / 1024} MB`, true);
|
|
9685
|
+
if (start + size > tar.length) throw new ArchiveError(`its tar is cut short inside ${JSON.stringify(path)}`);
|
|
9686
|
+
seen.set(key, "file");
|
|
9687
|
+
files.push({ path, bytes: Buffer.from(tar.subarray(start, start + size)), mode: mode & 4095 });
|
|
9688
|
+
}
|
|
9689
|
+
at = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
9690
|
+
}
|
|
9691
|
+
const allDirs = /* @__PURE__ */ new Map();
|
|
9692
|
+
const addDirsOf = (path, self) => {
|
|
9693
|
+
const parts = path.split("/");
|
|
9694
|
+
for (let i = 1; i <= parts.length - (self ? 0 : 1); i += 1) {
|
|
9695
|
+
const dir = parts.slice(0, i).join("/");
|
|
9696
|
+
if (!allDirs.has(dir.toLowerCase())) allDirs.set(dir.toLowerCase(), dir);
|
|
9697
|
+
}
|
|
9698
|
+
};
|
|
9699
|
+
for (const dir of dirs) addDirsOf(dir, true);
|
|
9700
|
+
for (const f of files) addDirsOf(f.path, false);
|
|
9701
|
+
return { files, dirs: [...allDirs.values()].sort((a, b) => a.split("/").length - b.split("/").length) };
|
|
9702
|
+
}
|
|
9703
|
+
function readSkillArchive(gz) {
|
|
9704
|
+
return readTar(gunzipChecked(gz));
|
|
9705
|
+
}
|
|
9706
|
+
|
|
9506
9707
|
// src/commands/skills-warning.ts
|
|
9507
9708
|
var WARNING_FIELD = "x-amdahl-warning";
|
|
9508
9709
|
var MAX_WARNING_CHARS = 300;
|
|
@@ -9929,7 +10130,7 @@ async function workspaceUse(ctx, args) {
|
|
|
9929
10130
|
}
|
|
9930
10131
|
|
|
9931
10132
|
// src/version.ts
|
|
9932
|
-
var VERSION = "0.
|
|
10133
|
+
var VERSION = "0.3.0";
|
|
9933
10134
|
|
|
9934
10135
|
// src/main.ts
|
|
9935
10136
|
var COMMANDS = {
|
|
@@ -9964,6 +10165,7 @@ async function runCli(argv, io) {
|
|
|
9964
10165
|
return EXIT.ok;
|
|
9965
10166
|
}
|
|
9966
10167
|
if (!parsed.command) {
|
|
10168
|
+
if (argv.length === 0 && shouldSetUp(io)) return await setup(createCtx(io, out, parsed.flags));
|
|
9967
10169
|
io.stderr(USAGE);
|
|
9968
10170
|
return EXIT.usage;
|
|
9969
10171
|
}
|
package/package.json
CHANGED