@jarel/myskills 0.1.0-alpha.0 → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/index.js +262 -50
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -37,7 +37,8 @@ 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>]
43
44
  myskills submit --path <file-directory-or-zip> [--api-url <url>] [--token <token>]
@@ -60,6 +61,7 @@ The first public alpha package is distributed through npm under the `alpha` tag:
60
61
  ```bash
61
62
  npm install -g @jarel/myskills@alpha
62
63
  myskills --version
64
+ myskills login
63
65
  ```
64
66
 
65
67
  Update the CLI with:
@@ -68,9 +70,11 @@ Update the CLI with:
68
70
  npm install -g @jarel/myskills@alpha
69
71
  ```
70
72
 
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`.
73
+ `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
74
 
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.
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`.
76
+
77
+ `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
78
 
75
79
  Common scopes:
76
80
 
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.1";
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 {
@@ -16450,23 +16451,46 @@ async function infoCommand(parsed, runtime) {
16450
16451
  return 0;
16451
16452
  }
16452
16453
  async function loginCommand(parsed, runtime) {
16453
- if (!runtime.tokenStore) {
16454
+ const tokenStore = runtime.tokenStore;
16455
+ if (!tokenStore) {
16454
16456
  throw new CliError("No token store is configured. Set MYSKILLS_TOKEN for one-off commands.", 1);
16455
16457
  }
16458
+ const apiUrl = await loginApiUrl(parsed, runtime);
16459
+ parsed.options["api-url"] = apiUrl;
16460
+ const method = await loginAuthMethod(parsed, runtime);
16461
+ if (method === "api-key") {
16462
+ return await loginWithApiKey(parsed, runtime, apiUrl, tokenStore);
16463
+ }
16464
+ return await loginWithPassword(parsed, runtime, apiUrl, tokenStore);
16465
+ }
16466
+ async function loginWithPassword(parsed, runtime, apiUrl, tokenStore) {
16456
16467
  const email3 = optionalStringOption(parsed, "email") ?? await promptText(runtime, "Email: ");
16457
16468
  const password = await promptSecret(runtime, "Password: ");
16458
16469
  const loginResponse = await apiPost("/v1/auth/login", { email: email3.trim(), password }, parsed, runtime);
16459
16470
  const session = loginResponse.mfaRequired === true ? await completeMfaLogin(loginResponse, parsed, runtime) : authSessionFromResponse(loginResponse);
16460
- const apiUrl = apiBaseUrl(parsed, runtime);
16461
- await runtime.tokenStore.set(apiUrl, {
16471
+ await tokenStore.set(apiUrl, {
16462
16472
  kind: "session",
16463
16473
  token: session.token,
16464
16474
  email: session.email,
16465
16475
  expiresAt: session.expiresAt
16466
16476
  });
16477
+ await runtime.configStore?.setApiUrl(apiUrl);
16467
16478
  runtime.io.stdout(`${session.email ?? email3.trim()} logged-in expires=${session.expiresAt}`);
16468
16479
  return 0;
16469
16480
  }
16481
+ async function loginWithApiKey(parsed, runtime, apiUrl, tokenStore) {
16482
+ const apiKey = await promptSecret(runtime, "API key: ");
16483
+ const response = await apiGet("/v1/me", parsed, runtime, apiKey);
16484
+ const user = response.user;
16485
+ await tokenStore.set(apiUrl, {
16486
+ kind: "api",
16487
+ token: apiKey,
16488
+ email: user.email
16489
+ });
16490
+ await runtime.configStore?.setApiUrl(apiUrl);
16491
+ runtime.io.stdout(`${user.email ?? "api-key"} api-key-stored`);
16492
+ return 0;
16493
+ }
16470
16494
  async function completeMfaLogin(loginResponse, parsed, runtime) {
16471
16495
  const challengeToken = stringFromRecord(loginResponse, "challengeToken", "API login response is missing MFA challenge token.");
16472
16496
  const mfaValue = (await promptSecret(runtime, "MFA code or recovery code: ")).trim();
@@ -17499,8 +17523,22 @@ function requiredPath(parsed) {
17499
17523
  }
17500
17524
  return value;
17501
17525
  }
17526
+ function apiBaseUrlResolution(parsed, runtime) {
17527
+ const optionValue = optionalStringOption(parsed, "api-url");
17528
+ if (optionValue) {
17529
+ return { url: normalizeApiUrlOption(optionValue), source: "option" };
17530
+ }
17531
+ if (runtime.env.MYSKILLS_API_URL) {
17532
+ return { url: normalizeApiUrlOption(runtime.env.MYSKILLS_API_URL), source: "env" };
17533
+ }
17534
+ const configuredApiUrl = runtime.configStore?.getApiUrl();
17535
+ if (configuredApiUrl) {
17536
+ return { url: normalizeApiUrlOption(configuredApiUrl), source: "config" };
17537
+ }
17538
+ return { url: DEFAULT_API_URL, source: "default" };
17539
+ }
17502
17540
  function apiBaseUrl(parsed, runtime) {
17503
- return String(parsed.options["api-url"] ?? runtime.env.MYSKILLS_API_URL ?? DEFAULT_API_URL).replace(/\/+$/, "");
17541
+ return apiBaseUrlResolution(parsed, runtime).url;
17504
17542
  }
17505
17543
  async function tokenOption(parsed, runtime) {
17506
17544
  return (await resolveToken(parsed, runtime))?.value ?? null;
@@ -17558,6 +17596,60 @@ async function promptSecret(runtime, label) {
17558
17596
  }
17559
17597
  return value;
17560
17598
  }
17599
+ async function promptOptionalText(runtime, label) {
17600
+ if (!runtime.prompt) {
17601
+ throw new CliError("Interactive input is unavailable. Set MYSKILLS_TOKEN for one-off commands.", 1);
17602
+ }
17603
+ return (await runtime.prompt.text(label)).trim();
17604
+ }
17605
+ async function loginApiUrl(parsed, runtime) {
17606
+ const resolved = apiBaseUrlResolution(parsed, runtime);
17607
+ if (resolved.source === "option" || resolved.source === "env" || !runtime.prompt) {
17608
+ return resolved.url;
17609
+ }
17610
+ const defaultUrl = resolved.source === "config" ? resolved.url : DEFAULT_API_URL;
17611
+ const input = await promptOptionalText(runtime, `API URL [${defaultUrl}]: `);
17612
+ return input ? normalizeApiUrlOption(input) : defaultUrl;
17613
+ }
17614
+ async function loginAuthMethod(parsed, runtime) {
17615
+ if (parsed.options["api-key"] === true) {
17616
+ return "api-key";
17617
+ }
17618
+ const methodOption = optionalStringOption(parsed, "auth-method") ?? optionalStringOption(parsed, "method");
17619
+ if (methodOption) {
17620
+ return parseLoginAuthMethod(methodOption);
17621
+ }
17622
+ if (optionalStringOption(parsed, "email") || !runtime.prompt) {
17623
+ return "password";
17624
+ }
17625
+ const input = await promptOptionalText(runtime, "Authentication method [password] (password/api-key): ");
17626
+ return input ? parseLoginAuthMethod(input) : "password";
17627
+ }
17628
+ function parseLoginAuthMethod(input) {
17629
+ const normalized = input.trim().toLowerCase();
17630
+ if (normalized === "password" || normalized === "email" || normalized === "user" || normalized === "username") {
17631
+ return "password";
17632
+ }
17633
+ if (normalized === "api-key" || normalized === "apikey" || normalized === "api" || normalized === "key") {
17634
+ return "api-key";
17635
+ }
17636
+ if (normalized === "browser" || normalized === "web") {
17637
+ throw new CliError("Browser login is not available in this CLI/API version yet. Choose password or api-key.", 2);
17638
+ }
17639
+ throw new CliError(`Authentication method must be one of: ${LOGIN_AUTH_METHODS.join(", ")}.`, 2);
17640
+ }
17641
+ function normalizeApiUrlOption(input) {
17642
+ const trimmed = input.trim().replace(/\/+$/, "");
17643
+ try {
17644
+ const url2 = new URL(trimmed);
17645
+ if (url2.protocol !== "http:" && url2.protocol !== "https:") {
17646
+ throw new Error("unsupported protocol");
17647
+ }
17648
+ return trimmed;
17649
+ } catch {
17650
+ throw new CliError("API URL must be a valid http:// or https:// URL.", 2);
17651
+ }
17652
+ }
17561
17653
  function authSessionFromResponse(response) {
17562
17654
  const user = response.user;
17563
17655
  let email3;
@@ -17666,7 +17758,7 @@ function parseArgs(argv) {
17666
17758
  continue;
17667
17759
  }
17668
17760
  const key = value.slice(2);
17669
- if (key === "json") {
17761
+ if (key === "json" || key === "api-key") {
17670
17762
  options2[key] = true;
17671
17763
  continue;
17672
17764
  }
@@ -17696,7 +17788,8 @@ function helpText() {
17696
17788
  " scan --path <file-directory-or-zip>",
17697
17789
  " search [query] [--api-url <url>]",
17698
17790
  " info <skill-slug> [--api-url <url>]",
17699
- " login [--api-url <url>] [--email <email>]",
17791
+ " login [--api-url <url>] [--method <password|api-key>] [--email <email>]",
17792
+ " login --api-key [--api-url <url>]",
17700
17793
  " logout [--api-url <url>] [--token <token>]",
17701
17794
  " whoami [--api-url <url>] [--token <token>]",
17702
17795
  " submit --path <file-directory-or-zip> [--api-url <url>] [--token <token>]",
@@ -17722,7 +17815,7 @@ function helpText() {
17722
17815
  "Options:",
17723
17816
  " --version Print CLI version.",
17724
17817
  " --json Print machine-readable JSON.",
17725
- " --api-url <url> API base URL. Defaults to MYSKILLS_API_URL or http://localhost:3001.",
17818
+ " --api-url <url> API base URL. Defaults to MYSKILLS_API_URL, saved config, or http://localhost:3001.",
17726
17819
  " --token <token> Bearer token. Defaults to MYSKILLS_TOKEN, then stored login token."
17727
17820
  ].join("\n");
17728
17821
  }
@@ -17734,60 +17827,175 @@ var CliError = class extends Error {
17734
17827
  exitCode;
17735
17828
  };
17736
17829
 
17737
- // src/token-store.ts
17738
- import { chmod, mkdir as mkdir2, readFile as readFile3, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
17830
+ // src/config-store.ts
17831
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17739
17832
  import os from "node:os";
17740
17833
  import path3 from "node:path";
17834
+ function createFileConfigStore(env = process.env) {
17835
+ const filePath = configFilePath(env);
17836
+ let payload = readPayload(filePath);
17837
+ return {
17838
+ getApiUrl() {
17839
+ return payload.apiUrl;
17840
+ },
17841
+ async setApiUrl(apiUrl) {
17842
+ payload = {
17843
+ ...payload,
17844
+ apiUrl: apiUrl.replace(/\/+$/, "")
17845
+ };
17846
+ writePayload(filePath, payload);
17847
+ }
17848
+ };
17849
+ }
17850
+ function configFilePath(env) {
17851
+ if (env.MYSKILLS_CONFIG_FILE) {
17852
+ return path3.resolve(env.MYSKILLS_CONFIG_FILE);
17853
+ }
17854
+ if (env.MYSKILLS_CONFIG_DIR) {
17855
+ return path3.join(path3.resolve(env.MYSKILLS_CONFIG_DIR), "config.json");
17856
+ }
17857
+ const baseDir = env.XDG_CONFIG_HOME ? path3.join(env.XDG_CONFIG_HOME, "myskills-app") : path3.join(os.homedir(), ".config", "myskills-app");
17858
+ return path3.join(baseDir, "config.json");
17859
+ }
17860
+ function readPayload(filePath) {
17861
+ try {
17862
+ return parsePayload(readFileSync(filePath, "utf8"));
17863
+ } catch (error51) {
17864
+ if (isNodeError2(error51) && error51.code === "ENOENT") {
17865
+ return { version: 1 };
17866
+ }
17867
+ throw error51;
17868
+ }
17869
+ }
17870
+ function writePayload(filePath, payload) {
17871
+ mkdirSync(path3.dirname(filePath), { recursive: true, mode: 448 });
17872
+ writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}
17873
+ `, { mode: 384 });
17874
+ chmodSync(filePath, 384);
17875
+ }
17876
+ function parsePayload(raw) {
17877
+ const parsed = JSON.parse(raw);
17878
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
17879
+ throw new Error("Config file must contain a JSON object.");
17880
+ }
17881
+ const record2 = parsed;
17882
+ if (record2.version !== 1) {
17883
+ throw new Error("Config file has an unsupported format.");
17884
+ }
17885
+ return {
17886
+ version: 1,
17887
+ apiUrl: typeof record2.apiUrl === "string" && record2.apiUrl ? record2.apiUrl.replace(/\/+$/, "") : void 0
17888
+ };
17889
+ }
17890
+ function isNodeError2(error51) {
17891
+ return error51 instanceof Error && "code" in error51;
17892
+ }
17893
+
17894
+ // src/token-store.ts
17895
+ import { chmod, mkdir as mkdir2, readFile as readFile3, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
17896
+ import os2 from "node:os";
17897
+ import path4 from "node:path";
17898
+ var KEYRING_SERVICE = "ai.jarel.myskills.cli";
17899
+ function createTokenStore(env = process.env) {
17900
+ const fileStore = createFileTokenStore(env);
17901
+ if (env.MYSKILLS_TOKEN_STORE === "file" || env.MYSKILLS_TOKEN_FILE) {
17902
+ return fileStore;
17903
+ }
17904
+ return createKeyringTokenStore(fileStore);
17905
+ }
17741
17906
  function createFileTokenStore(env = process.env) {
17742
17907
  const filePath = tokenFilePath(env);
17743
17908
  return {
17744
17909
  async get(apiUrl) {
17745
- return (await readPayload(filePath)).tokens[normalizeApiUrl(apiUrl)] ?? null;
17910
+ return (await readPayload2(filePath)).tokens[normalizeApiUrl(apiUrl)] ?? null;
17746
17911
  },
17747
17912
  async set(apiUrl, token) {
17748
- const payload = await readPayload(filePath);
17913
+ const payload = await readPayload2(filePath);
17749
17914
  payload.tokens[normalizeApiUrl(apiUrl)] = token;
17750
- await writePayload(filePath, payload);
17915
+ await writePayload2(filePath, payload);
17751
17916
  },
17752
17917
  async delete(apiUrl) {
17753
- const payload = await readPayload(filePath);
17918
+ const payload = await readPayload2(filePath);
17754
17919
  delete payload.tokens[normalizeApiUrl(apiUrl)];
17755
17920
  if (Object.keys(payload.tokens).length === 0) {
17756
17921
  await rm2(filePath, { force: true });
17757
17922
  return;
17758
17923
  }
17759
- await writePayload(filePath, payload);
17924
+ await writePayload2(filePath, payload);
17925
+ }
17926
+ };
17927
+ }
17928
+ function createKeyringTokenStore(fallback) {
17929
+ return {
17930
+ async get(apiUrl) {
17931
+ const keyringToken = await readKeyringToken(apiUrl);
17932
+ return keyringToken ?? await fallback.get(apiUrl);
17933
+ },
17934
+ async set(apiUrl, token) {
17935
+ if (await writeKeyringToken(apiUrl, token)) {
17936
+ return;
17937
+ }
17938
+ await fallback.set(apiUrl, token);
17939
+ },
17940
+ async delete(apiUrl) {
17941
+ await deleteKeyringToken(apiUrl);
17942
+ await fallback.delete(apiUrl);
17760
17943
  }
17761
17944
  };
17762
17945
  }
17946
+ async function readKeyringToken(apiUrl) {
17947
+ try {
17948
+ const { Entry } = await import("@napi-rs/keyring");
17949
+ const raw = new Entry(KEYRING_SERVICE, keyringAccount(apiUrl)).getPassword();
17950
+ return raw ? parseStoredToken(JSON.parse(raw)) : null;
17951
+ } catch {
17952
+ return null;
17953
+ }
17954
+ }
17955
+ async function writeKeyringToken(apiUrl, token) {
17956
+ try {
17957
+ const { Entry } = await import("@napi-rs/keyring");
17958
+ new Entry(KEYRING_SERVICE, keyringAccount(apiUrl)).setPassword(JSON.stringify(token));
17959
+ return true;
17960
+ } catch {
17961
+ return false;
17962
+ }
17963
+ }
17964
+ async function deleteKeyringToken(apiUrl) {
17965
+ try {
17966
+ const { Entry } = await import("@napi-rs/keyring");
17967
+ new Entry(KEYRING_SERVICE, keyringAccount(apiUrl)).deletePassword();
17968
+ } catch {
17969
+ }
17970
+ }
17763
17971
  function tokenFilePath(env) {
17764
17972
  if (env.MYSKILLS_TOKEN_FILE) {
17765
- return path3.resolve(env.MYSKILLS_TOKEN_FILE);
17973
+ return path4.resolve(env.MYSKILLS_TOKEN_FILE);
17766
17974
  }
17767
17975
  if (env.MYSKILLS_CONFIG_DIR) {
17768
- return path3.join(path3.resolve(env.MYSKILLS_CONFIG_DIR), "tokens.json");
17976
+ return path4.join(path4.resolve(env.MYSKILLS_CONFIG_DIR), "tokens.json");
17769
17977
  }
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");
17978
+ const baseDir = env.XDG_CONFIG_HOME ? path4.join(env.XDG_CONFIG_HOME, "myskills-app") : path4.join(os2.homedir(), ".config", "myskills-app");
17979
+ return path4.join(baseDir, "tokens.json");
17772
17980
  }
17773
- async function readPayload(filePath) {
17981
+ async function readPayload2(filePath) {
17774
17982
  try {
17775
17983
  const raw = await readFile3(filePath, "utf8");
17776
- return parsePayload(raw);
17984
+ return parsePayload2(raw);
17777
17985
  } catch (error51) {
17778
- if (isNodeError2(error51) && error51.code === "ENOENT") {
17986
+ if (isNodeError3(error51) && error51.code === "ENOENT") {
17779
17987
  return { version: 1, tokens: {} };
17780
17988
  }
17781
17989
  throw error51;
17782
17990
  }
17783
17991
  }
17784
- async function writePayload(filePath, payload) {
17785
- await mkdir2(path3.dirname(filePath), { recursive: true, mode: 448 });
17992
+ async function writePayload2(filePath, payload) {
17993
+ await mkdir2(path4.dirname(filePath), { recursive: true, mode: 448 });
17786
17994
  await writeFile2(filePath, `${JSON.stringify(payload, null, 2)}
17787
17995
  `, { mode: 384 });
17788
17996
  await chmod(filePath, 384);
17789
17997
  }
17790
- function parsePayload(raw) {
17998
+ function parsePayload2(raw) {
17791
17999
  const parsed = JSON.parse(raw);
17792
18000
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
17793
18001
  throw new Error("Token store file must contain a JSON object.");
@@ -17823,7 +18031,10 @@ function parseStoredToken(input) {
17823
18031
  function normalizeApiUrl(apiUrl) {
17824
18032
  return apiUrl.replace(/\/+$/, "");
17825
18033
  }
17826
- function isNodeError2(error51) {
18034
+ function keyringAccount(apiUrl) {
18035
+ return `api-url:${Buffer.from(normalizeApiUrl(apiUrl)).toString("base64url")}`;
18036
+ }
18037
+ function isNodeError3(error51) {
17827
18038
  return error51 instanceof Error && "code" in error51;
17828
18039
  }
17829
18040
 
@@ -17943,8 +18154,9 @@ try {
17943
18154
  const exitCode = await runCli(process.argv.slice(2), {
17944
18155
  env: process.env,
17945
18156
  fetch: fetchImpl,
18157
+ configStore: createFileConfigStore(process.env),
17946
18158
  prompt,
17947
- tokenStore: createFileTokenStore(process.env),
18159
+ tokenStore: createTokenStore(process.env),
17948
18160
  io: {
17949
18161
  stdout: (line) => console.log(line),
17950
18162
  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.1",
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
  }