@jarel/myskills 0.1.0-alpha.0 → 0.1.0-alpha.2
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 +23 -3
- package/dist/index.js +689 -96
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -37,12 +37,27 @@ myskills validate --path <file-directory-or-zip>
|
|
|
37
37
|
myskills scan --path <file-directory-or-zip>
|
|
38
38
|
myskills search [query] [--api-url <url>]
|
|
39
39
|
myskills info <skill-slug> [--api-url <url>]
|
|
40
|
-
myskills login [--api-url <url>] [--email <email>]
|
|
40
|
+
myskills login [--api-url <url>] [--method <password|api-key>] [--email <email>]
|
|
41
|
+
myskills login --api-key [--api-url <url>]
|
|
41
42
|
myskills logout [--api-url <url>] [--token <token>]
|
|
42
43
|
myskills whoami [--api-url <url>] [--token <token>]
|
|
44
|
+
myskills auth status [--api-url <url>] [--token <token>]
|
|
45
|
+
myskills doctor [--api-url <url>] [--json]
|
|
46
|
+
myskills config get api-url
|
|
47
|
+
myskills config set api-url <url>
|
|
48
|
+
myskills config reset api-url
|
|
49
|
+
myskills config list
|
|
43
50
|
myskills submit --path <file-directory-or-zip> [--api-url <url>] [--token <token>]
|
|
44
51
|
myskills review submissions [--api-url <url>] [--token <token>]
|
|
45
52
|
myskills review action <submission-id> --action <approve|publish> [--reason <text>]
|
|
53
|
+
myskills teams list|skills [--api-url <url>] [--token <token>]
|
|
54
|
+
myskills teams create <team-name> [--name <team-name>] [--api-url <url>] [--token <token>]
|
|
55
|
+
myskills teams invite <team-id> --email <email> [--api-url <url>] [--token <token>]
|
|
56
|
+
myskills teams accept <invitation-id> [--api-url <url>] [--token <token>]
|
|
57
|
+
myskills sharing get <skill-slug> [--api-url <url>] [--token <token>]
|
|
58
|
+
myskills sharing set <skill-slug> --visibility <scope> [--team <team-id>] [--user <email>]
|
|
59
|
+
myskills admin sharing get [--api-url <url>] [--token <token>]
|
|
60
|
+
myskills admin sharing set [--public <true|false>] [--authenticated <true|false>] [--teams <true|false>] [--team-visibility <true|false>] [--user-visibility <true|false>]
|
|
46
61
|
myskills export <skill-slug> --version <version> --platform <platform> --output <dir>
|
|
47
62
|
myskills install <skill-slug> [--version <version>] [--platform <platform>] [--dir <install-root>]
|
|
48
63
|
myskills list [--dir <install-root>]
|
|
@@ -60,6 +75,7 @@ The first public alpha package is distributed through npm under the `alpha` tag:
|
|
|
60
75
|
```bash
|
|
61
76
|
npm install -g @jarel/myskills@alpha
|
|
62
77
|
myskills --version
|
|
78
|
+
myskills login
|
|
63
79
|
```
|
|
64
80
|
|
|
65
81
|
Update the CLI with:
|
|
@@ -68,9 +84,13 @@ Update the CLI with:
|
|
|
68
84
|
npm install -g @jarel/myskills@alpha
|
|
69
85
|
```
|
|
70
86
|
|
|
71
|
-
`validate`, `scan`, and `submit` accept a manifest file, package directory, or local `.zip` package. `login` prompts for the
|
|
87
|
+
`validate`, `scan`, and `submit` accept a manifest file, package directory, or local `.zip` package. `login` prompts for the API URL when one is not supplied; the default is the local API at `http://localhost:3001`, and custom hosted URLs can be entered manually. Successful login stores the selected API URL in local CLI config so later commands can omit `--api-url`. API URL resolution is `--api-url`, then `MYSKILLS_API_URL`, then saved config, then `http://localhost:3001`.
|
|
72
88
|
|
|
73
|
-
`
|
|
89
|
+
`login` supports an email/password session flow and an API-key flow. The email/password flow handles MFA challenges with a TOTP or recovery code prompt and stores only the verified session token. The API-key flow validates the key with `/v1/me` before storing it. Token resolution is `--token`, then `MYSKILLS_TOKEN`, then the stored login token. The default token store uses the platform credential store through `@napi-rs/keyring` and falls back to `tokens.json` with user-only file permissions when keyring storage is unavailable or `MYSKILLS_TOKEN_STORE=file`/`MYSKILLS_TOKEN_FILE` is set. `auth status` validates the current token without printing it. `logout` revokes stored session tokens and clears the local entry; stored API tokens are removed locally and must be revoked with `token revoke`.
|
|
90
|
+
|
|
91
|
+
`config get api-url`, `config set api-url <url>`, `config reset api-url`, and `config list` manage the saved API URL. `doctor` checks the CLI version, Node version, resolved API URL, `/health`, auth status, token-store backend, install-directory writability, and `/v1/capabilities`. If the CLI is pointed at the web app instead of the API, or a newer command is sent to an older server, command errors include concrete next steps and `--json` returns structured error codes.
|
|
92
|
+
|
|
93
|
+
`submit` validates and scans locally before sending package directories as normalized text entries or `.zip` packages as base64 archive uploads for server-side extraction. `export` downloads server-authorized bundle content, verifies byte size and SHA-256 against release metadata, and writes normalized package paths under the requested output directory. `install` uses the same verified bundle path, writes into `--dir`, `MYSKILLS_INSTALL_DIR`, or the user data directory, and records local state in `.myskills-app/installed.json`; `update` preserves a rollback snapshot before replacing files, and `rollback` restores the most recent snapshot. `token create` prints the plaintext API token only once and does not overwrite the stored login session. Browser/device login, platform-specific install adapters, and archive creation are still planned.
|
|
74
94
|
|
|
75
95
|
Common scopes:
|
|
76
96
|
|
package/dist/index.js
CHANGED
|
@@ -548,7 +548,7 @@ var require_yauzl = __commonJS({
|
|
|
548
548
|
exports.Entry = Entry;
|
|
549
549
|
exports.LocalFileHeader = LocalFileHeader;
|
|
550
550
|
exports.RandomAccessReader = RandomAccessReader;
|
|
551
|
-
function open(
|
|
551
|
+
function open(path5, options2, callback) {
|
|
552
552
|
if (typeof options2 === "function") {
|
|
553
553
|
callback = options2;
|
|
554
554
|
options2 = null;
|
|
@@ -560,7 +560,7 @@ var require_yauzl = __commonJS({
|
|
|
560
560
|
if (options2.validateEntrySizes == null) options2.validateEntrySizes = true;
|
|
561
561
|
if (options2.strictFileNames == null) options2.strictFileNames = false;
|
|
562
562
|
if (callback == null) callback = defaultCallback;
|
|
563
|
-
fs.open(
|
|
563
|
+
fs.open(path5, "r", function(err, fd) {
|
|
564
564
|
if (err) return callback(err);
|
|
565
565
|
fromFd(fd, options2, function(err2, zipfile) {
|
|
566
566
|
if (err2) fs.close(fd, defaultCallback);
|
|
@@ -2039,10 +2039,10 @@ function mergeDefs(...defs) {
|
|
|
2039
2039
|
function cloneDef(schema) {
|
|
2040
2040
|
return mergeDefs(schema._zod.def);
|
|
2041
2041
|
}
|
|
2042
|
-
function getElementAtPath(obj,
|
|
2043
|
-
if (!
|
|
2042
|
+
function getElementAtPath(obj, path5) {
|
|
2043
|
+
if (!path5)
|
|
2044
2044
|
return obj;
|
|
2045
|
-
return
|
|
2045
|
+
return path5.reduce((acc, key) => acc?.[key], obj);
|
|
2046
2046
|
}
|
|
2047
2047
|
function promiseAllObject(promisesObj) {
|
|
2048
2048
|
const keys = Object.keys(promisesObj);
|
|
@@ -2451,11 +2451,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
2451
2451
|
}
|
|
2452
2452
|
return false;
|
|
2453
2453
|
}
|
|
2454
|
-
function prefixIssues(
|
|
2454
|
+
function prefixIssues(path5, issues) {
|
|
2455
2455
|
return issues.map((iss) => {
|
|
2456
2456
|
var _a3;
|
|
2457
2457
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
2458
|
-
iss.path.unshift(
|
|
2458
|
+
iss.path.unshift(path5);
|
|
2459
2459
|
return iss;
|
|
2460
2460
|
});
|
|
2461
2461
|
}
|
|
@@ -2602,16 +2602,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
|
|
|
2602
2602
|
}
|
|
2603
2603
|
function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
2604
2604
|
const fieldErrors = { _errors: [] };
|
|
2605
|
-
const processError = (error52,
|
|
2605
|
+
const processError = (error52, path5 = []) => {
|
|
2606
2606
|
for (const issue2 of error52.issues) {
|
|
2607
2607
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
2608
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
2608
|
+
issue2.errors.map((issues) => processError({ issues }, [...path5, ...issue2.path]));
|
|
2609
2609
|
} else if (issue2.code === "invalid_key") {
|
|
2610
|
-
processError({ issues: issue2.issues }, [...
|
|
2610
|
+
processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
|
|
2611
2611
|
} else if (issue2.code === "invalid_element") {
|
|
2612
|
-
processError({ issues: issue2.issues }, [...
|
|
2612
|
+
processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
|
|
2613
2613
|
} else {
|
|
2614
|
-
const fullpath = [...
|
|
2614
|
+
const fullpath = [...path5, ...issue2.path];
|
|
2615
2615
|
if (fullpath.length === 0) {
|
|
2616
2616
|
fieldErrors._errors.push(mapper(issue2));
|
|
2617
2617
|
} else {
|
|
@@ -2638,17 +2638,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
2638
2638
|
}
|
|
2639
2639
|
function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
2640
2640
|
const result = { errors: [] };
|
|
2641
|
-
const processError = (error52,
|
|
2641
|
+
const processError = (error52, path5 = []) => {
|
|
2642
2642
|
var _a3, _b;
|
|
2643
2643
|
for (const issue2 of error52.issues) {
|
|
2644
2644
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
2645
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
2645
|
+
issue2.errors.map((issues) => processError({ issues }, [...path5, ...issue2.path]));
|
|
2646
2646
|
} else if (issue2.code === "invalid_key") {
|
|
2647
|
-
processError({ issues: issue2.issues }, [...
|
|
2647
|
+
processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
|
|
2648
2648
|
} else if (issue2.code === "invalid_element") {
|
|
2649
|
-
processError({ issues: issue2.issues }, [...
|
|
2649
|
+
processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
|
|
2650
2650
|
} else {
|
|
2651
|
-
const fullpath = [...
|
|
2651
|
+
const fullpath = [...path5, ...issue2.path];
|
|
2652
2652
|
if (fullpath.length === 0) {
|
|
2653
2653
|
result.errors.push(mapper(issue2));
|
|
2654
2654
|
continue;
|
|
@@ -2680,8 +2680,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
2680
2680
|
}
|
|
2681
2681
|
function toDotPath(_path) {
|
|
2682
2682
|
const segs = [];
|
|
2683
|
-
const
|
|
2684
|
-
for (const seg of
|
|
2683
|
+
const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
2684
|
+
for (const seg of path5) {
|
|
2685
2685
|
if (typeof seg === "number")
|
|
2686
2686
|
segs.push(`[${seg}]`);
|
|
2687
2687
|
else if (typeof seg === "symbol")
|
|
@@ -15373,13 +15373,13 @@ function resolveRef(ref, ctx) {
|
|
|
15373
15373
|
if (!ref.startsWith("#")) {
|
|
15374
15374
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
15375
15375
|
}
|
|
15376
|
-
const
|
|
15377
|
-
if (
|
|
15376
|
+
const path5 = ref.slice(1).split("/").filter(Boolean);
|
|
15377
|
+
if (path5.length === 0) {
|
|
15378
15378
|
return ctx.rootSchema;
|
|
15379
15379
|
}
|
|
15380
15380
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
15381
|
-
if (
|
|
15382
|
-
const key =
|
|
15381
|
+
if (path5[0] === defsKey) {
|
|
15382
|
+
const key = path5[1];
|
|
15383
15383
|
if (!key || !ctx.defs[key]) {
|
|
15384
15384
|
throw new Error(`Reference not found: ${ref}`);
|
|
15385
15385
|
}
|
|
@@ -16321,8 +16321,9 @@ function decodePackageText(buffer, relativePath) {
|
|
|
16321
16321
|
|
|
16322
16322
|
// src/cli.ts
|
|
16323
16323
|
var DEFAULT_API_URL = "http://localhost:3001";
|
|
16324
|
-
var CLI_VERSION = "0.1.0-alpha.
|
|
16324
|
+
var CLI_VERSION = "0.1.0-alpha.2";
|
|
16325
16325
|
var CLI_VISIBILITY_SCOPES = ["public", "authenticated", "organization", "team", "private", "explicit-users"];
|
|
16326
|
+
var LOGIN_AUTH_METHODS = ["password", "api-key"];
|
|
16326
16327
|
async function runCli(argv, runtime) {
|
|
16327
16328
|
const parsed = parseArgs(argv);
|
|
16328
16329
|
try {
|
|
@@ -16352,6 +16353,12 @@ async function runCli(argv, runtime) {
|
|
|
16352
16353
|
return await logoutCommand(parsed, runtime);
|
|
16353
16354
|
case "whoami":
|
|
16354
16355
|
return await whoamiCommand(parsed, runtime);
|
|
16356
|
+
case "auth":
|
|
16357
|
+
return await authCommand(parsed, runtime);
|
|
16358
|
+
case "config":
|
|
16359
|
+
return await configCommand(parsed, runtime);
|
|
16360
|
+
case "doctor":
|
|
16361
|
+
return await doctorCommand(parsed, runtime);
|
|
16355
16362
|
case "submit":
|
|
16356
16363
|
return await submitCommand(parsed, runtime);
|
|
16357
16364
|
case "review":
|
|
@@ -16379,10 +16386,19 @@ async function runCli(argv, runtime) {
|
|
|
16379
16386
|
}
|
|
16380
16387
|
} catch (error51) {
|
|
16381
16388
|
if (error51 instanceof CliError) {
|
|
16382
|
-
|
|
16389
|
+
if (parsed.options.json) {
|
|
16390
|
+
runtime.io.stderr(JSON.stringify({ error: error51.toJSON() }, null, 2));
|
|
16391
|
+
} else {
|
|
16392
|
+
runtime.io.stderr(error51.message);
|
|
16393
|
+
}
|
|
16383
16394
|
return error51.exitCode;
|
|
16384
16395
|
}
|
|
16385
|
-
|
|
16396
|
+
const message = error51 instanceof Error ? error51.message : "Unexpected CLI failure.";
|
|
16397
|
+
if (parsed.options.json) {
|
|
16398
|
+
runtime.io.stderr(JSON.stringify({ error: { code: "UNEXPECTED_CLI_FAILURE", message } }, null, 2));
|
|
16399
|
+
} else {
|
|
16400
|
+
runtime.io.stderr(message);
|
|
16401
|
+
}
|
|
16386
16402
|
return 1;
|
|
16387
16403
|
}
|
|
16388
16404
|
}
|
|
@@ -16450,23 +16466,46 @@ async function infoCommand(parsed, runtime) {
|
|
|
16450
16466
|
return 0;
|
|
16451
16467
|
}
|
|
16452
16468
|
async function loginCommand(parsed, runtime) {
|
|
16453
|
-
|
|
16469
|
+
const tokenStore = runtime.tokenStore;
|
|
16470
|
+
if (!tokenStore) {
|
|
16454
16471
|
throw new CliError("No token store is configured. Set MYSKILLS_TOKEN for one-off commands.", 1);
|
|
16455
16472
|
}
|
|
16473
|
+
const apiUrl = await loginApiUrl(parsed, runtime);
|
|
16474
|
+
parsed.options["api-url"] = apiUrl;
|
|
16475
|
+
const method = await loginAuthMethod(parsed, runtime);
|
|
16476
|
+
if (method === "api-key") {
|
|
16477
|
+
return await loginWithApiKey(parsed, runtime, apiUrl, tokenStore);
|
|
16478
|
+
}
|
|
16479
|
+
return await loginWithPassword(parsed, runtime, apiUrl, tokenStore);
|
|
16480
|
+
}
|
|
16481
|
+
async function loginWithPassword(parsed, runtime, apiUrl, tokenStore) {
|
|
16456
16482
|
const email3 = optionalStringOption(parsed, "email") ?? await promptText(runtime, "Email: ");
|
|
16457
16483
|
const password = await promptSecret(runtime, "Password: ");
|
|
16458
16484
|
const loginResponse = await apiPost("/v1/auth/login", { email: email3.trim(), password }, parsed, runtime);
|
|
16459
16485
|
const session = loginResponse.mfaRequired === true ? await completeMfaLogin(loginResponse, parsed, runtime) : authSessionFromResponse(loginResponse);
|
|
16460
|
-
|
|
16461
|
-
await runtime.tokenStore.set(apiUrl, {
|
|
16486
|
+
await tokenStore.set(apiUrl, {
|
|
16462
16487
|
kind: "session",
|
|
16463
16488
|
token: session.token,
|
|
16464
16489
|
email: session.email,
|
|
16465
16490
|
expiresAt: session.expiresAt
|
|
16466
16491
|
});
|
|
16492
|
+
await runtime.configStore?.setApiUrl(apiUrl);
|
|
16467
16493
|
runtime.io.stdout(`${session.email ?? email3.trim()} logged-in expires=${session.expiresAt}`);
|
|
16468
16494
|
return 0;
|
|
16469
16495
|
}
|
|
16496
|
+
async function loginWithApiKey(parsed, runtime, apiUrl, tokenStore) {
|
|
16497
|
+
const apiKey = await promptSecret(runtime, "API key: ");
|
|
16498
|
+
const response = await apiGet("/v1/me", parsed, runtime, apiKey);
|
|
16499
|
+
const user = response.user;
|
|
16500
|
+
await tokenStore.set(apiUrl, {
|
|
16501
|
+
kind: "api",
|
|
16502
|
+
token: apiKey,
|
|
16503
|
+
email: user.email
|
|
16504
|
+
});
|
|
16505
|
+
await runtime.configStore?.setApiUrl(apiUrl);
|
|
16506
|
+
runtime.io.stdout(`${user.email ?? "api-key"} api-key-stored`);
|
|
16507
|
+
return 0;
|
|
16508
|
+
}
|
|
16470
16509
|
async function completeMfaLogin(loginResponse, parsed, runtime) {
|
|
16471
16510
|
const challengeToken = stringFromRecord(loginResponse, "challengeToken", "API login response is missing MFA challenge token.");
|
|
16472
16511
|
const mfaValue = (await promptSecret(runtime, "MFA code or recovery code: ")).trim();
|
|
@@ -16514,6 +16553,143 @@ async function whoamiCommand(parsed, runtime) {
|
|
|
16514
16553
|
}
|
|
16515
16554
|
return 0;
|
|
16516
16555
|
}
|
|
16556
|
+
async function authCommand(parsed, runtime) {
|
|
16557
|
+
const subcommand = parsed.args[0];
|
|
16558
|
+
if (subcommand === "status") {
|
|
16559
|
+
return await authStatusCommand(parsed, runtime);
|
|
16560
|
+
}
|
|
16561
|
+
throw new CliError("Usage: myskills auth status", 2, "USAGE_ERROR");
|
|
16562
|
+
}
|
|
16563
|
+
async function authStatusCommand(parsed, runtime) {
|
|
16564
|
+
const api = apiBaseUrlResolution(parsed, runtime);
|
|
16565
|
+
const resolved = await resolveToken(parsed, runtime);
|
|
16566
|
+
if (!resolved) {
|
|
16567
|
+
const status2 = {
|
|
16568
|
+
apiUrl: api.url,
|
|
16569
|
+
apiUrlSource: api.source,
|
|
16570
|
+
status: "not_logged_in",
|
|
16571
|
+
tokenSource: "none",
|
|
16572
|
+
tokenStore: await tokenStoreInfo(runtime)
|
|
16573
|
+
};
|
|
16574
|
+
if (parsed.options.json) {
|
|
16575
|
+
runtime.io.stdout(JSON.stringify(status2, null, 2));
|
|
16576
|
+
} else {
|
|
16577
|
+
runtime.io.stdout(`API URL: ${status2.apiUrl} (${status2.apiUrlSource})`);
|
|
16578
|
+
runtime.io.stdout("Status: not logged in");
|
|
16579
|
+
runtime.io.stdout(`Token store: ${status2.tokenStore.backend}`);
|
|
16580
|
+
}
|
|
16581
|
+
return 0;
|
|
16582
|
+
}
|
|
16583
|
+
const response = await apiGet("/v1/me", parsed, runtime, resolved.value);
|
|
16584
|
+
const user = response.user;
|
|
16585
|
+
const status = {
|
|
16586
|
+
apiUrl: api.url,
|
|
16587
|
+
apiUrlSource: api.source,
|
|
16588
|
+
status: "logged_in",
|
|
16589
|
+
tokenSource: resolved.source,
|
|
16590
|
+
tokenKind: resolved.stored.kind,
|
|
16591
|
+
tokenStore: await tokenStoreInfo(runtime),
|
|
16592
|
+
user: {
|
|
16593
|
+
email: user.email,
|
|
16594
|
+
roles: user.roles,
|
|
16595
|
+
mfaVerified: user.mfaVerified
|
|
16596
|
+
},
|
|
16597
|
+
expiresAt: resolved.stored.expiresAt ?? null
|
|
16598
|
+
};
|
|
16599
|
+
if (parsed.options.json) {
|
|
16600
|
+
runtime.io.stdout(JSON.stringify(status, null, 2));
|
|
16601
|
+
} else {
|
|
16602
|
+
runtime.io.stdout(`API URL: ${status.apiUrl} (${status.apiUrlSource})`);
|
|
16603
|
+
runtime.io.stdout(`Status: logged in (${status.tokenKind}, ${status.tokenSource})`);
|
|
16604
|
+
runtime.io.stdout(`User: ${user.email}`);
|
|
16605
|
+
runtime.io.stdout(`Roles: ${user.roles.join(",") || "-"}`);
|
|
16606
|
+
runtime.io.stdout(`MFA: ${user.mfaVerified ? "verified" : "not-verified"}`);
|
|
16607
|
+
runtime.io.stdout(`Expires: ${status.expiresAt ?? "-"}`);
|
|
16608
|
+
runtime.io.stdout(`Token store: ${status.tokenStore.backend}`);
|
|
16609
|
+
}
|
|
16610
|
+
return 0;
|
|
16611
|
+
}
|
|
16612
|
+
async function configCommand(parsed, runtime) {
|
|
16613
|
+
const subcommand = parsed.args[0];
|
|
16614
|
+
const key = parsed.args[1];
|
|
16615
|
+
if (!runtime.configStore) {
|
|
16616
|
+
throw new CliError("No config store is configured.", 1, "CONFIG_STORE_UNAVAILABLE");
|
|
16617
|
+
}
|
|
16618
|
+
if (subcommand === "get" && key === "api-url") {
|
|
16619
|
+
const apiUrl = runtime.configStore.getApiUrl() ?? null;
|
|
16620
|
+
if (parsed.options.json) {
|
|
16621
|
+
runtime.io.stdout(JSON.stringify({ apiUrl }, null, 2));
|
|
16622
|
+
} else {
|
|
16623
|
+
runtime.io.stdout(apiUrl ?? "unset");
|
|
16624
|
+
}
|
|
16625
|
+
return 0;
|
|
16626
|
+
}
|
|
16627
|
+
if (subcommand === "set" && key === "api-url") {
|
|
16628
|
+
const apiUrl = parsed.args[2];
|
|
16629
|
+
if (!apiUrl) {
|
|
16630
|
+
throw new CliError("Usage: myskills config set api-url <url>", 2, "USAGE_ERROR");
|
|
16631
|
+
}
|
|
16632
|
+
await runtime.configStore.setApiUrl(normalizeApiUrlOption(apiUrl));
|
|
16633
|
+
if (parsed.options.json) {
|
|
16634
|
+
runtime.io.stdout(JSON.stringify({ apiUrl: normalizeApiUrlOption(apiUrl) }, null, 2));
|
|
16635
|
+
} else {
|
|
16636
|
+
runtime.io.stdout(`api-url=${normalizeApiUrlOption(apiUrl)}`);
|
|
16637
|
+
}
|
|
16638
|
+
return 0;
|
|
16639
|
+
}
|
|
16640
|
+
if (subcommand === "reset" && key === "api-url") {
|
|
16641
|
+
await runtime.configStore.resetApiUrl();
|
|
16642
|
+
if (parsed.options.json) {
|
|
16643
|
+
runtime.io.stdout(JSON.stringify({ apiUrl: null }, null, 2));
|
|
16644
|
+
} else {
|
|
16645
|
+
runtime.io.stdout("api-url unset");
|
|
16646
|
+
}
|
|
16647
|
+
return 0;
|
|
16648
|
+
}
|
|
16649
|
+
if (subcommand === "list") {
|
|
16650
|
+
const resolved = apiBaseUrlResolution(parsed, runtime);
|
|
16651
|
+
const saved = runtime.configStore.getApiUrl() ?? null;
|
|
16652
|
+
if (parsed.options.json) {
|
|
16653
|
+
runtime.io.stdout(JSON.stringify({ apiUrl: saved, resolvedApiUrl: resolved.url, resolvedApiUrlSource: resolved.source }, null, 2));
|
|
16654
|
+
} else {
|
|
16655
|
+
runtime.io.stdout(`api-url=${saved ?? "unset"}`);
|
|
16656
|
+
runtime.io.stdout(`resolved-api-url=${resolved.url} source=${resolved.source}`);
|
|
16657
|
+
}
|
|
16658
|
+
return 0;
|
|
16659
|
+
}
|
|
16660
|
+
throw new CliError("Usage: myskills config get api-url | config set api-url <url> | config reset api-url | config list", 2, "USAGE_ERROR");
|
|
16661
|
+
}
|
|
16662
|
+
async function doctorCommand(parsed, runtime) {
|
|
16663
|
+
const api = apiBaseUrlResolution(parsed, runtime);
|
|
16664
|
+
const checks = [];
|
|
16665
|
+
checks.push(nodeVersionCheck());
|
|
16666
|
+
checks.push({ name: "cli_version", ok: true, message: CLI_VERSION, details: { version: CLI_VERSION } });
|
|
16667
|
+
checks.push({ name: "api_url", ok: true, message: `${api.url} (${api.source})`, details: api });
|
|
16668
|
+
const health = await doctorHealthCheck(parsed, runtime);
|
|
16669
|
+
checks.push(health);
|
|
16670
|
+
const token = await resolveToken(parsed, runtime);
|
|
16671
|
+
checks.push(await doctorAuthCheck(parsed, runtime, token));
|
|
16672
|
+
checks.push(await doctorTokenStoreCheck(runtime));
|
|
16673
|
+
checks.push(await doctorInstallDirCheck(parsed, runtime));
|
|
16674
|
+
checks.push(await doctorCapabilitiesCheck(parsed, runtime));
|
|
16675
|
+
const failed = checks.filter((check2) => !check2.ok);
|
|
16676
|
+
const result = {
|
|
16677
|
+
cliVersion: CLI_VERSION,
|
|
16678
|
+
apiUrl: api.url,
|
|
16679
|
+
apiUrlSource: api.source,
|
|
16680
|
+
checks
|
|
16681
|
+
};
|
|
16682
|
+
if (parsed.options.json) {
|
|
16683
|
+
runtime.io.stdout(JSON.stringify(result, null, 2));
|
|
16684
|
+
} else {
|
|
16685
|
+
runtime.io.stdout(`MySkills CLI ${CLI_VERSION}`);
|
|
16686
|
+
runtime.io.stdout("");
|
|
16687
|
+
for (const check2 of checks) {
|
|
16688
|
+
runtime.io.stdout(`${check2.ok ? "ok" : "fail"} ${check2.name} ${check2.message}`);
|
|
16689
|
+
}
|
|
16690
|
+
}
|
|
16691
|
+
return failed.length === 0 ? 0 : 1;
|
|
16692
|
+
}
|
|
16517
16693
|
async function tokenCommand(parsed, runtime) {
|
|
16518
16694
|
const token = await tokenOption(parsed, runtime);
|
|
16519
16695
|
if (!token) {
|
|
@@ -17388,101 +17564,296 @@ function assertChildPath(root, target) {
|
|
|
17388
17564
|
}
|
|
17389
17565
|
}
|
|
17390
17566
|
async function apiGet(pathname, parsed, runtime, token) {
|
|
17391
|
-
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17392
17567
|
const headers = {};
|
|
17393
17568
|
if (token) {
|
|
17394
17569
|
headers.authorization = `Bearer ${token}`;
|
|
17395
17570
|
}
|
|
17396
|
-
|
|
17397
|
-
const text = await response.text();
|
|
17398
|
-
const body = text ? JSON.parse(text) : {};
|
|
17399
|
-
if (!response.ok) {
|
|
17400
|
-
const error51 = body.error;
|
|
17401
|
-
throw new CliError(error51?.message ?? `API request failed with ${response.status}.`, 1);
|
|
17402
|
-
}
|
|
17403
|
-
return body;
|
|
17571
|
+
return await apiJsonRequest(pathname, parsed, runtime, { headers });
|
|
17404
17572
|
}
|
|
17405
17573
|
async function apiGetText(pathname, parsed, runtime, token) {
|
|
17406
|
-
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17407
17574
|
const headers = {};
|
|
17408
17575
|
if (token) {
|
|
17409
17576
|
headers.authorization = `Bearer ${token}`;
|
|
17410
17577
|
}
|
|
17411
|
-
const response = await
|
|
17412
|
-
const text = await response.text();
|
|
17578
|
+
const response = await apiFetch(pathname, parsed, runtime, { headers });
|
|
17413
17579
|
if (!response.ok) {
|
|
17414
|
-
|
|
17415
|
-
throw new CliError(error51 ?? `API request failed with ${response.status}.`, 1);
|
|
17580
|
+
throw apiErrorFromResponse(pathname, apiBaseUrl(parsed, runtime), response.status, response.text);
|
|
17416
17581
|
}
|
|
17417
|
-
return text;
|
|
17582
|
+
return response.text;
|
|
17418
17583
|
}
|
|
17419
17584
|
async function apiPost(pathname, payload, parsed, runtime, token) {
|
|
17420
|
-
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17421
17585
|
const headers = {
|
|
17422
17586
|
"content-type": "application/json"
|
|
17423
17587
|
};
|
|
17424
17588
|
if (token) {
|
|
17425
17589
|
headers.authorization = `Bearer ${token}`;
|
|
17426
17590
|
}
|
|
17427
|
-
|
|
17591
|
+
return await apiJsonRequest(pathname, parsed, runtime, {
|
|
17428
17592
|
method: "POST",
|
|
17429
17593
|
headers,
|
|
17430
17594
|
body: JSON.stringify(payload)
|
|
17431
17595
|
});
|
|
17432
|
-
const text = await response.text();
|
|
17433
|
-
const body = text ? JSON.parse(text) : {};
|
|
17434
|
-
if (!response.ok) {
|
|
17435
|
-
const error51 = body.error;
|
|
17436
|
-
throw new CliError(error51?.message ?? `API request failed with ${response.status}.`, 1);
|
|
17437
|
-
}
|
|
17438
|
-
return body;
|
|
17439
17596
|
}
|
|
17440
17597
|
async function apiPut(pathname, payload, parsed, runtime, token) {
|
|
17441
|
-
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17442
17598
|
const headers = {
|
|
17443
17599
|
"content-type": "application/json"
|
|
17444
17600
|
};
|
|
17445
17601
|
if (token) {
|
|
17446
17602
|
headers.authorization = `Bearer ${token}`;
|
|
17447
17603
|
}
|
|
17448
|
-
|
|
17604
|
+
return await apiJsonRequest(pathname, parsed, runtime, {
|
|
17449
17605
|
method: "PUT",
|
|
17450
17606
|
headers,
|
|
17451
17607
|
body: JSON.stringify(payload)
|
|
17452
17608
|
});
|
|
17453
|
-
const text = await response.text();
|
|
17454
|
-
const body = text ? JSON.parse(text) : {};
|
|
17455
|
-
if (!response.ok) {
|
|
17456
|
-
const error51 = body.error;
|
|
17457
|
-
throw new CliError(error51?.message ?? `API request failed with ${response.status}.`, 1);
|
|
17458
|
-
}
|
|
17459
|
-
return body;
|
|
17460
17609
|
}
|
|
17461
17610
|
async function apiDelete(pathname, parsed, runtime, token) {
|
|
17462
|
-
|
|
17463
|
-
const response = await runtime.fetch(`${baseUrl}${pathname}`, {
|
|
17611
|
+
return await apiJsonRequest(pathname, parsed, runtime, {
|
|
17464
17612
|
method: "DELETE",
|
|
17465
17613
|
headers: {
|
|
17466
17614
|
authorization: `Bearer ${token}`
|
|
17467
17615
|
}
|
|
17468
17616
|
});
|
|
17469
|
-
|
|
17470
|
-
|
|
17617
|
+
}
|
|
17618
|
+
async function apiJsonRequest(pathname, parsed, runtime, init) {
|
|
17619
|
+
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17620
|
+
const response = await apiFetch(pathname, parsed, runtime, init);
|
|
17621
|
+
const body = parseJsonResponse(pathname, baseUrl, response.text);
|
|
17471
17622
|
if (!response.ok) {
|
|
17472
|
-
|
|
17473
|
-
throw new CliError(error51?.message ?? `API request failed with ${response.status}.`, 1);
|
|
17623
|
+
throw apiErrorFromBody(pathname, baseUrl, response.status, body, response.text);
|
|
17474
17624
|
}
|
|
17475
17625
|
return body;
|
|
17476
17626
|
}
|
|
17477
|
-
function
|
|
17627
|
+
async function apiFetch(pathname, parsed, runtime, init) {
|
|
17628
|
+
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17629
|
+
let response;
|
|
17630
|
+
try {
|
|
17631
|
+
response = await runtime.fetch(`${baseUrl}${pathname}`, init);
|
|
17632
|
+
} catch {
|
|
17633
|
+
throw new CliError([
|
|
17634
|
+
"Could not reach the MySkills API.",
|
|
17635
|
+
"",
|
|
17636
|
+
`API URL: ${baseUrl}`,
|
|
17637
|
+
"Check that the API is running, or use:",
|
|
17638
|
+
" myskills <command> --api-url https://myskills.sh/api"
|
|
17639
|
+
].join("\n"), 1, "API_UNREACHABLE");
|
|
17640
|
+
}
|
|
17641
|
+
return {
|
|
17642
|
+
ok: response.ok,
|
|
17643
|
+
status: response.status,
|
|
17644
|
+
text: await response.text()
|
|
17645
|
+
};
|
|
17646
|
+
}
|
|
17647
|
+
function parseJsonResponse(pathname, baseUrl, text) {
|
|
17648
|
+
if (!text) {
|
|
17649
|
+
return {};
|
|
17650
|
+
}
|
|
17651
|
+
if (/^\s*</.test(text)) {
|
|
17652
|
+
throw htmlApiError(baseUrl);
|
|
17653
|
+
}
|
|
17654
|
+
try {
|
|
17655
|
+
const body = JSON.parse(text);
|
|
17656
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
17657
|
+
throw new Error("not object");
|
|
17658
|
+
}
|
|
17659
|
+
return body;
|
|
17660
|
+
} catch {
|
|
17661
|
+
throw new CliError(`API response for ${pathname} was not valid JSON.`, 1, "API_INVALID_JSON");
|
|
17662
|
+
}
|
|
17663
|
+
}
|
|
17664
|
+
function apiErrorFromBody(pathname, baseUrl, status, body, text) {
|
|
17665
|
+
if (status === 404 && isUnsupportedEndpointBody(body, text)) {
|
|
17666
|
+
const command = unsupportedCommandForPath(pathname);
|
|
17667
|
+
if (command) {
|
|
17668
|
+
return new CliError([
|
|
17669
|
+
`This MySkills server does not support the \`${command}\` command yet.`,
|
|
17670
|
+
"",
|
|
17671
|
+
`CLI version: ${CLI_VERSION}`,
|
|
17672
|
+
`API URL: ${baseUrl}`,
|
|
17673
|
+
"Run `myskills doctor` to inspect server capabilities."
|
|
17674
|
+
].join("\n"), 1, "API_UNSUPPORTED_ENDPOINT", status);
|
|
17675
|
+
}
|
|
17676
|
+
}
|
|
17677
|
+
const error51 = body.error;
|
|
17678
|
+
return new CliError(error51?.message ?? `API request failed with ${status}.`, 1, error51?.code ?? "API_REQUEST_FAILED", status);
|
|
17679
|
+
}
|
|
17680
|
+
function apiErrorFromResponse(pathname, baseUrl, status, text) {
|
|
17681
|
+
if (/^\s*</.test(text)) {
|
|
17682
|
+
return htmlApiError(baseUrl);
|
|
17683
|
+
}
|
|
17478
17684
|
try {
|
|
17479
17685
|
const body = text ? JSON.parse(text) : {};
|
|
17480
|
-
|
|
17481
|
-
return error51?.message ?? null;
|
|
17686
|
+
return apiErrorFromBody(pathname, baseUrl, status, body, text);
|
|
17482
17687
|
} catch {
|
|
17483
|
-
return
|
|
17688
|
+
return new CliError(`API request failed with ${status}.`, 1, "API_REQUEST_FAILED", status);
|
|
17689
|
+
}
|
|
17690
|
+
}
|
|
17691
|
+
function htmlApiError(baseUrl) {
|
|
17692
|
+
return new CliError([
|
|
17693
|
+
"The API URL returned HTML instead of JSON.",
|
|
17694
|
+
"You may be pointing the CLI at the web app.",
|
|
17695
|
+
"",
|
|
17696
|
+
`Current API URL: ${baseUrl}`,
|
|
17697
|
+
"Try: myskills <command> --api-url https://myskills.sh/api"
|
|
17698
|
+
].join("\n"), 1, "API_RETURNED_HTML");
|
|
17699
|
+
}
|
|
17700
|
+
function isUnsupportedEndpointBody(body, text) {
|
|
17701
|
+
return typeof body.message === "string" && /Route .+ not found/.test(body.message) || typeof body.error === "string" && body.error === "Not Found" || /Route .+ not found/.test(text);
|
|
17702
|
+
}
|
|
17703
|
+
function unsupportedCommandForPath(pathname) {
|
|
17704
|
+
if (pathname.startsWith("/v1/teams")) {
|
|
17705
|
+
return "teams";
|
|
17706
|
+
}
|
|
17707
|
+
if (pathname.includes("/sharing") || pathname.startsWith("/v1/admin/sharing")) {
|
|
17708
|
+
return "sharing";
|
|
17709
|
+
}
|
|
17710
|
+
return null;
|
|
17711
|
+
}
|
|
17712
|
+
function nodeVersionCheck() {
|
|
17713
|
+
const version2 = process.versions.node;
|
|
17714
|
+
const major = Number.parseInt(version2.split(".")[0] ?? "0", 10);
|
|
17715
|
+
return {
|
|
17716
|
+
name: "node",
|
|
17717
|
+
ok: major >= 20,
|
|
17718
|
+
message: `v${version2} (${major >= 20 ? "satisfies >=20" : "requires >=20"})`,
|
|
17719
|
+
details: { version: version2, engine: ">=20" }
|
|
17720
|
+
};
|
|
17721
|
+
}
|
|
17722
|
+
async function doctorHealthCheck(parsed, runtime) {
|
|
17723
|
+
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17724
|
+
try {
|
|
17725
|
+
const response = await apiFetch("/health", parsed, runtime);
|
|
17726
|
+
const body = parseJsonResponse("/health", baseUrl, response.text);
|
|
17727
|
+
return {
|
|
17728
|
+
name: "api_health",
|
|
17729
|
+
ok: response.ok,
|
|
17730
|
+
message: response.ok ? "ok" : `HTTP ${response.status}`,
|
|
17731
|
+
details: { status: response.status, body }
|
|
17732
|
+
};
|
|
17733
|
+
} catch (error51) {
|
|
17734
|
+
return {
|
|
17735
|
+
name: "api_health",
|
|
17736
|
+
ok: false,
|
|
17737
|
+
message: error51 instanceof Error ? firstLine(error51.message) : "failed"
|
|
17738
|
+
};
|
|
17739
|
+
}
|
|
17740
|
+
}
|
|
17741
|
+
async function doctorAuthCheck(parsed, runtime, resolved) {
|
|
17742
|
+
if (!resolved) {
|
|
17743
|
+
return {
|
|
17744
|
+
name: "auth",
|
|
17745
|
+
ok: true,
|
|
17746
|
+
message: "not logged in",
|
|
17747
|
+
details: { status: "not_logged_in" }
|
|
17748
|
+
};
|
|
17749
|
+
}
|
|
17750
|
+
try {
|
|
17751
|
+
const response = await apiGet("/v1/me", parsed, runtime, resolved.value);
|
|
17752
|
+
const user = response.user;
|
|
17753
|
+
return {
|
|
17754
|
+
name: "auth",
|
|
17755
|
+
ok: true,
|
|
17756
|
+
message: `${user.email ?? "unknown"} (${resolved.stored.kind}, ${resolved.source})`,
|
|
17757
|
+
details: {
|
|
17758
|
+
status: "logged_in",
|
|
17759
|
+
tokenSource: resolved.source,
|
|
17760
|
+
tokenKind: resolved.stored.kind,
|
|
17761
|
+
expiresAt: resolved.stored.expiresAt ?? null,
|
|
17762
|
+
user
|
|
17763
|
+
}
|
|
17764
|
+
};
|
|
17765
|
+
} catch (error51) {
|
|
17766
|
+
return {
|
|
17767
|
+
name: "auth",
|
|
17768
|
+
ok: false,
|
|
17769
|
+
message: error51 instanceof Error ? firstLine(error51.message) : "failed"
|
|
17770
|
+
};
|
|
17771
|
+
}
|
|
17772
|
+
}
|
|
17773
|
+
async function doctorTokenStoreCheck(runtime) {
|
|
17774
|
+
const info = await tokenStoreInfo(runtime);
|
|
17775
|
+
if (info.backend === "file" && info.filePath) {
|
|
17776
|
+
const permissions = await filePermissions(info.filePath);
|
|
17777
|
+
if (permissions && permissions !== "600") {
|
|
17778
|
+
return {
|
|
17779
|
+
name: "token_store",
|
|
17780
|
+
ok: false,
|
|
17781
|
+
message: `file permissions ${permissions}; expected 600`,
|
|
17782
|
+
details: { ...info, permissions }
|
|
17783
|
+
};
|
|
17784
|
+
}
|
|
17785
|
+
return {
|
|
17786
|
+
name: "token_store",
|
|
17787
|
+
ok: true,
|
|
17788
|
+
message: permissions ? `file ${info.filePath} (${permissions})` : `file ${info.filePath} (not created)`,
|
|
17789
|
+
details: { ...info, permissions }
|
|
17790
|
+
};
|
|
17791
|
+
}
|
|
17792
|
+
return {
|
|
17793
|
+
name: "token_store",
|
|
17794
|
+
ok: true,
|
|
17795
|
+
message: info.backend,
|
|
17796
|
+
details: info
|
|
17797
|
+
};
|
|
17798
|
+
}
|
|
17799
|
+
async function doctorInstallDirCheck(parsed, runtime) {
|
|
17800
|
+
const root = installRoot(parsed, runtime);
|
|
17801
|
+
const testFile = path2.join(root, ".myskills-app", "doctor-write-test");
|
|
17802
|
+
try {
|
|
17803
|
+
await mkdir(path2.dirname(testFile), { recursive: true });
|
|
17804
|
+
await writeFile(testFile, "ok\n", "utf8");
|
|
17805
|
+
await rm(testFile, { force: true });
|
|
17806
|
+
return {
|
|
17807
|
+
name: "install_dir",
|
|
17808
|
+
ok: true,
|
|
17809
|
+
message: `writable ${root}`,
|
|
17810
|
+
details: { path: root }
|
|
17811
|
+
};
|
|
17812
|
+
} catch (error51) {
|
|
17813
|
+
return {
|
|
17814
|
+
name: "install_dir",
|
|
17815
|
+
ok: false,
|
|
17816
|
+
message: error51 instanceof Error ? firstLine(error51.message) : "not writable",
|
|
17817
|
+
details: { path: root }
|
|
17818
|
+
};
|
|
17819
|
+
}
|
|
17820
|
+
}
|
|
17821
|
+
async function doctorCapabilitiesCheck(parsed, runtime) {
|
|
17822
|
+
try {
|
|
17823
|
+
const response = await apiGet("/v1/capabilities", parsed, runtime);
|
|
17824
|
+
const capabilities = response.capabilities && typeof response.capabilities === "object" && !Array.isArray(response.capabilities) ? response.capabilities : {};
|
|
17825
|
+
const supported = Object.entries(capabilities).filter(([, value]) => value === true).map(([key]) => key);
|
|
17826
|
+
const unsupported = Object.entries(capabilities).filter(([, value]) => value === false).map(([key]) => key);
|
|
17827
|
+
return {
|
|
17828
|
+
name: "capabilities",
|
|
17829
|
+
ok: true,
|
|
17830
|
+
message: `supported=${supported.join(",") || "-"} unsupported=${unsupported.join(",") || "-"}`,
|
|
17831
|
+
details: response
|
|
17832
|
+
};
|
|
17833
|
+
} catch (error51) {
|
|
17834
|
+
return {
|
|
17835
|
+
name: "capabilities",
|
|
17836
|
+
ok: true,
|
|
17837
|
+
message: `unknown (${error51 instanceof Error ? firstLine(error51.message) : "not available"})`
|
|
17838
|
+
};
|
|
17484
17839
|
}
|
|
17485
17840
|
}
|
|
17841
|
+
async function tokenStoreInfo(runtime) {
|
|
17842
|
+
return await runtime.tokenStore?.describe?.() ?? { backend: "memory" };
|
|
17843
|
+
}
|
|
17844
|
+
async function filePermissions(filePath) {
|
|
17845
|
+
try {
|
|
17846
|
+
return ((await stat(filePath)).mode & 511).toString(8).padStart(3, "0");
|
|
17847
|
+
} catch (error51) {
|
|
17848
|
+
if (isNodeError(error51) && error51.code === "ENOENT") {
|
|
17849
|
+
return null;
|
|
17850
|
+
}
|
|
17851
|
+
throw error51;
|
|
17852
|
+
}
|
|
17853
|
+
}
|
|
17854
|
+
function firstLine(message) {
|
|
17855
|
+
return message.split("\n")[0] ?? message;
|
|
17856
|
+
}
|
|
17486
17857
|
function printScanResult(result, io) {
|
|
17487
17858
|
if (result.findings.length === 0) {
|
|
17488
17859
|
io.stdout(`clean files=${result.filesScanned} bytes=${result.bytesScanned}`);
|
|
@@ -17499,8 +17870,22 @@ function requiredPath(parsed) {
|
|
|
17499
17870
|
}
|
|
17500
17871
|
return value;
|
|
17501
17872
|
}
|
|
17873
|
+
function apiBaseUrlResolution(parsed, runtime) {
|
|
17874
|
+
const optionValue = optionalStringOption(parsed, "api-url");
|
|
17875
|
+
if (optionValue) {
|
|
17876
|
+
return { url: normalizeApiUrlOption(optionValue), source: "option" };
|
|
17877
|
+
}
|
|
17878
|
+
if (runtime.env.MYSKILLS_API_URL) {
|
|
17879
|
+
return { url: normalizeApiUrlOption(runtime.env.MYSKILLS_API_URL), source: "env" };
|
|
17880
|
+
}
|
|
17881
|
+
const configuredApiUrl = runtime.configStore?.getApiUrl();
|
|
17882
|
+
if (configuredApiUrl) {
|
|
17883
|
+
return { url: normalizeApiUrlOption(configuredApiUrl), source: "config" };
|
|
17884
|
+
}
|
|
17885
|
+
return { url: DEFAULT_API_URL, source: "default" };
|
|
17886
|
+
}
|
|
17502
17887
|
function apiBaseUrl(parsed, runtime) {
|
|
17503
|
-
return
|
|
17888
|
+
return apiBaseUrlResolution(parsed, runtime).url;
|
|
17504
17889
|
}
|
|
17505
17890
|
async function tokenOption(parsed, runtime) {
|
|
17506
17891
|
return (await resolveToken(parsed, runtime))?.value ?? null;
|
|
@@ -17558,6 +17943,60 @@ async function promptSecret(runtime, label) {
|
|
|
17558
17943
|
}
|
|
17559
17944
|
return value;
|
|
17560
17945
|
}
|
|
17946
|
+
async function promptOptionalText(runtime, label) {
|
|
17947
|
+
if (!runtime.prompt) {
|
|
17948
|
+
throw new CliError("Interactive input is unavailable. Set MYSKILLS_TOKEN for one-off commands.", 1);
|
|
17949
|
+
}
|
|
17950
|
+
return (await runtime.prompt.text(label)).trim();
|
|
17951
|
+
}
|
|
17952
|
+
async function loginApiUrl(parsed, runtime) {
|
|
17953
|
+
const resolved = apiBaseUrlResolution(parsed, runtime);
|
|
17954
|
+
if (resolved.source === "option" || resolved.source === "env" || !runtime.prompt) {
|
|
17955
|
+
return resolved.url;
|
|
17956
|
+
}
|
|
17957
|
+
const defaultUrl = resolved.source === "config" ? resolved.url : DEFAULT_API_URL;
|
|
17958
|
+
const input = await promptOptionalText(runtime, `API URL [${defaultUrl}]: `);
|
|
17959
|
+
return input ? normalizeApiUrlOption(input) : defaultUrl;
|
|
17960
|
+
}
|
|
17961
|
+
async function loginAuthMethod(parsed, runtime) {
|
|
17962
|
+
if (parsed.options["api-key"] === true) {
|
|
17963
|
+
return "api-key";
|
|
17964
|
+
}
|
|
17965
|
+
const methodOption = optionalStringOption(parsed, "auth-method") ?? optionalStringOption(parsed, "method");
|
|
17966
|
+
if (methodOption) {
|
|
17967
|
+
return parseLoginAuthMethod(methodOption);
|
|
17968
|
+
}
|
|
17969
|
+
if (optionalStringOption(parsed, "email") || !runtime.prompt) {
|
|
17970
|
+
return "password";
|
|
17971
|
+
}
|
|
17972
|
+
const input = await promptOptionalText(runtime, "Authentication method [password] (password/api-key): ");
|
|
17973
|
+
return input ? parseLoginAuthMethod(input) : "password";
|
|
17974
|
+
}
|
|
17975
|
+
function parseLoginAuthMethod(input) {
|
|
17976
|
+
const normalized = input.trim().toLowerCase();
|
|
17977
|
+
if (normalized === "password" || normalized === "email" || normalized === "user" || normalized === "username") {
|
|
17978
|
+
return "password";
|
|
17979
|
+
}
|
|
17980
|
+
if (normalized === "api-key" || normalized === "apikey" || normalized === "api" || normalized === "key") {
|
|
17981
|
+
return "api-key";
|
|
17982
|
+
}
|
|
17983
|
+
if (normalized === "browser" || normalized === "web") {
|
|
17984
|
+
throw new CliError("Browser login is not available in this CLI/API version yet. Choose password or api-key.", 2);
|
|
17985
|
+
}
|
|
17986
|
+
throw new CliError(`Authentication method must be one of: ${LOGIN_AUTH_METHODS.join(", ")}.`, 2);
|
|
17987
|
+
}
|
|
17988
|
+
function normalizeApiUrlOption(input) {
|
|
17989
|
+
const trimmed = input.trim().replace(/\/+$/, "");
|
|
17990
|
+
try {
|
|
17991
|
+
const url2 = new URL(trimmed);
|
|
17992
|
+
if (url2.protocol !== "http:" && url2.protocol !== "https:") {
|
|
17993
|
+
throw new Error("unsupported protocol");
|
|
17994
|
+
}
|
|
17995
|
+
return trimmed;
|
|
17996
|
+
} catch {
|
|
17997
|
+
throw new CliError("API URL must be a valid http:// or https:// URL.", 2);
|
|
17998
|
+
}
|
|
17999
|
+
}
|
|
17561
18000
|
function authSessionFromResponse(response) {
|
|
17562
18001
|
const user = response.user;
|
|
17563
18002
|
let email3;
|
|
@@ -17666,7 +18105,7 @@ function parseArgs(argv) {
|
|
|
17666
18105
|
continue;
|
|
17667
18106
|
}
|
|
17668
18107
|
const key = value.slice(2);
|
|
17669
|
-
if (key === "json") {
|
|
18108
|
+
if (key === "json" || key === "api-key") {
|
|
17670
18109
|
options2[key] = true;
|
|
17671
18110
|
continue;
|
|
17672
18111
|
}
|
|
@@ -17696,9 +18135,16 @@ function helpText() {
|
|
|
17696
18135
|
" scan --path <file-directory-or-zip>",
|
|
17697
18136
|
" search [query] [--api-url <url>]",
|
|
17698
18137
|
" info <skill-slug> [--api-url <url>]",
|
|
17699
|
-
" login [--api-url <url>] [--email <email>]",
|
|
18138
|
+
" login [--api-url <url>] [--method <password|api-key>] [--email <email>]",
|
|
18139
|
+
" login --api-key [--api-url <url>]",
|
|
17700
18140
|
" logout [--api-url <url>] [--token <token>]",
|
|
17701
18141
|
" whoami [--api-url <url>] [--token <token>]",
|
|
18142
|
+
" auth status [--api-url <url>] [--token <token>]",
|
|
18143
|
+
" doctor [--api-url <url>] [--json]",
|
|
18144
|
+
" config get api-url",
|
|
18145
|
+
" config set api-url <url>",
|
|
18146
|
+
" config reset api-url",
|
|
18147
|
+
" config list",
|
|
17702
18148
|
" submit --path <file-directory-or-zip> [--api-url <url>] [--token <token>]",
|
|
17703
18149
|
" review submissions [--api-url <url>] [--token <token>]",
|
|
17704
18150
|
" review action <submission-id> --action <approve|publish> [--reason <text>]",
|
|
@@ -17722,72 +18168,215 @@ function helpText() {
|
|
|
17722
18168
|
"Options:",
|
|
17723
18169
|
" --version Print CLI version.",
|
|
17724
18170
|
" --json Print machine-readable JSON.",
|
|
17725
|
-
" --api-url <url> API base URL. Defaults to MYSKILLS_API_URL or http://localhost:3001.",
|
|
18171
|
+
" --api-url <url> API base URL. Defaults to MYSKILLS_API_URL, saved config, or http://localhost:3001.",
|
|
17726
18172
|
" --token <token> Bearer token. Defaults to MYSKILLS_TOKEN, then stored login token."
|
|
17727
18173
|
].join("\n");
|
|
17728
18174
|
}
|
|
17729
18175
|
var CliError = class extends Error {
|
|
17730
|
-
constructor(message, exitCode) {
|
|
18176
|
+
constructor(message, exitCode, code = "CLI_ERROR", status) {
|
|
17731
18177
|
super(message);
|
|
17732
18178
|
this.exitCode = exitCode;
|
|
18179
|
+
this.code = code;
|
|
18180
|
+
this.status = status;
|
|
17733
18181
|
}
|
|
17734
18182
|
exitCode;
|
|
18183
|
+
code;
|
|
18184
|
+
status;
|
|
18185
|
+
toJSON() {
|
|
18186
|
+
return {
|
|
18187
|
+
code: this.code,
|
|
18188
|
+
message: this.message,
|
|
18189
|
+
...this.status !== void 0 ? { status: this.status } : {}
|
|
18190
|
+
};
|
|
18191
|
+
}
|
|
17735
18192
|
};
|
|
17736
18193
|
|
|
17737
|
-
// src/
|
|
17738
|
-
import {
|
|
18194
|
+
// src/config-store.ts
|
|
18195
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
17739
18196
|
import os from "node:os";
|
|
17740
18197
|
import path3 from "node:path";
|
|
18198
|
+
function createFileConfigStore(env = process.env) {
|
|
18199
|
+
const filePath = configFilePath(env);
|
|
18200
|
+
let payload = readPayload(filePath);
|
|
18201
|
+
return {
|
|
18202
|
+
getApiUrl() {
|
|
18203
|
+
return payload.apiUrl;
|
|
18204
|
+
},
|
|
18205
|
+
async setApiUrl(apiUrl) {
|
|
18206
|
+
payload = {
|
|
18207
|
+
...payload,
|
|
18208
|
+
apiUrl: apiUrl.replace(/\/+$/, "")
|
|
18209
|
+
};
|
|
18210
|
+
writePayload(filePath, payload);
|
|
18211
|
+
},
|
|
18212
|
+
async resetApiUrl() {
|
|
18213
|
+
payload = { version: 1 };
|
|
18214
|
+
rmSync(filePath, { force: true });
|
|
18215
|
+
}
|
|
18216
|
+
};
|
|
18217
|
+
}
|
|
18218
|
+
function configFilePath(env) {
|
|
18219
|
+
if (env.MYSKILLS_CONFIG_FILE) {
|
|
18220
|
+
return path3.resolve(env.MYSKILLS_CONFIG_FILE);
|
|
18221
|
+
}
|
|
18222
|
+
if (env.MYSKILLS_CONFIG_DIR) {
|
|
18223
|
+
return path3.join(path3.resolve(env.MYSKILLS_CONFIG_DIR), "config.json");
|
|
18224
|
+
}
|
|
18225
|
+
const baseDir = env.XDG_CONFIG_HOME ? path3.join(env.XDG_CONFIG_HOME, "myskills-app") : path3.join(os.homedir(), ".config", "myskills-app");
|
|
18226
|
+
return path3.join(baseDir, "config.json");
|
|
18227
|
+
}
|
|
18228
|
+
function readPayload(filePath) {
|
|
18229
|
+
try {
|
|
18230
|
+
return parsePayload(readFileSync(filePath, "utf8"));
|
|
18231
|
+
} catch (error51) {
|
|
18232
|
+
if (isNodeError2(error51) && error51.code === "ENOENT") {
|
|
18233
|
+
return { version: 1 };
|
|
18234
|
+
}
|
|
18235
|
+
throw error51;
|
|
18236
|
+
}
|
|
18237
|
+
}
|
|
18238
|
+
function writePayload(filePath, payload) {
|
|
18239
|
+
mkdirSync(path3.dirname(filePath), { recursive: true, mode: 448 });
|
|
18240
|
+
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}
|
|
18241
|
+
`, { mode: 384 });
|
|
18242
|
+
chmodSync(filePath, 384);
|
|
18243
|
+
}
|
|
18244
|
+
function parsePayload(raw) {
|
|
18245
|
+
const parsed = JSON.parse(raw);
|
|
18246
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
18247
|
+
throw new Error("Config file must contain a JSON object.");
|
|
18248
|
+
}
|
|
18249
|
+
const record2 = parsed;
|
|
18250
|
+
if (record2.version !== 1) {
|
|
18251
|
+
throw new Error("Config file has an unsupported format.");
|
|
18252
|
+
}
|
|
18253
|
+
return {
|
|
18254
|
+
version: 1,
|
|
18255
|
+
apiUrl: typeof record2.apiUrl === "string" && record2.apiUrl ? record2.apiUrl.replace(/\/+$/, "") : void 0
|
|
18256
|
+
};
|
|
18257
|
+
}
|
|
18258
|
+
function isNodeError2(error51) {
|
|
18259
|
+
return error51 instanceof Error && "code" in error51;
|
|
18260
|
+
}
|
|
18261
|
+
|
|
18262
|
+
// src/token-store.ts
|
|
18263
|
+
import { chmod, mkdir as mkdir2, readFile as readFile3, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
18264
|
+
import os2 from "node:os";
|
|
18265
|
+
import path4 from "node:path";
|
|
18266
|
+
var KEYRING_SERVICE = "ai.jarel.myskills.cli";
|
|
18267
|
+
function createTokenStore(env = process.env) {
|
|
18268
|
+
const fileStore = createFileTokenStore(env);
|
|
18269
|
+
if (env.MYSKILLS_TOKEN_STORE === "file" || env.MYSKILLS_TOKEN_FILE) {
|
|
18270
|
+
return fileStore;
|
|
18271
|
+
}
|
|
18272
|
+
return createKeyringTokenStore(fileStore);
|
|
18273
|
+
}
|
|
17741
18274
|
function createFileTokenStore(env = process.env) {
|
|
17742
18275
|
const filePath = tokenFilePath(env);
|
|
17743
18276
|
return {
|
|
17744
18277
|
async get(apiUrl) {
|
|
17745
|
-
return (await
|
|
18278
|
+
return (await readPayload2(filePath)).tokens[normalizeApiUrl(apiUrl)] ?? null;
|
|
17746
18279
|
},
|
|
17747
18280
|
async set(apiUrl, token) {
|
|
17748
|
-
const payload = await
|
|
18281
|
+
const payload = await readPayload2(filePath);
|
|
17749
18282
|
payload.tokens[normalizeApiUrl(apiUrl)] = token;
|
|
17750
|
-
await
|
|
18283
|
+
await writePayload2(filePath, payload);
|
|
17751
18284
|
},
|
|
17752
18285
|
async delete(apiUrl) {
|
|
17753
|
-
const payload = await
|
|
18286
|
+
const payload = await readPayload2(filePath);
|
|
17754
18287
|
delete payload.tokens[normalizeApiUrl(apiUrl)];
|
|
17755
18288
|
if (Object.keys(payload.tokens).length === 0) {
|
|
17756
18289
|
await rm2(filePath, { force: true });
|
|
17757
18290
|
return;
|
|
17758
18291
|
}
|
|
17759
|
-
await
|
|
18292
|
+
await writePayload2(filePath, payload);
|
|
18293
|
+
},
|
|
18294
|
+
describe() {
|
|
18295
|
+
return {
|
|
18296
|
+
backend: "file",
|
|
18297
|
+
filePath
|
|
18298
|
+
};
|
|
18299
|
+
}
|
|
18300
|
+
};
|
|
18301
|
+
}
|
|
18302
|
+
function createKeyringTokenStore(fallback) {
|
|
18303
|
+
const fallbackInfo = fallback.describe?.();
|
|
18304
|
+
return {
|
|
18305
|
+
async get(apiUrl) {
|
|
18306
|
+
const keyringToken = await readKeyringToken(apiUrl);
|
|
18307
|
+
return keyringToken ?? await fallback.get(apiUrl);
|
|
18308
|
+
},
|
|
18309
|
+
async set(apiUrl, token) {
|
|
18310
|
+
if (await writeKeyringToken(apiUrl, token)) {
|
|
18311
|
+
return;
|
|
18312
|
+
}
|
|
18313
|
+
await fallback.set(apiUrl, token);
|
|
18314
|
+
},
|
|
18315
|
+
async delete(apiUrl) {
|
|
18316
|
+
await deleteKeyringToken(apiUrl);
|
|
18317
|
+
await fallback.delete(apiUrl);
|
|
18318
|
+
},
|
|
18319
|
+
describe() {
|
|
18320
|
+
return {
|
|
18321
|
+
backend: "keyring",
|
|
18322
|
+
fallbackFilePath: fallbackInfo?.filePath
|
|
18323
|
+
};
|
|
17760
18324
|
}
|
|
17761
18325
|
};
|
|
17762
18326
|
}
|
|
18327
|
+
async function readKeyringToken(apiUrl) {
|
|
18328
|
+
try {
|
|
18329
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
18330
|
+
const raw = new Entry(KEYRING_SERVICE, keyringAccount(apiUrl)).getPassword();
|
|
18331
|
+
return raw ? parseStoredToken(JSON.parse(raw)) : null;
|
|
18332
|
+
} catch {
|
|
18333
|
+
return null;
|
|
18334
|
+
}
|
|
18335
|
+
}
|
|
18336
|
+
async function writeKeyringToken(apiUrl, token) {
|
|
18337
|
+
try {
|
|
18338
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
18339
|
+
new Entry(KEYRING_SERVICE, keyringAccount(apiUrl)).setPassword(JSON.stringify(token));
|
|
18340
|
+
return true;
|
|
18341
|
+
} catch {
|
|
18342
|
+
return false;
|
|
18343
|
+
}
|
|
18344
|
+
}
|
|
18345
|
+
async function deleteKeyringToken(apiUrl) {
|
|
18346
|
+
try {
|
|
18347
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
18348
|
+
new Entry(KEYRING_SERVICE, keyringAccount(apiUrl)).deletePassword();
|
|
18349
|
+
} catch {
|
|
18350
|
+
}
|
|
18351
|
+
}
|
|
17763
18352
|
function tokenFilePath(env) {
|
|
17764
18353
|
if (env.MYSKILLS_TOKEN_FILE) {
|
|
17765
|
-
return
|
|
18354
|
+
return path4.resolve(env.MYSKILLS_TOKEN_FILE);
|
|
17766
18355
|
}
|
|
17767
18356
|
if (env.MYSKILLS_CONFIG_DIR) {
|
|
17768
|
-
return
|
|
18357
|
+
return path4.join(path4.resolve(env.MYSKILLS_CONFIG_DIR), "tokens.json");
|
|
17769
18358
|
}
|
|
17770
|
-
const baseDir = env.XDG_CONFIG_HOME ?
|
|
17771
|
-
return
|
|
18359
|
+
const baseDir = env.XDG_CONFIG_HOME ? path4.join(env.XDG_CONFIG_HOME, "myskills-app") : path4.join(os2.homedir(), ".config", "myskills-app");
|
|
18360
|
+
return path4.join(baseDir, "tokens.json");
|
|
17772
18361
|
}
|
|
17773
|
-
async function
|
|
18362
|
+
async function readPayload2(filePath) {
|
|
17774
18363
|
try {
|
|
17775
18364
|
const raw = await readFile3(filePath, "utf8");
|
|
17776
|
-
return
|
|
18365
|
+
return parsePayload2(raw);
|
|
17777
18366
|
} catch (error51) {
|
|
17778
|
-
if (
|
|
18367
|
+
if (isNodeError3(error51) && error51.code === "ENOENT") {
|
|
17779
18368
|
return { version: 1, tokens: {} };
|
|
17780
18369
|
}
|
|
17781
18370
|
throw error51;
|
|
17782
18371
|
}
|
|
17783
18372
|
}
|
|
17784
|
-
async function
|
|
17785
|
-
await mkdir2(
|
|
18373
|
+
async function writePayload2(filePath, payload) {
|
|
18374
|
+
await mkdir2(path4.dirname(filePath), { recursive: true, mode: 448 });
|
|
17786
18375
|
await writeFile2(filePath, `${JSON.stringify(payload, null, 2)}
|
|
17787
18376
|
`, { mode: 384 });
|
|
17788
18377
|
await chmod(filePath, 384);
|
|
17789
18378
|
}
|
|
17790
|
-
function
|
|
18379
|
+
function parsePayload2(raw) {
|
|
17791
18380
|
const parsed = JSON.parse(raw);
|
|
17792
18381
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
17793
18382
|
throw new Error("Token store file must contain a JSON object.");
|
|
@@ -17823,7 +18412,10 @@ function parseStoredToken(input) {
|
|
|
17823
18412
|
function normalizeApiUrl(apiUrl) {
|
|
17824
18413
|
return apiUrl.replace(/\/+$/, "");
|
|
17825
18414
|
}
|
|
17826
|
-
function
|
|
18415
|
+
function keyringAccount(apiUrl) {
|
|
18416
|
+
return `api-url:${Buffer.from(normalizeApiUrl(apiUrl)).toString("base64url")}`;
|
|
18417
|
+
}
|
|
18418
|
+
function isNodeError3(error51) {
|
|
17827
18419
|
return error51 instanceof Error && "code" in error51;
|
|
17828
18420
|
}
|
|
17829
18421
|
|
|
@@ -17943,8 +18535,9 @@ try {
|
|
|
17943
18535
|
const exitCode = await runCli(process.argv.slice(2), {
|
|
17944
18536
|
env: process.env,
|
|
17945
18537
|
fetch: fetchImpl,
|
|
18538
|
+
configStore: createFileConfigStore(process.env),
|
|
17946
18539
|
prompt,
|
|
17947
|
-
tokenStore:
|
|
18540
|
+
tokenStore: createTokenStore(process.env),
|
|
17948
18541
|
io: {
|
|
17949
18542
|
stdout: (line) => console.log(line),
|
|
17950
18543
|
stderr: (line) => console.error(line)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarel/myskills",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"description": "Command-line client for publishing, discovering, installing, updating, and rolling back MySkills packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -44,5 +44,8 @@
|
|
|
44
44
|
"@myskills-app/skill-package": "0.1.0-alpha.0",
|
|
45
45
|
"esbuild": "^0.28.1",
|
|
46
46
|
"tsx": "^4.20.6"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@napi-rs/keyring": "^1.3.0"
|
|
47
50
|
}
|
|
48
51
|
}
|