@ahood/cli 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -9
- package/dist/commands/add.js +69 -11
- package/dist/commands/completion.js +61 -37
- package/dist/commands/diff.js +4 -1
- package/dist/commands/edit.js +3 -1
- package/dist/commands/group.js +1 -1
- package/dist/commands/init.js +90 -10
- package/dist/commands/list.js +11 -5
- package/dist/commands/login.js +63 -3
- package/dist/commands/mcp.js +6 -3
- package/dist/commands/publish.js +35 -5
- package/dist/commands/read.js +3 -1
- package/dist/commands/remove.js +29 -1
- package/dist/commands/search.js +12 -4
- package/dist/commands/share.js +5 -2
- package/dist/commands/star.js +5 -2
- package/dist/commands/unpublish.js +5 -1
- package/dist/commands/update.js +61 -9
- package/dist/commands/versions.js +3 -1
- package/dist/commands/view.js +4 -1
- package/dist/commands/whoami.js +50 -9
- package/dist/config.js +7 -2
- package/dist/help.js +274 -11
- package/dist/http.js +125 -26
- package/dist/index.js +106 -22
- package/dist/kind-info.js +40 -0
- package/dist/kinds.js +122 -0
- package/dist/lockfile.js +142 -17
- package/dist/mcp/safe-tool.js +14 -2
- package/dist/spec.js +8 -5
- package/dist/useme/SKILL.md +170 -0
- package/dist/useme.js +14 -0
- package/package.json +3 -2
package/dist/commands/update.js
CHANGED
|
@@ -2,10 +2,9 @@ import { LOCKFILE_PATH, parseOwnerSkill } from "../spec.js";
|
|
|
2
2
|
import { readLockfile } from "../lockfile.js";
|
|
3
3
|
import { add, fetchVersionMeta, readMcpConfig, updateMcpEntry } from "./add.js";
|
|
4
4
|
import { UsageError } from "../usage-error.js";
|
|
5
|
+
import { assertKind, isCliKind, KINDS, localInstalledKind, strictKind } from "../kinds.js";
|
|
5
6
|
const USAGE = "Usage: ahood skill update [<owner>/<skill> ...] [--dry-run] [--json]";
|
|
6
|
-
|
|
7
|
-
const { owner, skill } = parseOwnerSkill(ownerSlashSkill, USAGE);
|
|
8
|
-
const meta = await fetchVersionMeta(owner, skill, "latest");
|
|
7
|
+
function toPreview(ownerSlashSkill, currentVersion, meta) {
|
|
9
8
|
const upToDate = currentVersion === meta.version;
|
|
10
9
|
return {
|
|
11
10
|
skill: ownerSlashSkill,
|
|
@@ -55,7 +54,7 @@ function padRow(cells, widths) {
|
|
|
55
54
|
// Prints a side-by-side current/latest table for every previewed skill, then
|
|
56
55
|
// -- for only the skills that actually have an update available -- the
|
|
57
56
|
// changelog for the version they'd move to. Nothing here touches disk or the
|
|
58
|
-
// network beyond the read-only resolution already done
|
|
57
|
+
// network beyond the read-only resolution already done by the caller before toPreview().
|
|
59
58
|
function printDryRunTable(previews) {
|
|
60
59
|
const header = ["SKILL", "CURRENT", "LATEST", "STATUS"];
|
|
61
60
|
const rows = previews.map((p) => [
|
|
@@ -78,7 +77,30 @@ function printDryRunTable(previews) {
|
|
|
78
77
|
console.log(p.changelog_md ? p.changelog_md : "(no changelog available)");
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
|
-
|
|
80
|
+
// Kind scoping for update/outdated (ahood-cli#172). Returns true when `key`
|
|
81
|
+
// should be skipped as out of scope, throws when it must be refused, and
|
|
82
|
+
// returns false when it is in scope.
|
|
83
|
+
//
|
|
84
|
+
// explicit target, wrong kind -> refused (counts as that entry's failure)
|
|
85
|
+
// no-arg run, wrong kind -> silently out of scope: `ahood agent update`
|
|
86
|
+
// only ever considers installed agents
|
|
87
|
+
// no-arg run, kind unknown -> skipped with a warning rather than guessed
|
|
88
|
+
// into (or out of) the run
|
|
89
|
+
function outOfScope(scope, key, actual, implicit) {
|
|
90
|
+
const kind = strictKind(scope);
|
|
91
|
+
if (kind === undefined || actual === kind)
|
|
92
|
+
return false;
|
|
93
|
+
if (!implicit) {
|
|
94
|
+
assertKind(scope, key, actual, "update", "update");
|
|
95
|
+
return false; // unreachable: assertKind throws for every actual !== kind
|
|
96
|
+
}
|
|
97
|
+
if (!isCliKind(actual)) {
|
|
98
|
+
console.warn(`WARNING: skipping ${key} -- the registry did not report a kind ahood can match against ${KINDS[kind].plural}.`);
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
export async function update(args, scope) {
|
|
103
|
+
const kind = strictKind(scope);
|
|
82
104
|
const dryRun = args.includes("--dry-run");
|
|
83
105
|
const jsonOutput = args.includes("--json");
|
|
84
106
|
if (jsonOutput && !dryRun) {
|
|
@@ -86,9 +108,23 @@ export async function update(args) {
|
|
|
86
108
|
}
|
|
87
109
|
const targets = args.filter((a) => a !== "--dry-run" && a !== "--json");
|
|
88
110
|
const lockfile = readLockfile(LOCKFILE_PATH);
|
|
89
|
-
const
|
|
111
|
+
const implicit = targets.length === 0;
|
|
112
|
+
let skillKeys = implicit ? Object.keys(lockfile) : targets;
|
|
113
|
+
if (implicit && kind) {
|
|
114
|
+
// A cheap local pre-filter so a pin this project's own files already
|
|
115
|
+
// show to be another kind costs no request (and can't fail the run).
|
|
116
|
+
// Unknown footprints stay in and are settled by the registry's answer.
|
|
117
|
+
skillKeys = skillKeys.filter((key) => {
|
|
118
|
+
const parts = key.split("/");
|
|
119
|
+
if (parts.length !== 2)
|
|
120
|
+
return true; // let the loop report the bad key as before
|
|
121
|
+
const local = localInstalledKind(parts[0], parts[1], lockfile[key]);
|
|
122
|
+
return local === undefined || local === kind;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const noneMessage = kind ? `No installed ${KINDS[kind].plural} to update.` : "No installed skills to update.";
|
|
90
126
|
if (skillKeys.length === 0) {
|
|
91
|
-
console.log(
|
|
127
|
+
console.log(noneMessage);
|
|
92
128
|
return;
|
|
93
129
|
}
|
|
94
130
|
if (dryRun) {
|
|
@@ -99,7 +135,11 @@ export async function update(args) {
|
|
|
99
135
|
const failures = [];
|
|
100
136
|
for (const key of skillKeys) {
|
|
101
137
|
try {
|
|
102
|
-
|
|
138
|
+
const { owner, skill } = parseOwnerSkill(key, USAGE);
|
|
139
|
+
const meta = await fetchVersionMeta(owner, skill, "latest");
|
|
140
|
+
if (outOfScope(scope, key, meta.kind, implicit))
|
|
141
|
+
continue;
|
|
142
|
+
previews.push(toPreview(key, lockfile[key]?.version ?? null, meta));
|
|
103
143
|
}
|
|
104
144
|
catch (error) {
|
|
105
145
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -120,6 +160,7 @@ export async function update(args) {
|
|
|
120
160
|
return;
|
|
121
161
|
}
|
|
122
162
|
const failures = [];
|
|
163
|
+
let inScope = 0;
|
|
123
164
|
for (const ownerSlashSkill of skillKeys) {
|
|
124
165
|
if (!lockfile[ownerSlashSkill]) {
|
|
125
166
|
// Reachable two ways: an explicitly-named skill that was never
|
|
@@ -141,6 +182,9 @@ export async function update(args) {
|
|
|
141
182
|
// mcp-update support) instead.
|
|
142
183
|
const { owner, skill } = parseOwnerSkill(ownerSlashSkill, USAGE);
|
|
143
184
|
const meta = await fetchVersionMeta(owner, skill, "latest");
|
|
185
|
+
if (outOfScope(scope, ownerSlashSkill, meta.kind, implicit))
|
|
186
|
+
continue;
|
|
187
|
+
inScope++;
|
|
144
188
|
if (meta.kind === "mcp") {
|
|
145
189
|
const currentEntry = lockfile[ownerSlashSkill];
|
|
146
190
|
if (currentEntry && currentEntry.version === meta.version && mcpEntryIsIntact(skill)) {
|
|
@@ -168,7 +212,12 @@ export async function update(args) {
|
|
|
168
212
|
await updateMcpEntry(owner, skill, meta, currentEntry);
|
|
169
213
|
continue;
|
|
170
214
|
}
|
|
171
|
-
|
|
215
|
+
// no @version -- resolves to latest again. A strict scope is passed on so
|
|
216
|
+
// add() re-checks the kind it resolves itself before writing anything.
|
|
217
|
+
if (kind)
|
|
218
|
+
await add([ownerSlashSkill], scope);
|
|
219
|
+
else
|
|
220
|
+
await add([ownerSlashSkill]);
|
|
172
221
|
}
|
|
173
222
|
catch (error) {
|
|
174
223
|
// One skill being removed/yanked/unreachable must not stop every other
|
|
@@ -178,6 +227,9 @@ export async function update(args) {
|
|
|
178
227
|
failures.push(ownerSlashSkill);
|
|
179
228
|
}
|
|
180
229
|
}
|
|
230
|
+
if (implicit && kind && inScope === 0 && failures.length === 0) {
|
|
231
|
+
console.log(noneMessage);
|
|
232
|
+
}
|
|
181
233
|
if (failures.length > 0) {
|
|
182
234
|
console.error(`${failures.length} of ${skillKeys.length} skill(s) failed to update: ${failures.join(", ")}`);
|
|
183
235
|
process.exitCode = 1;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { apiJson } from "../http.js";
|
|
2
2
|
import { parseOwnerSkill } from "../spec.js";
|
|
3
3
|
import { UsageError } from "../usage-error.js";
|
|
4
|
+
import { ensureRemoteKind } from "../kinds.js";
|
|
4
5
|
const USAGE = "Usage: ahood skill versions <owner>/<skill> [--json]";
|
|
5
6
|
function formatSize(bytes) {
|
|
6
7
|
if (bytes < 1024)
|
|
@@ -18,12 +19,13 @@ export async function listSkillVersions(owner, skill) {
|
|
|
18
19
|
const { versions } = await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}/versions`);
|
|
19
20
|
return versions ?? [];
|
|
20
21
|
}
|
|
21
|
-
export async function versions(args) {
|
|
22
|
+
export async function versions(args, scope) {
|
|
22
23
|
const jsonOutput = args.includes("--json");
|
|
23
24
|
const spec = args.find((a) => !a.startsWith("--"));
|
|
24
25
|
if (!spec)
|
|
25
26
|
throw new UsageError(USAGE);
|
|
26
27
|
const { owner, skill } = parseOwnerSkill(spec, USAGE);
|
|
28
|
+
await ensureRemoteKind(scope, owner, skill, "versions", "list the versions of");
|
|
27
29
|
const list = await listSkillVersions(owner, skill);
|
|
28
30
|
if (jsonOutput) {
|
|
29
31
|
console.log(JSON.stringify(list));
|
package/dist/commands/view.js
CHANGED
|
@@ -3,6 +3,7 @@ import { apiJson } from "../http.js";
|
|
|
3
3
|
import { getApiUrl } from "../config.js";
|
|
4
4
|
import { parseOwnerSkill } from "../spec.js";
|
|
5
5
|
import { UsageError } from "../usage-error.js";
|
|
6
|
+
import { assertKind, ensureRemoteKind } from "../kinds.js";
|
|
6
7
|
const USAGE = "Usage: ahood skill view <owner>/<skill> [--json] [--web]";
|
|
7
8
|
// Spawned with argv as an array (not a shell string), so a crafted owner/skill
|
|
8
9
|
// can never inject shell syntax -- and both segments are already restricted
|
|
@@ -26,7 +27,7 @@ function openBrowser(url) {
|
|
|
26
27
|
export async function viewSkill(owner, skill) {
|
|
27
28
|
return apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}`);
|
|
28
29
|
}
|
|
29
|
-
export async function view(args) {
|
|
30
|
+
export async function view(args, scope) {
|
|
30
31
|
const jsonOutput = args.includes("--json");
|
|
31
32
|
const web = args.includes("--web");
|
|
32
33
|
const spec = args.find((a) => !a.startsWith("--"));
|
|
@@ -35,10 +36,12 @@ export async function view(args) {
|
|
|
35
36
|
const { owner, skill } = parseOwnerSkill(spec, USAGE);
|
|
36
37
|
const url = `${getApiUrl()}/${owner}/${skill}`;
|
|
37
38
|
if (web) {
|
|
39
|
+
await ensureRemoteKind(scope, owner, skill, "view", "open");
|
|
38
40
|
openBrowser(url);
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
43
|
const detail = await viewSkill(owner, skill);
|
|
44
|
+
assertKind(scope, `${owner}/${skill}`, detail.kind, "view", "show");
|
|
42
45
|
if (jsonOutput) {
|
|
43
46
|
console.log(JSON.stringify(detail));
|
|
44
47
|
return;
|
package/dist/commands/whoami.js
CHANGED
|
@@ -4,14 +4,22 @@ import { resolveToken } from "../credentials.js";
|
|
|
4
4
|
// still authenticate?", which is already settled by the time this runs.
|
|
5
5
|
// A failure here (network blip, an otherwise-valid token hitting a 500 on
|
|
6
6
|
// this specific route, etc.) must never turn a successful auth check into a
|
|
7
|
-
// command failure -- so
|
|
8
|
-
// the plain "Authenticated..." message instead.
|
|
7
|
+
// command failure -- so most errors are swallowed and callers fall back to
|
|
8
|
+
// the plain "Authenticated..." message instead. The one exception is a 404,
|
|
9
|
+
// which carries a `provisioning` flag worth reading rather than discarding:
|
|
10
|
+
// see ProfileOutcome above.
|
|
9
11
|
async function fetchProfile() {
|
|
10
12
|
try {
|
|
11
|
-
|
|
13
|
+
const profile = await apiJson("/api/v1/profile");
|
|
14
|
+
return { profile };
|
|
12
15
|
}
|
|
13
|
-
catch {
|
|
14
|
-
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
18
|
+
const body = error.body;
|
|
19
|
+
const provisioning = !!body && typeof body === "object" && body.provisioning === true;
|
|
20
|
+
return { profileStatus: provisioning ? "provisioning" : "not_found" };
|
|
21
|
+
}
|
|
22
|
+
return {};
|
|
15
23
|
}
|
|
16
24
|
}
|
|
17
25
|
// Pure auth-status check: no console output, no process.exitCode -- callers
|
|
@@ -38,13 +46,13 @@ export async function checkAuth() {
|
|
|
38
46
|
await apiJson("/api/v1/auth/tokens");
|
|
39
47
|
// A session-backed caller (only reachable if this ever runs against a
|
|
40
48
|
// cookie-bearing client) -- the token list came back.
|
|
41
|
-
const
|
|
42
|
-
return { authenticated: true, mode: "session",
|
|
49
|
+
const profileOutcome = await fetchProfile();
|
|
50
|
+
return { authenticated: true, mode: "session", ...profileOutcome };
|
|
43
51
|
}
|
|
44
52
|
catch (error) {
|
|
45
53
|
if (error instanceof ApiError && error.status === 403) {
|
|
46
|
-
const
|
|
47
|
-
return { authenticated: true, mode: "token",
|
|
54
|
+
const profileOutcome = await fetchProfile();
|
|
55
|
+
return { authenticated: true, mode: "token", ...profileOutcome };
|
|
48
56
|
}
|
|
49
57
|
if (error instanceof ApiError && error.status === 401) {
|
|
50
58
|
return { authenticated: false, reason: "invalid_token" };
|
|
@@ -89,6 +97,39 @@ export async function whoami(args = []) {
|
|
|
89
97
|
process.exitCode = 1;
|
|
90
98
|
return;
|
|
91
99
|
}
|
|
100
|
+
const asToken = result.mode === "token";
|
|
101
|
+
if (result.profileStatus === "not_found") {
|
|
102
|
+
// Distinct from "provisioning" below: the server saw a 404 for this
|
|
103
|
+
// caller's profile and did NOT flag it as the expected provisioning
|
|
104
|
+
// window (ahood#340) -- authentication is genuinely fine (we got this
|
|
105
|
+
// far), but something about the account's profile is actually wrong
|
|
106
|
+
// rather than merely not-created-yet. Exit 5 ("Not found"), the same
|
|
107
|
+
// code exitCodeFor() already gives an ApiError 404 elsewhere -- this is
|
|
108
|
+
// that same class of failure, just decided locally since checkAuth()
|
|
109
|
+
// never throws.
|
|
110
|
+
if (wantsJson)
|
|
111
|
+
console.log(JSON.stringify({ authenticated: true, mode: result.mode, profileStatus: "not_found" }));
|
|
112
|
+
else {
|
|
113
|
+
console.error(`Authenticated${asToken ? " with a personal API token" : ""}, but no profile was found for this account -- ` +
|
|
114
|
+
"this is not the usual just-signed-up delay, so something is actually wrong. Try `ahood login` again, and " +
|
|
115
|
+
"if it keeps happening, contact support.");
|
|
116
|
+
}
|
|
117
|
+
process.exitCode = 5;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (result.profileStatus === "provisioning") {
|
|
121
|
+
// The expected, self-resolving window between signup and the profile
|
|
122
|
+
// row being created (ahood#336, ahood#340) -- not a failure of any kind,
|
|
123
|
+
// so this stays exit 0. "Refresh this page" (the web copy's hint) makes
|
|
124
|
+
// no sense here; re-running the command is the CLI-native equivalent.
|
|
125
|
+
if (wantsJson)
|
|
126
|
+
console.log(JSON.stringify({ authenticated: true, mode: result.mode, profileStatus: "provisioning" }));
|
|
127
|
+
else {
|
|
128
|
+
console.log(`Authenticated${asToken ? " with a personal API token" : ""}. Your account is still being set up -- ` +
|
|
129
|
+
"this finishes on its own and needs nothing from you. Run `ahood whoami` again in a bit to see your profile.");
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
92
133
|
if (wantsJson) {
|
|
93
134
|
console.log(JSON.stringify({ authenticated: true, mode: result.mode, ...result.profile }));
|
|
94
135
|
return;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { UsageError } from "./usage-error.js";
|
|
2
|
-
|
|
2
|
+
// www.ahood.dev since ahood#661: production Clerk only accepts ahood.dev
|
|
3
|
+
// origins, so web pages on the old host redirect here. The API still answers on
|
|
4
|
+
// https://ahood.vercel.app (CLIs <= 0.10.0 default to it), so an override naming
|
|
5
|
+
// that legacy host is official too and isn't warned about.
|
|
6
|
+
const DEFAULT_API_URL = "https://www.ahood.dev";
|
|
7
|
+
const LEGACY_API_URL = "https://ahood.vercel.app";
|
|
3
8
|
let warnedAboutOverride = false;
|
|
4
9
|
// AHOOD_API_URL is trusted with the bearer token on every request (see
|
|
5
10
|
// http.ts's apiFetch), so a scheme/host is validated rather than accepted
|
|
@@ -29,7 +34,7 @@ export function getApiUrl() {
|
|
|
29
34
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocalHost)) {
|
|
30
35
|
throw new UsageError(`AHOOD_API_URL must use https:// (http:// is only allowed for localhost/test hosts); got "${configured}".`);
|
|
31
36
|
}
|
|
32
|
-
if (trimmed !== DEFAULT_API_URL && !warnedAboutOverride) {
|
|
37
|
+
if (trimmed !== DEFAULT_API_URL && trimmed !== LEGACY_API_URL && !warnedAboutOverride) {
|
|
33
38
|
console.error(`warning: using non-default API endpoint from AHOOD_API_URL: ${trimmed}`);
|
|
34
39
|
warnedAboutOverride = true;
|
|
35
40
|
}
|