@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.
Files changed (3) hide show
  1. package/README.md +23 -3
  2. package/dist/index.js +689 -96
  3. 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 password, handles MFA challenges with a TOTP or recovery code prompt, and stores the returned session token by normalized API URL. Token resolution is `--token`, then `MYSKILLS_TOKEN`, then the stored login token. The default token store writes `tokens.json` under `MYSKILLS_CONFIG_DIR`, `MYSKILLS_TOKEN_FILE`, or the user config directory with user-only file permissions. `logout` revokes stored session tokens and clears the local entry; stored API tokens are removed locally and must be revoked with `token revoke`.
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
- `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. Platform keychain storage, browser login, platform-specific install adapters, and archive creation are still planned.
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(path4, options2, callback) {
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(path4, "r", function(err, fd) {
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, path4) {
2043
- if (!path4)
2042
+ function getElementAtPath(obj, path5) {
2043
+ if (!path5)
2044
2044
  return obj;
2045
- return path4.reduce((acc, key) => acc?.[key], obj);
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(path4, issues) {
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(path4);
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, path4 = []) => {
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 }, [...path4, ...issue2.path]));
2608
+ issue2.errors.map((issues) => processError({ issues }, [...path5, ...issue2.path]));
2609
2609
  } else if (issue2.code === "invalid_key") {
2610
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2610
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
2611
2611
  } else if (issue2.code === "invalid_element") {
2612
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2612
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
2613
2613
  } else {
2614
- const fullpath = [...path4, ...issue2.path];
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, path4 = []) => {
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 }, [...path4, ...issue2.path]));
2645
+ issue2.errors.map((issues) => processError({ issues }, [...path5, ...issue2.path]));
2646
2646
  } else if (issue2.code === "invalid_key") {
2647
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2647
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
2648
2648
  } else if (issue2.code === "invalid_element") {
2649
- processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2649
+ processError({ issues: issue2.issues }, [...path5, ...issue2.path]);
2650
2650
  } else {
2651
- const fullpath = [...path4, ...issue2.path];
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 path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2684
- for (const seg of path4) {
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 path4 = ref.slice(1).split("/").filter(Boolean);
15377
- if (path4.length === 0) {
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 (path4[0] === defsKey) {
15382
- const key = path4[1];
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.0";
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
- 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
+ }
16383
16394
  return error51.exitCode;
16384
16395
  }
16385
- 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
+ }
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
- if (!runtime.tokenStore) {
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
- const apiUrl = apiBaseUrl(parsed, runtime);
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
- const response = await runtime.fetch(`${baseUrl}${pathname}`, { headers });
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 runtime.fetch(`${baseUrl}${pathname}`, { headers });
17412
- const text = await response.text();
17578
+ const response = await apiFetch(pathname, parsed, runtime, { headers });
17413
17579
  if (!response.ok) {
17414
- const error51 = parseApiError(text);
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
- const response = await runtime.fetch(`${baseUrl}${pathname}`, {
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
- const response = await runtime.fetch(`${baseUrl}${pathname}`, {
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
- const baseUrl = apiBaseUrl(parsed, runtime);
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
- const text = await response.text();
17470
- 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);
17471
17622
  if (!response.ok) {
17472
- const error51 = body.error;
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 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
+ }
17478
17684
  try {
17479
17685
  const body = text ? JSON.parse(text) : {};
17480
- const error51 = body.error;
17481
- return error51?.message ?? null;
17686
+ return apiErrorFromBody(pathname, baseUrl, status, body, text);
17482
17687
  } catch {
17483
- 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
+ };
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 String(parsed.options["api-url"] ?? runtime.env.MYSKILLS_API_URL ?? DEFAULT_API_URL).replace(/\/+$/, "");
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/token-store.ts
17738
- import { chmod, mkdir as mkdir2, readFile as readFile3, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
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 readPayload(filePath)).tokens[normalizeApiUrl(apiUrl)] ?? null;
18278
+ return (await readPayload2(filePath)).tokens[normalizeApiUrl(apiUrl)] ?? null;
17746
18279
  },
17747
18280
  async set(apiUrl, token) {
17748
- const payload = await readPayload(filePath);
18281
+ const payload = await readPayload2(filePath);
17749
18282
  payload.tokens[normalizeApiUrl(apiUrl)] = token;
17750
- await writePayload(filePath, payload);
18283
+ await writePayload2(filePath, payload);
17751
18284
  },
17752
18285
  async delete(apiUrl) {
17753
- const payload = await readPayload(filePath);
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 writePayload(filePath, payload);
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 path3.resolve(env.MYSKILLS_TOKEN_FILE);
18354
+ return path4.resolve(env.MYSKILLS_TOKEN_FILE);
17766
18355
  }
17767
18356
  if (env.MYSKILLS_CONFIG_DIR) {
17768
- return path3.join(path3.resolve(env.MYSKILLS_CONFIG_DIR), "tokens.json");
18357
+ return path4.join(path4.resolve(env.MYSKILLS_CONFIG_DIR), "tokens.json");
17769
18358
  }
17770
- const baseDir = env.XDG_CONFIG_HOME ? path3.join(env.XDG_CONFIG_HOME, "myskills-app") : path3.join(os.homedir(), ".config", "myskills-app");
17771
- return path3.join(baseDir, "tokens.json");
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 readPayload(filePath) {
18362
+ async function readPayload2(filePath) {
17774
18363
  try {
17775
18364
  const raw = await readFile3(filePath, "utf8");
17776
- return parsePayload(raw);
18365
+ return parsePayload2(raw);
17777
18366
  } catch (error51) {
17778
- if (isNodeError2(error51) && error51.code === "ENOENT") {
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 writePayload(filePath, payload) {
17785
- await mkdir2(path3.dirname(filePath), { recursive: true, mode: 448 });
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 parsePayload(raw) {
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 isNodeError2(error51) {
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: createFileTokenStore(process.env),
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.0",
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
  }