@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.
Files changed (3) hide show
  1. package/README.md +17 -1
  2. package/dist/index.js +429 -48
  3. 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.1";
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
- runtime.io.stderr(error51.message);
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
- runtime.io.stderr(error51 instanceof Error ? error51.message : "Unexpected CLI failure.");
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
- const response = await runtime.fetch(`${baseUrl}${pathname}`, { headers });
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 runtime.fetch(`${baseUrl}${pathname}`, { headers });
17436
- const text = await response.text();
17578
+ const response = await apiFetch(pathname, parsed, runtime, { headers });
17437
17579
  if (!response.ok) {
17438
- const error51 = parseApiError(text);
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
- const response = await runtime.fetch(`${baseUrl}${pathname}`, {
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
- const response = await runtime.fetch(`${baseUrl}${pathname}`, {
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
- const baseUrl = apiBaseUrl(parsed, runtime);
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
- const text = await response.text();
17494
- const body = text ? JSON.parse(text) : {};
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
- const error51 = body.error;
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 parseApiError(text) {
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
- const error51 = body.error;
17505
- return error51?.message ?? null;
17686
+ return apiErrorFromBody(pathname, baseUrl, status, body, text);
17506
17687
  } catch {
17507
- return null;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jarel/myskills",
3
- "version": "0.1.0-alpha.1",
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,