@jarel/myskills 0.1.0-alpha.1 → 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 +17 -1
- package/dist/index.js +429 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,9 +41,23 @@ myskills login [--api-url <url>] [--method <password|api-key>] [--email <email>]
|
|
|
41
41
|
myskills login --api-key [--api-url <url>]
|
|
42
42
|
myskills logout [--api-url <url>] [--token <token>]
|
|
43
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
|
|
44
50
|
myskills submit --path <file-directory-or-zip> [--api-url <url>] [--token <token>]
|
|
45
51
|
myskills review submissions [--api-url <url>] [--token <token>]
|
|
46
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>]
|
|
47
61
|
myskills export <skill-slug> --version <version> --platform <platform> --output <dir>
|
|
48
62
|
myskills install <skill-slug> [--version <version>] [--platform <platform>] [--dir <install-root>]
|
|
49
63
|
myskills list [--dir <install-root>]
|
|
@@ -72,7 +86,9 @@ npm install -g @jarel/myskills@alpha
|
|
|
72
86
|
|
|
73
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`.
|
|
74
88
|
|
|
75
|
-
`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. `logout` revokes stored session tokens and clears the local entry; stored API tokens are removed locally and must be revoked with `token revoke`.
|
|
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.
|
|
76
92
|
|
|
77
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.
|
|
78
94
|
|
package/dist/index.js
CHANGED
|
@@ -16321,7 +16321,7 @@ 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
16326
|
var LOGIN_AUTH_METHODS = ["password", "api-key"];
|
|
16327
16327
|
async function runCli(argv, runtime) {
|
|
@@ -16353,6 +16353,12 @@ async function runCli(argv, runtime) {
|
|
|
16353
16353
|
return await logoutCommand(parsed, runtime);
|
|
16354
16354
|
case "whoami":
|
|
16355
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);
|
|
16356
16362
|
case "submit":
|
|
16357
16363
|
return await submitCommand(parsed, runtime);
|
|
16358
16364
|
case "review":
|
|
@@ -16380,10 +16386,19 @@ async function runCli(argv, runtime) {
|
|
|
16380
16386
|
}
|
|
16381
16387
|
} catch (error51) {
|
|
16382
16388
|
if (error51 instanceof CliError) {
|
|
16383
|
-
|
|
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
|
+
}
|
|
16384
16394
|
return error51.exitCode;
|
|
16385
16395
|
}
|
|
16386
|
-
|
|
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
|
+
}
|
|
16387
16402
|
return 1;
|
|
16388
16403
|
}
|
|
16389
16404
|
}
|
|
@@ -16538,6 +16553,143 @@ async function whoamiCommand(parsed, runtime) {
|
|
|
16538
16553
|
}
|
|
16539
16554
|
return 0;
|
|
16540
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
|
+
}
|
|
16541
16693
|
async function tokenCommand(parsed, runtime) {
|
|
16542
16694
|
const token = await tokenOption(parsed, runtime);
|
|
16543
16695
|
if (!token) {
|
|
@@ -17412,101 +17564,296 @@ function assertChildPath(root, target) {
|
|
|
17412
17564
|
}
|
|
17413
17565
|
}
|
|
17414
17566
|
async function apiGet(pathname, parsed, runtime, token) {
|
|
17415
|
-
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17416
17567
|
const headers = {};
|
|
17417
17568
|
if (token) {
|
|
17418
17569
|
headers.authorization = `Bearer ${token}`;
|
|
17419
17570
|
}
|
|
17420
|
-
|
|
17421
|
-
const text = await response.text();
|
|
17422
|
-
const body = text ? JSON.parse(text) : {};
|
|
17423
|
-
if (!response.ok) {
|
|
17424
|
-
const error51 = body.error;
|
|
17425
|
-
throw new CliError(error51?.message ?? `API request failed with ${response.status}.`, 1);
|
|
17426
|
-
}
|
|
17427
|
-
return body;
|
|
17571
|
+
return await apiJsonRequest(pathname, parsed, runtime, { headers });
|
|
17428
17572
|
}
|
|
17429
17573
|
async function apiGetText(pathname, parsed, runtime, token) {
|
|
17430
|
-
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17431
17574
|
const headers = {};
|
|
17432
17575
|
if (token) {
|
|
17433
17576
|
headers.authorization = `Bearer ${token}`;
|
|
17434
17577
|
}
|
|
17435
|
-
const response = await
|
|
17436
|
-
const text = await response.text();
|
|
17578
|
+
const response = await apiFetch(pathname, parsed, runtime, { headers });
|
|
17437
17579
|
if (!response.ok) {
|
|
17438
|
-
|
|
17439
|
-
throw new CliError(error51 ?? `API request failed with ${response.status}.`, 1);
|
|
17580
|
+
throw apiErrorFromResponse(pathname, apiBaseUrl(parsed, runtime), response.status, response.text);
|
|
17440
17581
|
}
|
|
17441
|
-
return text;
|
|
17582
|
+
return response.text;
|
|
17442
17583
|
}
|
|
17443
17584
|
async function apiPost(pathname, payload, parsed, runtime, token) {
|
|
17444
|
-
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17445
17585
|
const headers = {
|
|
17446
17586
|
"content-type": "application/json"
|
|
17447
17587
|
};
|
|
17448
17588
|
if (token) {
|
|
17449
17589
|
headers.authorization = `Bearer ${token}`;
|
|
17450
17590
|
}
|
|
17451
|
-
|
|
17591
|
+
return await apiJsonRequest(pathname, parsed, runtime, {
|
|
17452
17592
|
method: "POST",
|
|
17453
17593
|
headers,
|
|
17454
17594
|
body: JSON.stringify(payload)
|
|
17455
17595
|
});
|
|
17456
|
-
const text = await response.text();
|
|
17457
|
-
const body = text ? JSON.parse(text) : {};
|
|
17458
|
-
if (!response.ok) {
|
|
17459
|
-
const error51 = body.error;
|
|
17460
|
-
throw new CliError(error51?.message ?? `API request failed with ${response.status}.`, 1);
|
|
17461
|
-
}
|
|
17462
|
-
return body;
|
|
17463
17596
|
}
|
|
17464
17597
|
async function apiPut(pathname, payload, parsed, runtime, token) {
|
|
17465
|
-
const baseUrl = apiBaseUrl(parsed, runtime);
|
|
17466
17598
|
const headers = {
|
|
17467
17599
|
"content-type": "application/json"
|
|
17468
17600
|
};
|
|
17469
17601
|
if (token) {
|
|
17470
17602
|
headers.authorization = `Bearer ${token}`;
|
|
17471
17603
|
}
|
|
17472
|
-
|
|
17604
|
+
return await apiJsonRequest(pathname, parsed, runtime, {
|
|
17473
17605
|
method: "PUT",
|
|
17474
17606
|
headers,
|
|
17475
17607
|
body: JSON.stringify(payload)
|
|
17476
17608
|
});
|
|
17477
|
-
const text = await response.text();
|
|
17478
|
-
const body = text ? JSON.parse(text) : {};
|
|
17479
|
-
if (!response.ok) {
|
|
17480
|
-
const error51 = body.error;
|
|
17481
|
-
throw new CliError(error51?.message ?? `API request failed with ${response.status}.`, 1);
|
|
17482
|
-
}
|
|
17483
|
-
return body;
|
|
17484
17609
|
}
|
|
17485
17610
|
async function apiDelete(pathname, parsed, runtime, token) {
|
|
17486
|
-
|
|
17487
|
-
const response = await runtime.fetch(`${baseUrl}${pathname}`, {
|
|
17611
|
+
return await apiJsonRequest(pathname, parsed, runtime, {
|
|
17488
17612
|
method: "DELETE",
|
|
17489
17613
|
headers: {
|
|
17490
17614
|
authorization: `Bearer ${token}`
|
|
17491
17615
|
}
|
|
17492
17616
|
});
|
|
17493
|
-
|
|
17494
|
-
|
|
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);
|
|
17495
17622
|
if (!response.ok) {
|
|
17496
|
-
|
|
17497
|
-
throw new CliError(error51?.message ?? `API request failed with ${response.status}.`, 1);
|
|
17623
|
+
throw apiErrorFromBody(pathname, baseUrl, response.status, body, response.text);
|
|
17498
17624
|
}
|
|
17499
17625
|
return body;
|
|
17500
17626
|
}
|
|
17501
|
-
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
|
+
}
|
|
17502
17684
|
try {
|
|
17503
17685
|
const body = text ? JSON.parse(text) : {};
|
|
17504
|
-
|
|
17505
|
-
return error51?.message ?? null;
|
|
17686
|
+
return apiErrorFromBody(pathname, baseUrl, status, body, text);
|
|
17506
17687
|
} catch {
|
|
17507
|
-
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
|
+
};
|
|
17508
17739
|
}
|
|
17509
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
|
+
};
|
|
17839
|
+
}
|
|
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
|
+
}
|
|
17510
17857
|
function printScanResult(result, io) {
|
|
17511
17858
|
if (result.findings.length === 0) {
|
|
17512
17859
|
io.stdout(`clean files=${result.filesScanned} bytes=${result.bytesScanned}`);
|
|
@@ -17792,6 +18139,12 @@ function helpText() {
|
|
|
17792
18139
|
" login --api-key [--api-url <url>]",
|
|
17793
18140
|
" logout [--api-url <url>] [--token <token>]",
|
|
17794
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",
|
|
17795
18148
|
" submit --path <file-directory-or-zip> [--api-url <url>] [--token <token>]",
|
|
17796
18149
|
" review submissions [--api-url <url>] [--token <token>]",
|
|
17797
18150
|
" review action <submission-id> --action <approve|publish> [--reason <text>]",
|
|
@@ -17820,15 +18173,26 @@ function helpText() {
|
|
|
17820
18173
|
].join("\n");
|
|
17821
18174
|
}
|
|
17822
18175
|
var CliError = class extends Error {
|
|
17823
|
-
constructor(message, exitCode) {
|
|
18176
|
+
constructor(message, exitCode, code = "CLI_ERROR", status) {
|
|
17824
18177
|
super(message);
|
|
17825
18178
|
this.exitCode = exitCode;
|
|
18179
|
+
this.code = code;
|
|
18180
|
+
this.status = status;
|
|
17826
18181
|
}
|
|
17827
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
|
+
}
|
|
17828
18192
|
};
|
|
17829
18193
|
|
|
17830
18194
|
// src/config-store.ts
|
|
17831
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18195
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
17832
18196
|
import os from "node:os";
|
|
17833
18197
|
import path3 from "node:path";
|
|
17834
18198
|
function createFileConfigStore(env = process.env) {
|
|
@@ -17844,6 +18208,10 @@ function createFileConfigStore(env = process.env) {
|
|
|
17844
18208
|
apiUrl: apiUrl.replace(/\/+$/, "")
|
|
17845
18209
|
};
|
|
17846
18210
|
writePayload(filePath, payload);
|
|
18211
|
+
},
|
|
18212
|
+
async resetApiUrl() {
|
|
18213
|
+
payload = { version: 1 };
|
|
18214
|
+
rmSync(filePath, { force: true });
|
|
17847
18215
|
}
|
|
17848
18216
|
};
|
|
17849
18217
|
}
|
|
@@ -17922,10 +18290,17 @@ function createFileTokenStore(env = process.env) {
|
|
|
17922
18290
|
return;
|
|
17923
18291
|
}
|
|
17924
18292
|
await writePayload2(filePath, payload);
|
|
18293
|
+
},
|
|
18294
|
+
describe() {
|
|
18295
|
+
return {
|
|
18296
|
+
backend: "file",
|
|
18297
|
+
filePath
|
|
18298
|
+
};
|
|
17925
18299
|
}
|
|
17926
18300
|
};
|
|
17927
18301
|
}
|
|
17928
18302
|
function createKeyringTokenStore(fallback) {
|
|
18303
|
+
const fallbackInfo = fallback.describe?.();
|
|
17929
18304
|
return {
|
|
17930
18305
|
async get(apiUrl) {
|
|
17931
18306
|
const keyringToken = await readKeyringToken(apiUrl);
|
|
@@ -17940,6 +18315,12 @@ function createKeyringTokenStore(fallback) {
|
|
|
17940
18315
|
async delete(apiUrl) {
|
|
17941
18316
|
await deleteKeyringToken(apiUrl);
|
|
17942
18317
|
await fallback.delete(apiUrl);
|
|
18318
|
+
},
|
|
18319
|
+
describe() {
|
|
18320
|
+
return {
|
|
18321
|
+
backend: "keyring",
|
|
18322
|
+
fallbackFilePath: fallbackInfo?.filePath
|
|
18323
|
+
};
|
|
17943
18324
|
}
|
|
17944
18325
|
};
|
|
17945
18326
|
}
|
package/package.json
CHANGED