skilld 1.7.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/add.mjs +66 -0
- package/dist/_chunks/add.mjs.map +1 -0
- package/dist/_chunks/agent-prompt.mjs +88 -0
- package/dist/_chunks/agent-prompt.mjs.map +1 -0
- package/dist/_chunks/agent.mjs +737 -619
- package/dist/_chunks/agent.mjs.map +1 -1
- package/dist/_chunks/args.mjs +42 -0
- package/dist/_chunks/args.mjs.map +1 -0
- package/dist/_chunks/assemble.mjs +11 -8
- package/dist/_chunks/assemble.mjs.map +1 -1
- package/dist/_chunks/author.mjs +77 -131
- package/dist/_chunks/author.mjs.map +1 -1
- package/dist/_chunks/cache.mjs +320 -54
- package/dist/_chunks/cache.mjs.map +1 -1
- package/dist/_chunks/cache2.mjs +7 -6
- package/dist/_chunks/cache2.mjs.map +1 -1
- package/dist/_chunks/client.mjs +117 -0
- package/dist/_chunks/client.mjs.map +1 -0
- package/dist/_chunks/core.mjs +7 -4
- package/dist/_chunks/detect.mjs +54 -44
- package/dist/_chunks/detect.mjs.map +1 -1
- package/dist/_chunks/eject.mjs +69 -0
- package/dist/_chunks/eject.mjs.map +1 -0
- package/dist/_chunks/embedding-cache2.mjs +2 -2
- package/dist/_chunks/env.mjs +19 -0
- package/dist/_chunks/env.mjs.map +1 -0
- package/dist/_chunks/install-many.mjs +376 -0
- package/dist/_chunks/install-many.mjs.map +1 -0
- package/dist/_chunks/install.mjs +86 -371
- package/dist/_chunks/install.mjs.map +1 -1
- package/dist/_chunks/intro.mjs +63 -0
- package/dist/_chunks/intro.mjs.map +1 -0
- package/dist/_chunks/list.mjs +2 -2
- package/dist/_chunks/list.mjs.map +1 -1
- package/dist/_chunks/lockfile.mjs +31 -7
- package/dist/_chunks/lockfile.mjs.map +1 -1
- package/dist/_chunks/login.mjs +233 -0
- package/dist/_chunks/login.mjs.map +1 -0
- package/dist/_chunks/logout.mjs +27 -0
- package/dist/_chunks/logout.mjs.map +1 -0
- package/dist/_chunks/map.mjs +11 -0
- package/dist/_chunks/map.mjs.map +1 -0
- package/dist/_chunks/markdown.mjs +79 -54
- package/dist/_chunks/markdown.mjs.map +1 -1
- package/dist/_chunks/menu.mjs +33 -0
- package/dist/_chunks/menu.mjs.map +1 -0
- package/dist/_chunks/model-picker.mjs +61 -0
- package/dist/_chunks/model-picker.mjs.map +1 -0
- package/dist/_chunks/monorepo.mjs +73 -0
- package/dist/_chunks/monorepo.mjs.map +1 -0
- package/dist/_chunks/package-json.mjs.map +1 -1
- package/dist/_chunks/paths.mjs +47 -0
- package/dist/_chunks/paths.mjs.map +1 -0
- package/dist/_chunks/pipeline.mjs +985 -0
- package/dist/_chunks/pipeline.mjs.map +1 -0
- package/dist/_chunks/pool2.mjs +2 -2
- package/dist/_chunks/portable.mjs +151 -0
- package/dist/_chunks/portable.mjs.map +1 -0
- package/dist/_chunks/prepare-hook.mjs +2 -0
- package/dist/_chunks/prepare-hook2.mjs +61 -0
- package/dist/_chunks/prepare-hook2.mjs.map +1 -0
- package/dist/_chunks/prepare.mjs +47 -3
- package/dist/_chunks/prepare.mjs.map +1 -1
- package/dist/_chunks/prepare2.mjs +9 -8
- package/dist/_chunks/prepare2.mjs.map +1 -1
- package/dist/_chunks/prompts.mjs +784 -26
- package/dist/_chunks/prompts.mjs.map +1 -1
- package/dist/_chunks/pull.mjs +219 -0
- package/dist/_chunks/pull.mjs.map +1 -0
- package/dist/_chunks/regex.mjs +19 -0
- package/dist/_chunks/regex.mjs.map +1 -0
- package/dist/_chunks/retriv.mjs +2 -171
- package/dist/_chunks/retriv2.mjs +159 -0
- package/dist/_chunks/retriv2.mjs.map +1 -0
- package/dist/_chunks/sanitize.mjs +12 -9
- package/dist/_chunks/sanitize.mjs.map +1 -1
- package/dist/_chunks/search-helpers.mjs +9 -8
- package/dist/_chunks/search-helpers.mjs.map +1 -1
- package/dist/_chunks/search-interactive.mjs +23 -20
- package/dist/_chunks/search-interactive.mjs.map +1 -1
- package/dist/_chunks/search.mjs +3 -4
- package/dist/_chunks/search.mjs.map +1 -1
- package/dist/_chunks/{sources.mjs → semver.mjs} +1128 -838
- package/dist/_chunks/semver.mjs.map +1 -0
- package/dist/_chunks/skill-installer.mjs +2 -0
- package/dist/_chunks/skill-installer2.mjs +154 -0
- package/dist/_chunks/skill-installer2.mjs.map +1 -0
- package/dist/_chunks/skills.mjs +12 -12
- package/dist/_chunks/skills.mjs.map +1 -1
- package/dist/_chunks/store.mjs +107 -0
- package/dist/_chunks/store.mjs.map +1 -0
- package/dist/_chunks/sync.mjs +761 -1349
- package/dist/_chunks/sync.mjs.map +1 -1
- package/dist/_chunks/sync2.mjs +2 -3
- package/dist/_chunks/telemetry.mjs +26 -0
- package/dist/_chunks/telemetry.mjs.map +1 -0
- package/dist/_chunks/uninstall.mjs +15 -13
- package/dist/_chunks/uninstall.mjs.map +1 -1
- package/dist/_chunks/update.mjs +171 -0
- package/dist/_chunks/update.mjs.map +1 -0
- package/dist/_chunks/upload.mjs +4 -4
- package/dist/_chunks/validate.mjs +1 -1
- package/dist/_chunks/version.mjs +16 -27
- package/dist/_chunks/version.mjs.map +1 -1
- package/dist/_chunks/whoami.mjs +21 -0
- package/dist/_chunks/whoami.mjs.map +1 -0
- package/dist/_chunks/wizard.mjs +2 -190
- package/dist/_chunks/wizard2.mjs +200 -0
- package/dist/_chunks/wizard2.mjs.map +1 -0
- package/dist/cli.mjs +77 -59
- package/dist/cli.mjs.map +1 -1
- package/dist/prepare.mjs +5 -4
- package/dist/prepare.mjs.map +1 -1
- package/dist/retriv/worker.d.mts +5 -1
- package/dist/retriv/worker.d.mts.map +1 -1
- package/dist/retriv/worker.mjs +1 -1
- package/package.json +20 -29
- package/dist/_chunks/author-group.mjs +0 -17
- package/dist/_chunks/author-group.mjs.map +0 -1
- package/dist/_chunks/cli-helpers.mjs +0 -335
- package/dist/_chunks/cli-helpers.mjs.map +0 -1
- package/dist/_chunks/cli-helpers2.mjs +0 -2
- package/dist/_chunks/config.mjs +0 -122
- package/dist/_chunks/config.mjs.map +0 -1
- package/dist/_chunks/index.d.mts +0 -151
- package/dist/_chunks/index.d.mts.map +0 -1
- package/dist/_chunks/index2.d.mts +0 -44
- package/dist/_chunks/index2.d.mts.map +0 -1
- package/dist/_chunks/index3.d.mts +0 -589
- package/dist/_chunks/index3.d.mts.map +0 -1
- package/dist/_chunks/prefix.mjs +0 -108
- package/dist/_chunks/prefix.mjs.map +0 -1
- package/dist/_chunks/retriv.mjs.map +0 -1
- package/dist/_chunks/setup.mjs +0 -17
- package/dist/_chunks/setup.mjs.map +0 -1
- package/dist/_chunks/shared.mjs +0 -503
- package/dist/_chunks/shared.mjs.map +0 -1
- package/dist/_chunks/skill.mjs +0 -329
- package/dist/_chunks/skill.mjs.map +0 -1
- package/dist/_chunks/sources.mjs.map +0 -1
- package/dist/_chunks/sync-registry.mjs +0 -59
- package/dist/_chunks/sync-registry.mjs.map +0 -1
- package/dist/_chunks/sync-shared.mjs +0 -2
- package/dist/_chunks/sync-shared2.mjs +0 -1020
- package/dist/_chunks/sync-shared2.mjs.map +0 -1
- package/dist/_chunks/types.d.mts +0 -88
- package/dist/_chunks/types.d.mts.map +0 -1
- package/dist/_chunks/wizard.mjs.map +0 -1
- package/dist/agent/index.d.mts +0 -346
- package/dist/agent/index.d.mts.map +0 -1
- package/dist/agent/index.mjs +0 -5
- package/dist/cache/index.d.mts +0 -2
- package/dist/cache/index.mjs +0 -4
- package/dist/index.d.mts +0 -5
- package/dist/index.mjs +0 -5
- package/dist/retriv/index.d.mts +0 -3
- package/dist/retriv/index.mjs +0 -2
- package/dist/sources/index.d.mts +0 -2
- package/dist/sources/index.mjs +0 -3
- package/dist/types.d.mts +0 -4
- package/dist/types.mjs +0 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { t as version } from "./version.mjs";
|
|
2
|
+
import { i as saveSession } from "./store.mjs";
|
|
3
|
+
import { i as getRegistryBase } from "./client.mjs";
|
|
4
|
+
import { t as track } from "./telemetry.mjs";
|
|
5
|
+
import { styleText } from "node:util";
|
|
6
|
+
import * as p from "@clack/prompts";
|
|
7
|
+
import { defineCommand } from "citty";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
10
|
+
import { ofetch } from "ofetch";
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
async function runDeviceFlow(opts) {
|
|
13
|
+
const start = await ofetch(`${opts.registryBase}/cli/device/start`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
body: {
|
|
16
|
+
cli_version: opts.cliVersion,
|
|
17
|
+
machine_hint: opts.machineHint
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
opts.onUserCode({
|
|
21
|
+
userCode: start.user_code,
|
|
22
|
+
verificationUri: start.verification_uri
|
|
23
|
+
});
|
|
24
|
+
const deadline = Date.now() + start.expires_in * 1e3;
|
|
25
|
+
const interval = opts.intervalMs ?? start.interval * 1e3;
|
|
26
|
+
while (Date.now() < deadline) {
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
28
|
+
const poll = await ofetch(`${opts.registryBase}/cli/device/poll`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: { device_code: start.device_code }
|
|
31
|
+
}).catch(() => null);
|
|
32
|
+
if (!poll || poll.status === "pending") continue;
|
|
33
|
+
if (poll.status === "expired") throw new Error("Device code expired before authorization");
|
|
34
|
+
if (poll.status === "denied") throw new Error("Device authorization denied");
|
|
35
|
+
if (poll.status === "authorized" && poll.tokens) return poll.tokens;
|
|
36
|
+
}
|
|
37
|
+
throw new Error("Device authorization timed out");
|
|
38
|
+
}
|
|
39
|
+
function isGhaOidcAvailable() {
|
|
40
|
+
return !!(process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
|
|
41
|
+
}
|
|
42
|
+
async function runOidcExchange(opts) {
|
|
43
|
+
const token = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
44
|
+
const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
45
|
+
if (!token || !url) throw new Error("Not running in GitHub Actions with id-token: write permission");
|
|
46
|
+
const audience = opts.audience ?? "skilld.dev";
|
|
47
|
+
const idToken = await ofetch(`${url}&audience=${encodeURIComponent(audience)}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
48
|
+
return ofetch(`${opts.registryBase}/cli/oidc/exchange`, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
body: { id_token: idToken.value }
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>skilld — signed in</title>
|
|
54
|
+
<body style="font-family: ui-sans-serif, system-ui; padding: 4rem; text-align: center">
|
|
55
|
+
<h1>Signed in to skilld</h1><p>You can close this tab and return to the CLI.</p>`;
|
|
56
|
+
function ERROR_HTML(msg) {
|
|
57
|
+
return `<!doctype html><meta charset="utf-8"><title>skilld — error</title>
|
|
58
|
+
<body style="font-family: ui-sans-serif, system-ui; padding: 4rem; text-align: center; color: #b00">
|
|
59
|
+
<h1>Sign-in failed</h1><p>${msg}</p>`;
|
|
60
|
+
}
|
|
61
|
+
const PLUS_RE = /\+/g;
|
|
62
|
+
const SLASH_RE = /\//g;
|
|
63
|
+
const EQ_RE = /=+$/;
|
|
64
|
+
const TRAILING_SLASH_RE = /\/$/;
|
|
65
|
+
const TRAILING_API_RE = /\/api$/;
|
|
66
|
+
function base64url(buf) {
|
|
67
|
+
return buf.toString("base64").replace(PLUS_RE, "-").replace(SLASH_RE, "_").replace(EQ_RE, "");
|
|
68
|
+
}
|
|
69
|
+
function generateVerifier() {
|
|
70
|
+
return base64url(randomBytes(32));
|
|
71
|
+
}
|
|
72
|
+
function challengeFromVerifier(verifier) {
|
|
73
|
+
return base64url(createHash("sha256").update(verifier).digest());
|
|
74
|
+
}
|
|
75
|
+
async function runPkceFlow(opts) {
|
|
76
|
+
const verifier = generateVerifier();
|
|
77
|
+
const challenge = challengeFromVerifier(verifier);
|
|
78
|
+
const state = base64url(randomBytes(16));
|
|
79
|
+
const { port, server, gotCode } = await bindLoopback(state);
|
|
80
|
+
const verificationUrl = new URL(`${opts.registryBase.replace(TRAILING_SLASH_RE, "").replace(TRAILING_API_RE, "")}/cli/authorize`);
|
|
81
|
+
verificationUrl.searchParams.set("challenge", challenge);
|
|
82
|
+
verificationUrl.searchParams.set("port", String(port));
|
|
83
|
+
verificationUrl.searchParams.set("state", state);
|
|
84
|
+
verificationUrl.searchParams.set("v", opts.cliVersion);
|
|
85
|
+
await (opts.openBrowser ?? defaultOpenBrowser)(verificationUrl.toString());
|
|
86
|
+
try {
|
|
87
|
+
const code = await Promise.race([gotCode, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("PKCE flow timed out")), opts.timeoutMs ?? 5 * 6e4))]);
|
|
88
|
+
return await ofetch(`${opts.registryBase}/cli/oauth/token`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: {
|
|
91
|
+
code,
|
|
92
|
+
code_verifier: verifier,
|
|
93
|
+
redirect_uri: `http://127.0.0.1:${port}/`
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
} finally {
|
|
97
|
+
server.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function bindLoopback(expectedState) {
|
|
101
|
+
let resolveCode;
|
|
102
|
+
let rejectCode;
|
|
103
|
+
const gotCode = new Promise((res, rej) => {
|
|
104
|
+
resolveCode = res;
|
|
105
|
+
rejectCode = rej;
|
|
106
|
+
});
|
|
107
|
+
const handler = (req, res) => {
|
|
108
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
109
|
+
const code = url.searchParams.get("code");
|
|
110
|
+
const state = url.searchParams.get("state");
|
|
111
|
+
if (!code || state !== expectedState) {
|
|
112
|
+
res.writeHead(400, { "content-type": "text/html" }).end(ERROR_HTML("Missing or invalid state parameter."));
|
|
113
|
+
rejectCode(/* @__PURE__ */ new Error("PKCE callback missing code or state mismatch"));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
res.writeHead(200, { "content-type": "text/html" }).end(SUCCESS_HTML);
|
|
117
|
+
resolveCode(code);
|
|
118
|
+
};
|
|
119
|
+
const v4 = createServer(handler);
|
|
120
|
+
const v6 = createServer(handler);
|
|
121
|
+
await new Promise((resolve, reject) => {
|
|
122
|
+
v4.once("error", reject).listen(0, "127.0.0.1", () => resolve());
|
|
123
|
+
});
|
|
124
|
+
const port = v4.address().port;
|
|
125
|
+
await new Promise((resolve) => {
|
|
126
|
+
v6.once("error", () => resolve()).listen(port, "::1", () => resolve());
|
|
127
|
+
});
|
|
128
|
+
const close = () => {
|
|
129
|
+
v4.closeAllConnections();
|
|
130
|
+
v6.closeAllConnections();
|
|
131
|
+
v4.close();
|
|
132
|
+
v6.close();
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
port,
|
|
136
|
+
server: { close },
|
|
137
|
+
gotCode
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function defaultOpenBrowser(url) {
|
|
141
|
+
spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open", [url], {
|
|
142
|
+
stdio: "ignore",
|
|
143
|
+
detached: true
|
|
144
|
+
}).unref();
|
|
145
|
+
}
|
|
146
|
+
function shouldUseDevice(force) {
|
|
147
|
+
if (force) return true;
|
|
148
|
+
if (process.env.BROWSER) return false;
|
|
149
|
+
if (process.platform === "darwin" || process.platform === "win32") return false;
|
|
150
|
+
return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
151
|
+
}
|
|
152
|
+
const loginCommandDef = defineCommand({
|
|
153
|
+
meta: {
|
|
154
|
+
name: "login",
|
|
155
|
+
description: "Authenticate with skilld.dev"
|
|
156
|
+
},
|
|
157
|
+
args: { device: {
|
|
158
|
+
type: "boolean",
|
|
159
|
+
description: "Use RFC 8628 device flow"
|
|
160
|
+
} },
|
|
161
|
+
async run({ args }) {
|
|
162
|
+
const registryBase = getRegistryBase();
|
|
163
|
+
if (isGhaOidcAvailable()) {
|
|
164
|
+
const spin = p.spinner();
|
|
165
|
+
spin.start("Exchanging GitHub Actions OIDC token");
|
|
166
|
+
const tokens = await runOidcExchange({ registryBase });
|
|
167
|
+
spin.stop(`Authenticated as @${tokens.login} (oidc)`);
|
|
168
|
+
await saveSession({
|
|
169
|
+
login: tokens.login,
|
|
170
|
+
accessToken: tokens.accessToken,
|
|
171
|
+
expiresAt: tokens.expiresAt,
|
|
172
|
+
tokens: { accessToken: tokens.accessToken }
|
|
173
|
+
});
|
|
174
|
+
track({
|
|
175
|
+
event: "auth-flow",
|
|
176
|
+
surface: "cli:auth",
|
|
177
|
+
flow: "oidc"
|
|
178
|
+
});
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
if (shouldUseDevice(!!args.device)) {
|
|
182
|
+
const tokens = await runDeviceFlow({
|
|
183
|
+
registryBase,
|
|
184
|
+
cliVersion: version,
|
|
185
|
+
onUserCode: ({ userCode, verificationUri }) => {
|
|
186
|
+
p.log.info(`Visit ${styleText("cyan", verificationUri)} and enter ${styleText("bold", userCode)}`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
await saveSession({
|
|
190
|
+
login: tokens.login,
|
|
191
|
+
accessToken: tokens.accessToken,
|
|
192
|
+
refreshToken: tokens.refreshToken,
|
|
193
|
+
expiresAt: tokens.expiresAt,
|
|
194
|
+
tokens: {
|
|
195
|
+
accessToken: tokens.accessToken,
|
|
196
|
+
refreshToken: tokens.refreshToken
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
track({
|
|
200
|
+
event: "auth-flow",
|
|
201
|
+
surface: "cli:auth",
|
|
202
|
+
flow: "device"
|
|
203
|
+
});
|
|
204
|
+
p.log.success(`Logged in as @${tokens.login}`);
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
p.log.info("Opening browser to authenticate…");
|
|
208
|
+
const tokens = await runPkceFlow({
|
|
209
|
+
registryBase,
|
|
210
|
+
cliVersion: version
|
|
211
|
+
});
|
|
212
|
+
await saveSession({
|
|
213
|
+
login: tokens.login,
|
|
214
|
+
accessToken: tokens.accessToken,
|
|
215
|
+
refreshToken: tokens.refreshToken,
|
|
216
|
+
expiresAt: tokens.expiresAt,
|
|
217
|
+
tokens: {
|
|
218
|
+
accessToken: tokens.accessToken,
|
|
219
|
+
refreshToken: tokens.refreshToken
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
track({
|
|
223
|
+
event: "auth-flow",
|
|
224
|
+
surface: "cli:auth",
|
|
225
|
+
flow: "pkce"
|
|
226
|
+
});
|
|
227
|
+
p.log.success(`Logged in as @${tokens.login}`);
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
export { loginCommandDef };
|
|
232
|
+
|
|
233
|
+
//# sourceMappingURL=login.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.mjs","names":[],"sources":["../../src/auth/device-flow.ts","../../src/auth/oidc.ts","../../src/auth/pkce-flow.ts","../../src/commands/login.ts"],"sourcesContent":["/**\n * RFC 8628 device flow. Used when --device is passed, no browser is detected,\n * or PKCE bind fails.\n */\n\nimport type { DevicePollResponse, DeviceStartResponse, TokenResponse } from 'skilld-protocol/wire'\nimport { ofetch } from 'ofetch'\n\nexport type { DeviceStartResponse }\n\nexport interface DeviceFlowOptions {\n registryBase: string\n cliVersion: string\n machineHint?: string\n /** Hook called once the user_code is known so the CLI can prompt the user. */\n onUserCode: (info: { userCode: string, verificationUri: string }) => void\n /** Override polling interval for tests. */\n intervalMs?: number\n}\n\nexport async function runDeviceFlow(opts: DeviceFlowOptions): Promise<TokenResponse> {\n const start = await ofetch<DeviceStartResponse>(`${opts.registryBase}/cli/device/start`, {\n method: 'POST',\n body: { cli_version: opts.cliVersion, machine_hint: opts.machineHint },\n })\n\n opts.onUserCode({ userCode: start.user_code, verificationUri: start.verification_uri })\n\n const deadline = Date.now() + start.expires_in * 1000\n const interval = opts.intervalMs ?? start.interval * 1000\n\n while (Date.now() < deadline) {\n await new Promise(resolve => setTimeout(resolve, interval))\n const poll = await ofetch<DevicePollResponse>(`${opts.registryBase}/cli/device/poll`, {\n method: 'POST',\n body: { device_code: start.device_code },\n }).catch(() => null)\n\n if (!poll || poll.status === 'pending')\n continue\n if (poll.status === 'expired')\n throw new Error('Device code expired before authorization')\n if (poll.status === 'denied')\n throw new Error('Device authorization denied')\n if (poll.status === 'authorized' && poll.tokens)\n return poll.tokens\n }\n\n throw new Error('Device authorization timed out')\n}\n","/**\n * GitHub Actions OIDC exchange. Auto-detected via `ACTIONS_ID_TOKEN_REQUEST_TOKEN`;\n * fetches a short-lived JWT against `audience=skilld.dev` and trades it for a\n * session token. No browser, no prompt, no refresh.\n */\n\nimport type { TokenResponse } from './types.ts'\nimport { ofetch } from 'ofetch'\n\ninterface GhaOidcResponse {\n value: string\n count?: number\n}\n\nexport function isGhaOidcAvailable(): boolean {\n return !!(process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && process.env.ACTIONS_ID_TOKEN_REQUEST_URL)\n}\n\nexport async function runOidcExchange(opts: { registryBase: string, audience?: string }): Promise<TokenResponse> {\n const token = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN\n const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL\n if (!token || !url)\n throw new Error('Not running in GitHub Actions with id-token: write permission')\n\n const audience = opts.audience ?? 'skilld.dev'\n const idToken = await ofetch<GhaOidcResponse>(`${url}&audience=${encodeURIComponent(audience)}`, {\n headers: { Authorization: `Bearer ${token}` },\n })\n\n return ofetch<TokenResponse>(`${opts.registryBase}/cli/oidc/exchange`, {\n method: 'POST',\n body: { id_token: idToken.value },\n })\n}\n","/**\n * RFC 7636 PKCE loopback flow.\n *\n * Binds `127.0.0.1:<port>` and `[::1]:<port>` simultaneously so the browser\n * can hit either; opens the system browser to the verification URL; serves a\n * single GET callback that captures the auth code, then exchanges it for\n * tokens against `/api/cli/oauth/token`.\n */\n\nimport type { AddressInfo } from 'node:net'\nimport type { TokenResponse } from './types.ts'\nimport { spawn } from 'node:child_process'\nimport { createHash, randomBytes } from 'node:crypto'\nimport { createServer } from 'node:http'\nimport { ofetch } from 'ofetch'\n\nconst SUCCESS_HTML = `<!doctype html><meta charset=\"utf-8\"><title>skilld — signed in</title>\n<body style=\"font-family: ui-sans-serif, system-ui; padding: 4rem; text-align: center\">\n<h1>Signed in to skilld</h1><p>You can close this tab and return to the CLI.</p>`\n\nfunction ERROR_HTML(msg: string) {\n return `<!doctype html><meta charset=\"utf-8\"><title>skilld — error</title>\n<body style=\"font-family: ui-sans-serif, system-ui; padding: 4rem; text-align: center; color: #b00\">\n<h1>Sign-in failed</h1><p>${msg}</p>`\n}\n\nexport interface PkceFlowOptions {\n registryBase: string\n cliVersion: string\n openBrowser?: (url: string) => Promise<void> | void\n timeoutMs?: number\n}\n\nconst PLUS_RE = /\\+/g\nconst SLASH_RE = /\\//g\nconst EQ_RE = /=+$/\nconst TRAILING_SLASH_RE = /\\/$/\nconst TRAILING_API_RE = /\\/api$/\n\nfunction base64url(buf: Buffer): string {\n return buf.toString('base64').replace(PLUS_RE, '-').replace(SLASH_RE, '_').replace(EQ_RE, '')\n}\n\nfunction generateVerifier(): string {\n return base64url(randomBytes(32))\n}\n\nfunction challengeFromVerifier(verifier: string): string {\n return base64url(createHash('sha256').update(verifier).digest())\n}\n\nexport async function runPkceFlow(opts: PkceFlowOptions): Promise<TokenResponse> {\n const verifier = generateVerifier()\n const challenge = challengeFromVerifier(verifier)\n const state = base64url(randomBytes(16))\n\n const { port, server, gotCode } = await bindLoopback(state)\n const verificationUrl = new URL(`${opts.registryBase.replace(TRAILING_SLASH_RE, '').replace(TRAILING_API_RE, '')}/cli/authorize`)\n verificationUrl.searchParams.set('challenge', challenge)\n verificationUrl.searchParams.set('port', String(port))\n verificationUrl.searchParams.set('state', state)\n verificationUrl.searchParams.set('v', opts.cliVersion)\n\n await (opts.openBrowser ?? defaultOpenBrowser)(verificationUrl.toString())\n\n try {\n const code = await Promise.race([\n gotCode,\n new Promise<never>((_, reject) => setTimeout(() => reject(new Error('PKCE flow timed out')), opts.timeoutMs ?? 5 * 60_000)),\n ])\n\n return await ofetch<TokenResponse>(`${opts.registryBase}/cli/oauth/token`, {\n method: 'POST',\n body: { code, code_verifier: verifier, redirect_uri: `http://127.0.0.1:${port}/` },\n })\n }\n finally {\n server.close()\n }\n}\n\ninterface LoopbackBinding {\n port: number\n server: { close: () => void }\n gotCode: Promise<string>\n}\n\nasync function bindLoopback(expectedState: string): Promise<LoopbackBinding> {\n let resolveCode!: (code: string) => void\n let rejectCode!: (err: Error) => void\n const gotCode = new Promise<string>((res, rej) => {\n resolveCode = res\n rejectCode = rej\n })\n\n const handler = (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse): void => {\n const url = new URL(req.url ?? '/', 'http://localhost')\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n if (!code || state !== expectedState) {\n res.writeHead(400, { 'content-type': 'text/html' }).end(ERROR_HTML('Missing or invalid state parameter.'))\n rejectCode(new Error('PKCE callback missing code or state mismatch'))\n return\n }\n res.writeHead(200, { 'content-type': 'text/html' }).end(SUCCESS_HTML)\n resolveCode(code)\n }\n\n const v4 = createServer(handler)\n const v6 = createServer(handler)\n\n await new Promise<void>((resolve, reject) => {\n v4.once('error', reject).listen(0, '127.0.0.1', () => resolve())\n })\n const port = (v4.address() as AddressInfo).port\n await new Promise<void>((resolve) => {\n v6.once('error', () => resolve()).listen(port, '::1', () => resolve())\n })\n\n const close = (): void => {\n // closeAllConnections() forces still-open keep-alive sockets shut so the\n // process can exit promptly after the browser hits the success page.\n v4.closeAllConnections()\n v6.closeAllConnections()\n v4.close()\n v6.close()\n }\n\n return {\n port,\n server: { close },\n gotCode,\n }\n}\n\nfunction defaultOpenBrowser(url: string): void {\n const cmd = process.platform === 'darwin'\n ? 'open'\n : process.platform === 'win32'\n ? 'start'\n : 'xdg-open'\n spawn(cmd, [url], { stdio: 'ignore', detached: true }).unref()\n}\n","/**\n * `skilld login` — authenticate with skilld.dev.\n *\n * Picks a flow based on env:\n * - `ACTIONS_ID_TOKEN_REQUEST_TOKEN` set → GHA OIDC exchange.\n * - `--device` or no `DISPLAY`/`BROWSER` env → RFC 8628 device flow.\n * - Otherwise → PKCE loopback.\n */\n\nimport { styleText } from 'node:util'\nimport * as p from '@clack/prompts'\nimport { defineCommand } from 'citty'\nimport { runDeviceFlow } from '../auth/device-flow.ts'\nimport { isGhaOidcAvailable, runOidcExchange } from '../auth/oidc.ts'\nimport { runPkceFlow } from '../auth/pkce-flow.ts'\nimport { saveSession } from '../auth/store.ts'\nimport { getRegistryBase } from '../registry/client.ts'\nimport { track } from '../telemetry.ts'\nimport { version } from '../version.ts'\n\nfunction shouldUseDevice(force: boolean): boolean {\n if (force)\n return true\n if (process.env.BROWSER)\n return false\n if (process.platform === 'darwin' || process.platform === 'win32')\n return false\n return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY\n}\n\nexport const loginCommandDef = defineCommand({\n meta: { name: 'login', description: 'Authenticate with skilld.dev' },\n args: {\n device: { type: 'boolean', description: 'Use RFC 8628 device flow' },\n },\n async run({ args }) {\n const registryBase = getRegistryBase()\n\n if (isGhaOidcAvailable()) {\n const spin = p.spinner()\n spin.start('Exchanging GitHub Actions OIDC token')\n const tokens = await runOidcExchange({ registryBase })\n spin.stop(`Authenticated as @${tokens.login} (oidc)`)\n await saveSession({\n login: tokens.login,\n accessToken: tokens.accessToken,\n expiresAt: tokens.expiresAt,\n tokens: { accessToken: tokens.accessToken },\n })\n track({ event: 'auth-flow', surface: 'cli:auth', flow: 'oidc' })\n process.exit(0)\n }\n\n if (shouldUseDevice(!!args.device)) {\n const tokens = await runDeviceFlow({\n registryBase,\n cliVersion: version,\n onUserCode: ({ userCode, verificationUri }) => {\n p.log.info(`Visit ${styleText('cyan', verificationUri)} and enter ${styleText('bold', userCode)}`)\n },\n })\n await saveSession({\n login: tokens.login,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n tokens: { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken },\n })\n track({ event: 'auth-flow', surface: 'cli:auth', flow: 'device' })\n p.log.success(`Logged in as @${tokens.login}`)\n process.exit(0)\n }\n\n p.log.info('Opening browser to authenticate…')\n const tokens = await runPkceFlow({ registryBase, cliVersion: version })\n await saveSession({\n login: tokens.login,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n tokens: { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken },\n })\n track({ event: 'auth-flow', surface: 'cli:auth', flow: 'pkce' })\n p.log.success(`Logged in as @${tokens.login}`)\n // Node's global fetch keep-alive pool + telemetry fire-and-forget leave\n // sockets ref'd; force exit so we don't wait for the 4s idle timeout.\n process.exit(0)\n },\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { n as loadSession, t as clearSession } from "./store.mjs";
|
|
2
|
+
import { i as getRegistryBase } from "./client.mjs";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { ofetch } from "ofetch";
|
|
6
|
+
const logoutCommandDef = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "logout",
|
|
9
|
+
description: "Sign out of skilld.dev"
|
|
10
|
+
},
|
|
11
|
+
async run() {
|
|
12
|
+
const session = await loadSession();
|
|
13
|
+
if (!session) {
|
|
14
|
+
p.log.info("Not logged in.");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
await ofetch(`${getRegistryBase()}/cli/logout`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { Authorization: `Bearer ${session.accessToken}` }
|
|
20
|
+
}).catch(() => {});
|
|
21
|
+
await clearSession();
|
|
22
|
+
p.log.success("Logged out.");
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
export { logoutCommandDef };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=logout.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logout.mjs","names":[],"sources":["../../src/commands/logout.ts"],"sourcesContent":["/**\n * `skilld logout` — revoke the active session server-side and clear local credentials.\n * Local state is cleared even if the server revoke fails.\n */\n\nimport * as p from '@clack/prompts'\nimport { defineCommand } from 'citty'\nimport { ofetch } from 'ofetch'\nimport { clearSession, loadSession } from '../auth/store.ts'\nimport { getRegistryBase } from '../registry/client.ts'\n\nexport const logoutCommandDef = defineCommand({\n meta: { name: 'logout', description: 'Sign out of skilld.dev' },\n async run() {\n const session = await loadSession()\n if (!session) {\n p.log.info('Not logged in.')\n return\n }\n\n await ofetch(`${getRegistryBase()}/cli/logout`, {\n method: 'POST',\n headers: { Authorization: `Bearer ${session.accessToken}` },\n }).catch(() => {})\n\n await clearSession()\n p.log.success('Logged out.')\n },\n})\n"],"mappings":";;;;;;;;EAWA,aAAa;EACX;OAAQ,MAAM;EAAU,MAAA,UAAa,MAAA,aAAA;EAA0B,IAAA,CAAA,SAAA;GAC/D,EAAA,IAAM,KAAM,iBAAA;GACV;;QAEI,OAAS,GAAA,iBAAiB,CAAA,cAAA;GAC5B,QAAA;;GAGF,CAAA,CAAA,YAAa,GAAG;QACd,cAAQ;IACR,IAAA,QAAW,cAAe;;EAG5B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map.mjs","names":[],"sources":["../../src/core/map.ts"],"sourcesContent":["/** Get-or-create for Maps. Polyfill for Map.getOrInsertComputed (not yet in Node.js). */\nexport function mapInsert<K, V>(map: Map<K, V>, key: K, create: () => V): V {\n let val = map.get(key)\n if (val === undefined) {\n val = create()\n map.set(key, val)\n }\n return val\n}\n"],"mappings":"AACA,SAAgB,UAAgB,KAAgB,KAAQ,QAAoB;CAC1E,IAAI,MAAM,IAAI,IAAI,IAAI;CACtB,IAAI,QAAQ,KAAA,GAAW;EACrB,MAAM,QAAQ;EACd,IAAI,IAAI,KAAK,IAAI;;CAEnB,OAAO"}
|
|
@@ -1,76 +1,101 @@
|
|
|
1
1
|
import { n as yamlParseKV } from "./yaml.mjs";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
2
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
3
|
+
const HEADING_LINE_RE = /^(#{1,6})[ \t]+([^ \t\r\n][^\r\n]*)$/gm;
|
|
4
|
+
const ANCHOR_RE = /\s*\{#[^}]+\}\s*$/;
|
|
5
|
+
const BACKSLASH_PREFIX_RE = /^\\+\s*/;
|
|
6
|
+
const INLINE_CODE_RE = /`([^`]+)`/g;
|
|
7
|
+
const LINK_RE = /(?<!!)\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
8
|
+
const HEADING_START_RE = /^#{1,6}\s/;
|
|
9
|
+
const BLOCKQUOTE_START_RE = /^>\s?/;
|
|
10
|
+
const LIST_ITEM_START_RE = /^[-*+]\s/;
|
|
11
|
+
const ORDERED_LIST_ITEM_START_RE = /^\d+\.\s/;
|
|
12
|
+
const TABLE_ROW_START_RE = /^\|/;
|
|
13
|
+
const INDENTED_CODE_START_RE = /^ {4}/;
|
|
14
|
+
const FENCE_START_RE = /^\s*```/;
|
|
15
|
+
const MARKDOWN_LINK_RE = /\[([^\]]+)\]\([^)]+\)/g;
|
|
16
|
+
const MARKDOWN_FORMATTING_RE = /[`*_~]/g;
|
|
17
|
+
const FENCED_CODE_BLOCK_RE = /```[\s\S]*?```/g;
|
|
18
|
+
const INLINE_CODE_SPAN_RE = /`[^`\n]*`/g;
|
|
19
|
+
function stripFrontmatter(content) {
|
|
20
|
+
const m = content.match(FRONTMATTER_RE);
|
|
21
|
+
return m ? content.slice(m[0].length).trim() : content;
|
|
23
22
|
}
|
|
24
23
|
function parseFrontmatter(content) {
|
|
25
|
-
|
|
24
|
+
const m = content.match(FRONTMATTER_RE);
|
|
25
|
+
if (!m) return {};
|
|
26
|
+
const fm = {};
|
|
27
|
+
for (const line of m[1].split("\n")) {
|
|
28
|
+
const kv = yamlParseKV(line);
|
|
29
|
+
if (kv) fm[kv[0]] = kv[1];
|
|
30
|
+
}
|
|
31
|
+
return fm;
|
|
26
32
|
}
|
|
27
|
-
function
|
|
28
|
-
return
|
|
33
|
+
function cleanHeadingText(raw) {
|
|
34
|
+
return raw.replace(ANCHOR_RE, "").replace(BACKSLASH_PREFIX_RE, "").replace(INLINE_CODE_RE, "$1").trim();
|
|
29
35
|
}
|
|
30
36
|
function extractTitle(content) {
|
|
31
|
-
const
|
|
37
|
+
const fm = parseFrontmatter(content);
|
|
32
38
|
if (fm.title) return fm.title;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const body = stripFrontmatter(content);
|
|
40
|
+
for (const m of body.matchAll(HEADING_LINE_RE)) if (m[1] === "#") {
|
|
41
|
+
const text = cleanHeadingText(m[2]);
|
|
42
|
+
if (text) return text;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function isBlockStarter(trimmed, raw) {
|
|
47
|
+
return HEADING_START_RE.test(trimmed) || BLOCKQUOTE_START_RE.test(trimmed) || LIST_ITEM_START_RE.test(trimmed) || ORDERED_LIST_ITEM_START_RE.test(trimmed) || TABLE_ROW_START_RE.test(trimmed) || trimmed.startsWith("<") || INDENTED_CODE_START_RE.test(raw);
|
|
41
48
|
}
|
|
42
49
|
function extractDescription(content) {
|
|
43
|
-
const
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
+
const lines = stripFrontmatter(content).split("\n");
|
|
51
|
+
let inFence = false;
|
|
52
|
+
let inHtml = false;
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
const line = lines[i];
|
|
55
|
+
if (FENCE_START_RE.test(line)) {
|
|
56
|
+
inFence = !inFence;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (inFence) continue;
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (inHtml) {
|
|
62
|
+
if (!trimmed) inHtml = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (trimmed.startsWith("<")) {
|
|
66
|
+
inHtml = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!trimmed || isBlockStarter(trimmed, line)) continue;
|
|
70
|
+
let para = trimmed;
|
|
71
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
72
|
+
const next = lines[j];
|
|
73
|
+
const nextTrim = next.trim();
|
|
74
|
+
if (!nextTrim || isBlockStarter(nextTrim, next)) break;
|
|
75
|
+
para += ` ${nextTrim}`;
|
|
76
|
+
}
|
|
77
|
+
let clean = para.replace(MARKDOWN_LINK_RE, "$1").replace(MARKDOWN_FORMATTING_RE, "");
|
|
50
78
|
if (clean.length > 150) clean = `${clean.slice(0, 147)}...`;
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
return
|
|
79
|
+
return clean;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
54
82
|
}
|
|
55
83
|
function extractLinks(content) {
|
|
56
|
-
const
|
|
84
|
+
const sanitized = stripFrontmatter(content).replace(FENCED_CODE_BLOCK_RE, "").replace(INLINE_CODE_SPAN_RE, "");
|
|
57
85
|
const links = [];
|
|
58
86
|
const seen = /* @__PURE__ */ new Set();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
87
|
+
for (const m of sanitized.matchAll(LINK_RE)) {
|
|
88
|
+
const url = m[2];
|
|
89
|
+
if (!seen.has(url)) {
|
|
90
|
+
seen.add(url);
|
|
62
91
|
links.push({
|
|
63
|
-
title:
|
|
64
|
-
url
|
|
92
|
+
title: m[1],
|
|
93
|
+
url
|
|
65
94
|
});
|
|
66
95
|
}
|
|
67
|
-
}
|
|
96
|
+
}
|
|
68
97
|
return links;
|
|
69
98
|
}
|
|
70
|
-
function stripFrontmatter(content) {
|
|
71
|
-
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/);
|
|
72
|
-
return match ? content.slice(match[0].length).trim() : content;
|
|
73
|
-
}
|
|
74
99
|
export { stripFrontmatter as a, parseFrontmatter as i, extractLinks as n, extractTitle as r, extractDescription as t };
|
|
75
100
|
|
|
76
101
|
//# sourceMappingURL=markdown.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.mjs","names":[],"sources":["../../src/core/markdown.ts"],"sourcesContent":["/**\n *
|
|
1
|
+
{"version":3,"file":"markdown.mjs","names":[],"sources":["../../src/core/markdown.ts"],"sourcesContent":["/**\n * Lightweight regex-based markdown utilities.\n * Operations needed (frontmatter, title, description, links, headings) are\n * simple enough that a full AST stack would be overkill.\n */\n\nimport { yamlParseKV } from './yaml.ts'\n\nexport interface MdLink {\n title: string\n url: string\n}\n\nexport interface Heading {\n depth: number\n text: string\n}\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?/\nconst HEADING_LINE_RE = /^(#{1,6})[ \\t]+([^ \\t\\r\\n][^\\r\\n]*)$/gm\nconst ANCHOR_RE = /\\s*\\{#[^}]+\\}\\s*$/\nconst BACKSLASH_PREFIX_RE = /^\\\\+\\s*/\nconst INLINE_CODE_RE = /`([^`]+)`/g\nconst LINK_RE = /(?<!!)\\[([^\\]]+)\\]\\(([^)\\s]+)(?:\\s+\"[^\"]*\")?\\)/g\nconst HEADING_START_RE = /^#{1,6}\\s/\nconst BLOCKQUOTE_START_RE = /^>\\s?/\nconst LIST_ITEM_START_RE = /^[-*+]\\s/\nconst ORDERED_LIST_ITEM_START_RE = /^\\d+\\.\\s/\nconst TABLE_ROW_START_RE = /^\\|/\nconst INDENTED_CODE_START_RE = /^ {4}/\nconst FENCE_START_RE = /^\\s*```/\nconst MARKDOWN_LINK_RE = /\\[([^\\]]+)\\]\\([^)]+\\)/g\nconst MARKDOWN_FORMATTING_RE = /[`*_~]/g\nconst FENCED_CODE_BLOCK_RE = /```[\\s\\S]*?```/g\nconst INLINE_CODE_SPAN_RE = /`[^`\\n]*`/g\n\n/** Strip frontmatter block, return body only. */\nexport function stripFrontmatter(content: string): string {\n const m = content.match(FRONTMATTER_RE)\n return m ? content.slice(m[0].length).trim() : content\n}\n\n/** Extract frontmatter key-value pairs. */\nexport function parseFrontmatter(content: string): Record<string, string> {\n const m = content.match(FRONTMATTER_RE)\n if (!m)\n return {}\n const fm: Record<string, string> = {}\n for (const line of m[1]!.split('\\n')) {\n const kv = yamlParseKV(line)\n if (kv)\n fm[kv[0]] = kv[1]\n }\n return fm\n}\n\nfunction cleanHeadingText(raw: string): string {\n return raw\n .replace(ANCHOR_RE, '')\n .replace(BACKSLASH_PREFIX_RE, '')\n .replace(INLINE_CODE_RE, '$1')\n .trim()\n}\n\n/** Extract all headings in document order. */\nexport function extractHeadings(content: string): Heading[] {\n const body = stripFrontmatter(content)\n const headings: Heading[] = []\n for (const m of body.matchAll(HEADING_LINE_RE)) {\n const text = cleanHeadingText(m[2]!)\n if (text)\n headings.push({ depth: m[1]!.length, text })\n }\n return headings\n}\n\n/** Extract title: frontmatter title > first h1 > null. */\nexport function extractTitle(content: string): string | null {\n const fm = parseFrontmatter(content)\n if (fm.title)\n return fm.title\n const body = stripFrontmatter(content)\n for (const m of body.matchAll(HEADING_LINE_RE)) {\n if (m[1] === '#') {\n const text = cleanHeadingText(m[2]!)\n if (text)\n return text\n }\n }\n return null\n}\n\nfunction isBlockStarter(trimmed: string, raw: string): boolean {\n return HEADING_START_RE.test(trimmed)\n || BLOCKQUOTE_START_RE.test(trimmed)\n || LIST_ITEM_START_RE.test(trimmed)\n || ORDERED_LIST_ITEM_START_RE.test(trimmed)\n || TABLE_ROW_START_RE.test(trimmed)\n || trimmed.startsWith('<')\n || INDENTED_CODE_START_RE.test(raw)\n}\n\n/** Extract first top-level paragraph, stripped of formatting, max 150 chars. */\nexport function extractDescription(content: string): string | null {\n const body = stripFrontmatter(content)\n const lines = body.split('\\n')\n let inFence = false\n let inHtml = false\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n if (FENCE_START_RE.test(line)) {\n inFence = !inFence\n continue\n }\n if (inFence)\n continue\n\n const trimmed = line.trim()\n\n if (inHtml) {\n if (!trimmed)\n inHtml = false\n continue\n }\n if (trimmed.startsWith('<')) {\n inHtml = true\n continue\n }\n\n if (!trimmed || isBlockStarter(trimmed, line))\n continue\n\n let para = trimmed\n for (let j = i + 1; j < lines.length; j++) {\n const next = lines[j]!\n const nextTrim = next.trim()\n if (!nextTrim || isBlockStarter(nextTrim, next))\n break\n para += ` ${nextTrim}`\n }\n\n let clean = para.replace(MARKDOWN_LINK_RE, '$1').replace(MARKDOWN_FORMATTING_RE, '')\n if (clean.length > 150)\n clean = `${clean.slice(0, 147)}...`\n return clean\n }\n\n return null\n}\n\n/** Extract all links (deduped by url), excluding images and links in code. */\nexport function extractLinks(content: string): MdLink[] {\n const body = stripFrontmatter(content)\n const sanitized = body\n .replace(FENCED_CODE_BLOCK_RE, '')\n .replace(INLINE_CODE_SPAN_RE, '')\n\n const links: MdLink[] = []\n const seen = new Set<string>()\n for (const m of sanitized.matchAll(LINK_RE)) {\n const url = m[2]!\n if (!seen.has(url)) {\n seen.add(url)\n links.push({ title: m[1]!, url })\n }\n }\n return links\n}\n"],"mappings":";;;;;AAkBA,MAAM,iBAAiB;AACvB,MAAM,UAAA;AACN,MAAM,mBAAY;AAClB,MAAM,sBAAsB;AAC5B,MAAM,qBAAiB;AACvB,MAAM,6BAAU;AAChB,MAAM,qBAAmB;AACzB,MAAM,yBAAsB;AAC5B,MAAM,iBAAA;AACN,MAAM,mBAAA;AACN,MAAM,yBAAqB;AAC3B,MAAM,uBAAA;AACN,MAAM,sBAAiB;AAEvB,SAAM,iBAAA,SAAyB;CAC/B,MAAM,IAAA,QAAA,MAAA,eAAuB;CAC7B,OAAM,IAAA,QAAA,MAAA,EAAsB,GAAA,OAAA,CAAA,MAAA,GAAA;;SAIpB,iBAAkB,SAAA;CACxB,MAAA,IAAO,QAAI,MAAQ,eAAmB;;;CAIxC,KAAA,MAAgB,QAAA,EAAA,GAAA,MAAiB,KAAyC,EAAA;EACxE,MAAM,KAAI,YAAc,KAAA;EACxB,IAAK,IACH,GAAA,GAAO,MAAE,GAAA;;CAEX,OAAK;;SAEC,iBACa,KAAA;;;AAKrB,SAAS,aAAA,SAAsC;CAC7C,MAAA,KACG,iBAAQ,QACR;;;CAkBL,KAAA,MAAgB,KAAA,KAAa,SAAgC,gBAAA,EAAA,IAAA,EAAA,OAAA,KAAA;EAC3D,MAAM,OAAK,iBAAiB,EAAA,GAAQ;EACpC,IAAI,MAAG,OACL;;CAEF,OAAK;;SAGG,eACK,SAAA,KAAA;;;AAMf,SAAS,mBAAe,SAAiB;CACvC,MAAA,QAAO,iBAAsB,QAAQ,CAAA,MAChC,KAAA;;;CASP,KAAA,IAAgB,IAAA,GAAA,IAAA,MAAA,QAAmB,KAAgC;EAEjE,MAAM,OADO,MAAA;EAEb,IAAI,eAAU,KAAA,KAAA,EAAA;GACd,UAAI,CAAS;GAEb;;EAEE,IAAI,SAAA;QACF,UAAW,KAAA,MAAA;MACX,QAAA;;GAEF;;EAKA,IAAI,QAAQ,WAAA,IAAA,EAAA;GACV,SAAK;GAEL;;EAEF,IAAI,CAAA,WAAQ,eAAiB,SAAA,KAAA,EAAA;MAC3B,OAAS;OACT,IAAA,IAAA,IAAA,GAAA,IAAA,MAAA,QAAA,KAAA;;GAGF,MAAK,WAAW,KAAA,MAAA;GAGhB,IAAI,CAAA,YAAO,eAAA,UAAA,KAAA,EAAA;GACX,QAAS,IAAI;;MAEX,QAAM,KAAA,QAAgB,kBAAM,KAAA,CAAA,QAAA,wBAAA,GAAA;MAC5B,MAAK,SAAY,KAAA,QAAA,GAAe,MAAA,MAAU,GACxC,IAAA,CAAA;SACF;;QAGE;;;CAMN,MAAA,YAAO,iBAAA,QAAA,CAAA,QAAA,sBAAA,GAAA,CAAA,QAAA,qBAAA,GAAA;;;CAIT,KAAA,MAAgB,KAAA,UAAa,SAA2B,QAAA,EAAA;EAEtD,MAAM,MAAA,EAAA;EAIN,IAAA,CAAM,KAAA,IAAkB,IAAE,EAAA;GAC1B,KAAM,IAAA,IAAA;GACN,MAAK,KAAM;IACT,OAAM,EAAA;IACN;IACE,CAAA;;;QAC2B"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
var MenuCancel = class extends Error {
|
|
3
|
+
name = "MenuCancel";
|
|
4
|
+
};
|
|
5
|
+
function guard(value) {
|
|
6
|
+
if (p.isCancel(value)) throw new MenuCancel();
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
async function menuLoop(opts) {
|
|
10
|
+
while (true) {
|
|
11
|
+
const options = await opts.options();
|
|
12
|
+
const initial = typeof opts.initialValue === "function" ? opts.initialValue() : opts.initialValue;
|
|
13
|
+
const choice = opts.searchable ? await p.autocomplete({
|
|
14
|
+
message: opts.message,
|
|
15
|
+
options,
|
|
16
|
+
...initial != null ? { initialValue: initial } : {}
|
|
17
|
+
}) : await p.select({
|
|
18
|
+
message: opts.message,
|
|
19
|
+
options,
|
|
20
|
+
...initial != null ? { initialValue: initial } : {}
|
|
21
|
+
});
|
|
22
|
+
if (p.isCancel(choice)) return;
|
|
23
|
+
try {
|
|
24
|
+
if (await opts.onSelect(choice)) return;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err instanceof MenuCancel) continue;
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export { menuLoop as n, guard as t };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=menu.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.mjs","names":[],"sources":["../../src/cli/menu.ts"],"sourcesContent":["import * as p from '@clack/prompts'\n\nexport class MenuCancel extends Error { override name = 'MenuCancel' }\n\nexport function guard<T>(value: T | symbol): T {\n if (p.isCancel(value))\n throw new MenuCancel()\n return value as T\n}\n\nexport interface MenuOption {\n label: string\n value: string\n hint?: string\n}\n\nexport async function menuLoop(opts: {\n message: string\n options: () => MenuOption[] | Promise<MenuOption[]>\n onSelect: (value: string) => Promise<boolean | void>\n initialValue?: string | (() => string | undefined)\n searchable?: boolean\n}): Promise<void> {\n while (true) {\n const options = await opts.options()\n const initial = typeof opts.initialValue === 'function' ? opts.initialValue() : opts.initialValue\n const choice = opts.searchable\n ? await p.autocomplete({ message: opts.message, options, ...(initial != null ? { initialValue: initial } : {}) })\n : await p.select({ message: opts.message, options, ...(initial != null ? { initialValue: initial } : {}) })\n if (p.isCancel(choice))\n return\n try {\n if (await opts.onSelect(choice as string))\n return\n }\n catch (err) {\n if (err instanceof MenuCancel)\n continue\n throw err\n }\n }\n}\n"],"mappings":";AAEA,IAAa,aAAb,cAAgC,MAAM;CAAE,OAAgB;;AAExD,SAAgB,MAAS,OAAsB;CAC7C,IAAI,EAAE,SAAS,MAAM,EACnB,MAAM,IAAI,YAAY;CACxB,OAAO;;AAST,eAAsB,SAAS,MAMb;CAChB,OAAO,MAAM;EACX,MAAM,UAAU,MAAM,KAAK,SAAS;EACpC,MAAM,UAAU,OAAO,KAAK,iBAAiB,aAAa,KAAK,cAAc,GAAG,KAAK;EACrF,MAAM,SAAS,KAAK,aAChB,MAAM,EAAE,aAAa;GAAE,SAAS,KAAK;GAAS;GAAS,GAAI,WAAW,OAAO,EAAE,cAAc,SAAS,GAAG,EAAE;GAAG,CAAC,GAC/G,MAAM,EAAE,OAAO;GAAE,SAAS,KAAK;GAAS;GAAS,GAAI,WAAW,OAAO,EAAE,cAAc,SAAS,GAAG,EAAE;GAAG,CAAC;EAC7G,IAAI,EAAE,SAAS,OAAO,EACpB;EACF,IAAI;GACF,IAAI,MAAM,KAAK,SAAS,OAAiB,EACvC;WAEG,KAAK;GACV,IAAI,eAAe,YACjB;GACF,MAAM"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
const OAUTH_NOTE = `${styleText("yellow", "⚠")} OAuth providers are disabled.\n\nConsumer subscription OAuth impersonates official CLI clients and\nviolates provider Terms of Service, risking account bans.\n\nUse API keys or native CLI tools instead:\n ${styleText("cyan", "ANTHROPIC_API_KEY")} / ${styleText("cyan", "claude")} CLI\n ${styleText("cyan", "OPENAI_API_KEY")} / ${styleText("cyan", "codex")} CLI\n ${styleText("cyan", "GEMINI_API_KEY")} / ${styleText("cyan", "gemini")} CLI`;
|
|
4
|
+
const NO_MODELS_MESSAGE = `No enhancement models detected.\n ${styleText("gray", "Skills work fine without this, you get raw docs, issues, and types.\n Enhancement compresses them into a concise cheat sheet with gotchas.")}\n\n To connect a model (optional):\n 1. Set an env var: ANTHROPIC_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY\n 2. Install a CLI tool: ${styleText("cyan", "claude")}, ${styleText("cyan", "gemini")}, or ${styleText("cyan", "codex")} (restart wizard after)`;
|
|
5
|
+
function groupModelsByProvider(models) {
|
|
6
|
+
const byVendor = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const m of models) {
|
|
8
|
+
const key = m.vendorGroup ?? m.provider;
|
|
9
|
+
if (!byVendor.has(key)) byVendor.set(key, {
|
|
10
|
+
name: key,
|
|
11
|
+
models: []
|
|
12
|
+
});
|
|
13
|
+
byVendor.get(key).models.push(m);
|
|
14
|
+
}
|
|
15
|
+
return byVendor;
|
|
16
|
+
}
|
|
17
|
+
async function pickModel(models, opts = {}) {
|
|
18
|
+
const byProvider = groupModelsByProvider(models);
|
|
19
|
+
const before = opts.before ?? [];
|
|
20
|
+
const after = opts.after ?? [];
|
|
21
|
+
if (byProvider.size === 1 && before.length === 0) {
|
|
22
|
+
const [, group] = [...byProvider.entries()][0];
|
|
23
|
+
const choice = await p.select({
|
|
24
|
+
message: `${group.name}`,
|
|
25
|
+
options: [...group.models.map((m) => ({
|
|
26
|
+
label: m.recommended ? `${m.name} (recommended - fast and cheap)` : m.name,
|
|
27
|
+
value: m.id,
|
|
28
|
+
hint: m.hint
|
|
29
|
+
})), ...after]
|
|
30
|
+
});
|
|
31
|
+
return p.isCancel(choice) ? null : choice;
|
|
32
|
+
}
|
|
33
|
+
const providerChoice = await p.select({
|
|
34
|
+
message: "Select provider",
|
|
35
|
+
options: [
|
|
36
|
+
...before,
|
|
37
|
+
...Array.from(byProvider.entries(), ([key, { name, models: ms }]) => ({
|
|
38
|
+
label: name,
|
|
39
|
+
value: key,
|
|
40
|
+
hint: `${ms.length} models`
|
|
41
|
+
})),
|
|
42
|
+
...after
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
if (p.isCancel(providerChoice)) return null;
|
|
46
|
+
const providerStr = providerChoice;
|
|
47
|
+
if (before.some((o) => o.value === providerStr) || after.some((o) => o.value === providerStr)) return providerStr;
|
|
48
|
+
const group = byProvider.get(providerStr);
|
|
49
|
+
const modelChoice = await p.select({
|
|
50
|
+
message: `Select model (${group.name})`,
|
|
51
|
+
options: group.models.map((m) => ({
|
|
52
|
+
label: m.recommended ? `${m.name} (recommended - fast and cheap)` : m.name,
|
|
53
|
+
value: m.id,
|
|
54
|
+
hint: m.hint
|
|
55
|
+
}))
|
|
56
|
+
});
|
|
57
|
+
return p.isCancel(modelChoice) ? null : modelChoice;
|
|
58
|
+
}
|
|
59
|
+
export { OAUTH_NOTE as n, pickModel as r, NO_MODELS_MESSAGE as t };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=model-picker.mjs.map
|