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

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 +27 -3
  2. package/dist/index.js +603 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -41,9 +41,29 @@ 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
- myskills review action <submission-id> --action <approve|publish> [--reason <text>]
52
+ myskills review action <submission-id> --action <approve|request-changes|reject|publish> [--reason <text>] [--api-url <url>] [--token <token>]
53
+ myskills submissions list [--api-url <url>] [--token <token>]
54
+ myskills submissions withdraw <submission-id> [--reason <text>] [--api-url <url>] [--token <token>]
55
+ myskills skills edit <skill-slug> [--title <text>] [--summary <text>] [--visibility <scope>] [--tag <tag>] [--reason <text>] [--api-url <url>] [--token <token>]
56
+ myskills skills archive|restore|delete <skill-slug> [--reason <text>] [--api-url <url>] [--token <token>]
57
+ myskills releases list <skill-slug> [--api-url <url>] [--token <token>]
58
+ myskills releases deprecate|unpublish|revoke|restore|delete <skill-slug>@<version> [--reason <text>] [--replacement <version>] [--api-url <url>] [--token <token>]
59
+ myskills teams list|skills [--api-url <url>] [--token <token>]
60
+ myskills teams create <team-name> [--name <team-name>] [--api-url <url>] [--token <token>]
61
+ myskills teams invite <team-id> --email <email> [--api-url <url>] [--token <token>]
62
+ myskills teams accept <invitation-id> [--api-url <url>] [--token <token>]
63
+ myskills sharing get <skill-slug> [--api-url <url>] [--token <token>]
64
+ myskills sharing set <skill-slug> --visibility <scope> [--team <team-id>] [--user <email>]
65
+ myskills admin sharing get [--api-url <url>] [--token <token>]
66
+ myskills admin sharing set [--public <true|false>] [--authenticated <true|false>] [--teams <true|false>] [--team-visibility <true|false>] [--user-visibility <true|false>]
47
67
  myskills export <skill-slug> --version <version> --platform <platform> --output <dir>
48
68
  myskills install <skill-slug> [--version <version>] [--platform <platform>] [--dir <install-root>]
49
69
  myskills list [--dir <install-root>]
@@ -72,9 +92,13 @@ npm install -g @jarel/myskills@alpha
72
92
 
73
93
  `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
94
 
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`.
95
+ `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`.
76
96
 
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.
97
+ `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.
98
+
99
+ `submit` validates and scans locally before sending package directories as normalized text entries or `.zip` packages as base64 archive uploads for server-side extraction. Authors can inspect their submitted versions with `submissions list` and withdraw unreviewed or changes-requested submissions with `submissions withdraw`. Maintainers can approve, request changes, reject, and publish submitted versions through `review action`.
100
+
101
+ Published artifacts remain immutable. `skills edit` changes mutable skill metadata only, while `releases deprecate`, `releases unpublish`, `releases revoke`, `releases restore`, and `releases delete` update server-owned lifecycle state for a specific version. Deprecated releases remain visible and installable; unpublished, revoked, archived, and deleted releases are hidden from install/export queries. `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
102
 
79
103
  Common scopes:
80
104
 
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.3";
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,10 +16353,22 @@ 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":
16359
16365
  return await reviewCommand(parsed, runtime);
16366
+ case "submissions":
16367
+ return await submissionsCommand(parsed, runtime);
16368
+ case "skills":
16369
+ return await skillsCommand(parsed, runtime);
16370
+ case "releases":
16371
+ return await releasesCommand(parsed, runtime);
16360
16372
  case "teams":
16361
16373
  return await teamsCommand(parsed, runtime);
16362
16374
  case "sharing":
@@ -16380,10 +16392,19 @@ async function runCli(argv, runtime) {
16380
16392
  }
16381
16393
  } catch (error51) {
16382
16394
  if (error51 instanceof CliError) {
16383
- runtime.io.stderr(error51.message);
16395
+ if (parsed.options.json) {
16396
+ runtime.io.stderr(JSON.stringify({ error: error51.toJSON() }, null, 2));
16397
+ } else {
16398
+ runtime.io.stderr(error51.message);
16399
+ }
16384
16400
  return error51.exitCode;
16385
16401
  }
16386
- runtime.io.stderr(error51 instanceof Error ? error51.message : "Unexpected CLI failure.");
16402
+ const message = error51 instanceof Error ? error51.message : "Unexpected CLI failure.";
16403
+ if (parsed.options.json) {
16404
+ runtime.io.stderr(JSON.stringify({ error: { code: "UNEXPECTED_CLI_FAILURE", message } }, null, 2));
16405
+ } else {
16406
+ runtime.io.stderr(message);
16407
+ }
16387
16408
  return 1;
16388
16409
  }
16389
16410
  }
@@ -16538,6 +16559,143 @@ async function whoamiCommand(parsed, runtime) {
16538
16559
  }
16539
16560
  return 0;
16540
16561
  }
16562
+ async function authCommand(parsed, runtime) {
16563
+ const subcommand = parsed.args[0];
16564
+ if (subcommand === "status") {
16565
+ return await authStatusCommand(parsed, runtime);
16566
+ }
16567
+ throw new CliError("Usage: myskills auth status", 2, "USAGE_ERROR");
16568
+ }
16569
+ async function authStatusCommand(parsed, runtime) {
16570
+ const api = apiBaseUrlResolution(parsed, runtime);
16571
+ const resolved = await resolveToken(parsed, runtime);
16572
+ if (!resolved) {
16573
+ const status2 = {
16574
+ apiUrl: api.url,
16575
+ apiUrlSource: api.source,
16576
+ status: "not_logged_in",
16577
+ tokenSource: "none",
16578
+ tokenStore: await tokenStoreInfo(runtime)
16579
+ };
16580
+ if (parsed.options.json) {
16581
+ runtime.io.stdout(JSON.stringify(status2, null, 2));
16582
+ } else {
16583
+ runtime.io.stdout(`API URL: ${status2.apiUrl} (${status2.apiUrlSource})`);
16584
+ runtime.io.stdout("Status: not logged in");
16585
+ runtime.io.stdout(`Token store: ${status2.tokenStore.backend}`);
16586
+ }
16587
+ return 0;
16588
+ }
16589
+ const response = await apiGet("/v1/me", parsed, runtime, resolved.value);
16590
+ const user = response.user;
16591
+ const status = {
16592
+ apiUrl: api.url,
16593
+ apiUrlSource: api.source,
16594
+ status: "logged_in",
16595
+ tokenSource: resolved.source,
16596
+ tokenKind: resolved.stored.kind,
16597
+ tokenStore: await tokenStoreInfo(runtime),
16598
+ user: {
16599
+ email: user.email,
16600
+ roles: user.roles,
16601
+ mfaVerified: user.mfaVerified
16602
+ },
16603
+ expiresAt: resolved.stored.expiresAt ?? null
16604
+ };
16605
+ if (parsed.options.json) {
16606
+ runtime.io.stdout(JSON.stringify(status, null, 2));
16607
+ } else {
16608
+ runtime.io.stdout(`API URL: ${status.apiUrl} (${status.apiUrlSource})`);
16609
+ runtime.io.stdout(`Status: logged in (${status.tokenKind}, ${status.tokenSource})`);
16610
+ runtime.io.stdout(`User: ${user.email}`);
16611
+ runtime.io.stdout(`Roles: ${user.roles.join(",") || "-"}`);
16612
+ runtime.io.stdout(`MFA: ${user.mfaVerified ? "verified" : "not-verified"}`);
16613
+ runtime.io.stdout(`Expires: ${status.expiresAt ?? "-"}`);
16614
+ runtime.io.stdout(`Token store: ${status.tokenStore.backend}`);
16615
+ }
16616
+ return 0;
16617
+ }
16618
+ async function configCommand(parsed, runtime) {
16619
+ const subcommand = parsed.args[0];
16620
+ const key = parsed.args[1];
16621
+ if (!runtime.configStore) {
16622
+ throw new CliError("No config store is configured.", 1, "CONFIG_STORE_UNAVAILABLE");
16623
+ }
16624
+ if (subcommand === "get" && key === "api-url") {
16625
+ const apiUrl = runtime.configStore.getApiUrl() ?? null;
16626
+ if (parsed.options.json) {
16627
+ runtime.io.stdout(JSON.stringify({ apiUrl }, null, 2));
16628
+ } else {
16629
+ runtime.io.stdout(apiUrl ?? "unset");
16630
+ }
16631
+ return 0;
16632
+ }
16633
+ if (subcommand === "set" && key === "api-url") {
16634
+ const apiUrl = parsed.args[2];
16635
+ if (!apiUrl) {
16636
+ throw new CliError("Usage: myskills config set api-url <url>", 2, "USAGE_ERROR");
16637
+ }
16638
+ await runtime.configStore.setApiUrl(normalizeApiUrlOption(apiUrl));
16639
+ if (parsed.options.json) {
16640
+ runtime.io.stdout(JSON.stringify({ apiUrl: normalizeApiUrlOption(apiUrl) }, null, 2));
16641
+ } else {
16642
+ runtime.io.stdout(`api-url=${normalizeApiUrlOption(apiUrl)}`);
16643
+ }
16644
+ return 0;
16645
+ }
16646
+ if (subcommand === "reset" && key === "api-url") {
16647
+ await runtime.configStore.resetApiUrl();
16648
+ if (parsed.options.json) {
16649
+ runtime.io.stdout(JSON.stringify({ apiUrl: null }, null, 2));
16650
+ } else {
16651
+ runtime.io.stdout("api-url unset");
16652
+ }
16653
+ return 0;
16654
+ }
16655
+ if (subcommand === "list") {
16656
+ const resolved = apiBaseUrlResolution(parsed, runtime);
16657
+ const saved = runtime.configStore.getApiUrl() ?? null;
16658
+ if (parsed.options.json) {
16659
+ runtime.io.stdout(JSON.stringify({ apiUrl: saved, resolvedApiUrl: resolved.url, resolvedApiUrlSource: resolved.source }, null, 2));
16660
+ } else {
16661
+ runtime.io.stdout(`api-url=${saved ?? "unset"}`);
16662
+ runtime.io.stdout(`resolved-api-url=${resolved.url} source=${resolved.source}`);
16663
+ }
16664
+ return 0;
16665
+ }
16666
+ throw new CliError("Usage: myskills config get api-url | config set api-url <url> | config reset api-url | config list", 2, "USAGE_ERROR");
16667
+ }
16668
+ async function doctorCommand(parsed, runtime) {
16669
+ const api = apiBaseUrlResolution(parsed, runtime);
16670
+ const checks = [];
16671
+ checks.push(nodeVersionCheck());
16672
+ checks.push({ name: "cli_version", ok: true, message: CLI_VERSION, details: { version: CLI_VERSION } });
16673
+ checks.push({ name: "api_url", ok: true, message: `${api.url} (${api.source})`, details: api });
16674
+ const health = await doctorHealthCheck(parsed, runtime);
16675
+ checks.push(health);
16676
+ const token = await resolveToken(parsed, runtime);
16677
+ checks.push(await doctorAuthCheck(parsed, runtime, token));
16678
+ checks.push(await doctorTokenStoreCheck(runtime));
16679
+ checks.push(await doctorInstallDirCheck(parsed, runtime));
16680
+ checks.push(await doctorCapabilitiesCheck(parsed, runtime));
16681
+ const failed = checks.filter((check2) => !check2.ok);
16682
+ const result = {
16683
+ cliVersion: CLI_VERSION,
16684
+ apiUrl: api.url,
16685
+ apiUrlSource: api.source,
16686
+ checks
16687
+ };
16688
+ if (parsed.options.json) {
16689
+ runtime.io.stdout(JSON.stringify(result, null, 2));
16690
+ } else {
16691
+ runtime.io.stdout(`MySkills CLI ${CLI_VERSION}`);
16692
+ runtime.io.stdout("");
16693
+ for (const check2 of checks) {
16694
+ runtime.io.stdout(`${check2.ok ? "ok" : "fail"} ${check2.name} ${check2.message}`);
16695
+ }
16696
+ }
16697
+ return failed.length === 0 ? 0 : 1;
16698
+ }
16541
16699
  async function tokenCommand(parsed, runtime) {
16542
16700
  const token = await tokenOption(parsed, runtime);
16543
16701
  if (!token) {
@@ -16622,11 +16780,11 @@ async function reviewCommand(parsed, runtime) {
16622
16780
  if (subcommand === "action") {
16623
16781
  const submissionId = parsed.args[1];
16624
16782
  if (!submissionId) {
16625
- throw new CliError("Usage: myskills review action <submission-id> --action <approve|publish>", 2);
16783
+ throw new CliError("Usage: myskills review action <submission-id> --action <approve|request-changes|reject|publish> [--reason <text>] [--api-url <url>] [--token <token>]", 2);
16626
16784
  }
16627
16785
  const action = stringOption(parsed, "action");
16628
- if (action !== "approve" && action !== "publish") {
16629
- throw new CliError("--action must be approve or publish.", 2);
16786
+ if (action !== "approve" && action !== "request-changes" && action !== "reject" && action !== "publish") {
16787
+ throw new CliError("--action must be approve, request-changes, reject, or publish.", 2);
16630
16788
  }
16631
16789
  const reason = optionalStringOption(parsed, "reason");
16632
16790
  const response = await apiPost(`/v1/review/submissions/${encodeURIComponent(submissionId)}/actions`, {
@@ -16641,7 +16799,144 @@ async function reviewCommand(parsed, runtime) {
16641
16799
  }
16642
16800
  return 0;
16643
16801
  }
16644
- throw new CliError("Usage: myskills review submissions | review action <submission-id> --action <approve|publish>", 2);
16802
+ throw new CliError("Usage: myskills review submissions | review action <submission-id> --action <approve|request-changes|reject|publish> [--reason <text>]", 2);
16803
+ }
16804
+ async function submissionsCommand(parsed, runtime) {
16805
+ const token = await requireToken(parsed, runtime);
16806
+ const subcommand = parsed.args[0];
16807
+ if (subcommand === "list" || subcommand === "mine") {
16808
+ const response = await apiGet("/v1/submissions/mine", parsed, runtime, token);
16809
+ if (parsed.options.json) {
16810
+ runtime.io.stdout(JSON.stringify(response, null, 2));
16811
+ } else {
16812
+ const submissions = arrayField(response, "submissions");
16813
+ if (submissions.length === 0) {
16814
+ runtime.io.stdout("No submissions.");
16815
+ } else {
16816
+ for (const value of submissions) {
16817
+ const submission = recordField(value, "submission");
16818
+ runtime.io.stdout([
16819
+ requiredRecordString(submission, "id", "Submission response is missing id."),
16820
+ `${requiredRecordString(submission, "slug", "Submission response is missing slug.")}@${requiredRecordString(submission, "version", "Submission response is missing version.")}`,
16821
+ optionalRecordString(submission, "reviewStatus") ?? "-",
16822
+ optionalRecordString(submission, "lifecycleStatus") ?? "-",
16823
+ optionalRecordString(submission, "securityStatus") ?? "-"
16824
+ ].join(" "));
16825
+ }
16826
+ }
16827
+ }
16828
+ return 0;
16829
+ }
16830
+ if (subcommand === "withdraw") {
16831
+ const submissionId = parsed.args[1];
16832
+ if (!submissionId) {
16833
+ throw new CliError("Usage: myskills submissions withdraw <submission-id> [--reason <text>] [--api-url <url>] [--token <token>]", 2);
16834
+ }
16835
+ const response = await apiPost(`/v1/submissions/${encodeURIComponent(submissionId)}/actions`, {
16836
+ action: "withdraw",
16837
+ ...reasonPayload(parsed)
16838
+ }, parsed, runtime, token);
16839
+ printNamedRecord(response, "submission", runtime.io, ["id", "slug", "version", "reviewStatus", "lifecycleStatus"]);
16840
+ return 0;
16841
+ }
16842
+ throw new CliError("Usage: myskills submissions list | submissions withdraw <submission-id> [--reason <text>] [--api-url <url>] [--token <token>]", 2);
16843
+ }
16844
+ async function skillsCommand(parsed, runtime) {
16845
+ const token = await requireToken(parsed, runtime);
16846
+ const subcommand = parsed.args[0];
16847
+ const slug = parsed.args[1];
16848
+ if (subcommand === "edit") {
16849
+ if (!slug) {
16850
+ throw new CliError("Usage: myskills skills edit <skill-slug> [--title <text>] [--summary <text>] [--visibility <scope>] [--tag <tag>] [--reason <text>] [--api-url <url>] [--token <token>]", 2);
16851
+ }
16852
+ const payload = {
16853
+ ...reasonPayload(parsed)
16854
+ };
16855
+ const title = optionalStringOption(parsed, "title");
16856
+ const summary = optionalStringOption(parsed, "summary");
16857
+ const visibility = optionalStringOption(parsed, "visibility");
16858
+ const tags = stringListOption(parsed, "tag");
16859
+ if (title !== void 0) {
16860
+ payload.title = title;
16861
+ }
16862
+ if (summary !== void 0) {
16863
+ payload.summary = summary;
16864
+ }
16865
+ if (visibility !== void 0) {
16866
+ if (!CLI_VISIBILITY_SCOPES.includes(visibility)) {
16867
+ throw new CliError(`--visibility must be one of: ${CLI_VISIBILITY_SCOPES.join(", ")}.`, 2);
16868
+ }
16869
+ payload.visibility = visibility;
16870
+ }
16871
+ if (tags.length > 0) {
16872
+ payload.tags = tags;
16873
+ }
16874
+ if (Object.keys(payload).every((key) => key === "reason")) {
16875
+ throw new CliError("At least one metadata option is required.", 2);
16876
+ }
16877
+ const response = await apiPut(`/v1/skills/${encodeURIComponent(parseInstallSlug(slug))}`, payload, parsed, runtime, token);
16878
+ printNamedRecord(response, "skill", runtime.io, ["slug", "title", "lifecycleStatus", "visibility"]);
16879
+ return 0;
16880
+ }
16881
+ if (subcommand === "archive" || subcommand === "restore" || subcommand === "delete") {
16882
+ if (!slug) {
16883
+ throw new CliError("Usage: myskills skills archive|restore|delete <skill-slug> [--reason <text>] [--api-url <url>] [--token <token>]", 2);
16884
+ }
16885
+ const response = await apiPost(`/v1/skills/${encodeURIComponent(parseInstallSlug(slug))}/actions`, {
16886
+ action: subcommand,
16887
+ ...reasonPayload(parsed)
16888
+ }, parsed, runtime, token);
16889
+ printNamedRecord(response, "skill", runtime.io, ["slug", "title", "lifecycleStatus", "visibility"]);
16890
+ return 0;
16891
+ }
16892
+ throw new CliError("Usage: myskills skills edit|archive|restore|delete <skill-slug> [--api-url <url>] [--token <token>]", 2);
16893
+ }
16894
+ async function releasesCommand(parsed, runtime) {
16895
+ const subcommand = parsed.args[0];
16896
+ if (subcommand === "list") {
16897
+ const slug = parsed.args[1];
16898
+ if (!slug) {
16899
+ throw new CliError("Usage: myskills releases list <skill-slug>", 2);
16900
+ }
16901
+ const token = await tokenOption(parsed, runtime) ?? void 0;
16902
+ const response = await apiGet(`/v1/skills/${encodeURIComponent(parseInstallSlug(slug))}/releases`, parsed, runtime, token);
16903
+ if (parsed.options.json) {
16904
+ runtime.io.stdout(JSON.stringify(response, null, 2));
16905
+ } else {
16906
+ const releases = arrayField(response, "releases");
16907
+ if (releases.length === 0) {
16908
+ runtime.io.stdout("No releases.");
16909
+ } else {
16910
+ for (const value of releases) {
16911
+ const release = recordField(value, "release");
16912
+ runtime.io.stdout([
16913
+ `${requiredRecordString(release, "slug", "Release response is missing slug.")}@${requiredRecordString(release, "version", "Release response is missing version.")}`,
16914
+ optionalRecordString(release, "lifecycleStatus") ?? "-",
16915
+ optionalRecordString(release, "reviewStatus") ?? "-",
16916
+ optionalRecordString(release, "securityStatus") ?? "-",
16917
+ `published=${optionalRecordString(release, "publishedAt") ?? "-"}`
16918
+ ].join(" "));
16919
+ }
16920
+ }
16921
+ }
16922
+ return 0;
16923
+ }
16924
+ if (["deprecate", "unpublish", "revoke", "restore", "delete"].includes(subcommand ?? "")) {
16925
+ const target = parsed.args[1];
16926
+ if (!target) {
16927
+ throw new CliError("Usage: myskills releases deprecate|unpublish|revoke|restore|delete <skill-slug>@<version> [--reason <text>] [--replacement <version>] [--api-url <url>] [--token <token>]", 2);
16928
+ }
16929
+ const { slug, version: version2 } = parseReleaseTarget(target);
16930
+ const token = await requireToken(parsed, runtime);
16931
+ const response = await apiPost(`/v1/skills/${encodeURIComponent(slug)}/releases/${encodeURIComponent(version2)}/actions`, {
16932
+ action: subcommand,
16933
+ ...reasonPayload(parsed),
16934
+ ...optionalStringOption(parsed, "replacement") ? { replacement: optionalStringOption(parsed, "replacement") } : {}
16935
+ }, parsed, runtime, token);
16936
+ printNamedRecord(response, "release", runtime.io, ["slug", "version", "lifecycleStatus", "reviewStatus", "securityStatus"]);
16937
+ return 0;
16938
+ }
16939
+ throw new CliError("Usage: myskills releases list <skill-slug> | releases deprecate|unpublish|revoke|restore|delete <skill-slug>@<version> [--api-url <url>] [--token <token>]", 2);
16645
16940
  }
16646
16941
  async function teamsCommand(parsed, runtime) {
16647
16942
  const token = await requireToken(parsed, runtime);
@@ -17356,6 +17651,26 @@ function recordField(input, label) {
17356
17651
  }
17357
17652
  return input;
17358
17653
  }
17654
+ function printNamedRecord(response, key, io, fields) {
17655
+ const record2 = recordField(response[key], key);
17656
+ io.stdout(fields.map((field) => optionalRecordString(record2, field) ?? "-").join(" "));
17657
+ }
17658
+ function reasonPayload(parsed) {
17659
+ const reason = optionalStringOption(parsed, "reason");
17660
+ return reason ? { reason } : {};
17661
+ }
17662
+ function parseReleaseTarget(target) {
17663
+ const separator = target.lastIndexOf("@");
17664
+ if (separator <= 0 || separator === target.length - 1) {
17665
+ throw new CliError("Release target must be <skill-slug>@<version>.", 2);
17666
+ }
17667
+ const slug = parseInstallSlug(target.slice(0, separator));
17668
+ const version2 = target.slice(separator + 1);
17669
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version2)) {
17670
+ throw new CliError("Release version is invalid.", 2);
17671
+ }
17672
+ return { slug, version: version2 };
17673
+ }
17359
17674
  function arrayField(record2, key) {
17360
17675
  const value = record2[key];
17361
17676
  return Array.isArray(value) ? value : [];
@@ -17412,101 +17727,296 @@ function assertChildPath(root, target) {
17412
17727
  }
17413
17728
  }
17414
17729
  async function apiGet(pathname, parsed, runtime, token) {
17415
- const baseUrl = apiBaseUrl(parsed, runtime);
17416
17730
  const headers = {};
17417
17731
  if (token) {
17418
17732
  headers.authorization = `Bearer ${token}`;
17419
17733
  }
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;
17734
+ return await apiJsonRequest(pathname, parsed, runtime, { headers });
17428
17735
  }
17429
17736
  async function apiGetText(pathname, parsed, runtime, token) {
17430
- const baseUrl = apiBaseUrl(parsed, runtime);
17431
17737
  const headers = {};
17432
17738
  if (token) {
17433
17739
  headers.authorization = `Bearer ${token}`;
17434
17740
  }
17435
- const response = await runtime.fetch(`${baseUrl}${pathname}`, { headers });
17436
- const text = await response.text();
17741
+ const response = await apiFetch(pathname, parsed, runtime, { headers });
17437
17742
  if (!response.ok) {
17438
- const error51 = parseApiError(text);
17439
- throw new CliError(error51 ?? `API request failed with ${response.status}.`, 1);
17743
+ throw apiErrorFromResponse(pathname, apiBaseUrl(parsed, runtime), response.status, response.text);
17440
17744
  }
17441
- return text;
17745
+ return response.text;
17442
17746
  }
17443
17747
  async function apiPost(pathname, payload, parsed, runtime, token) {
17444
- const baseUrl = apiBaseUrl(parsed, runtime);
17445
17748
  const headers = {
17446
17749
  "content-type": "application/json"
17447
17750
  };
17448
17751
  if (token) {
17449
17752
  headers.authorization = `Bearer ${token}`;
17450
17753
  }
17451
- const response = await runtime.fetch(`${baseUrl}${pathname}`, {
17754
+ return await apiJsonRequest(pathname, parsed, runtime, {
17452
17755
  method: "POST",
17453
17756
  headers,
17454
17757
  body: JSON.stringify(payload)
17455
17758
  });
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
17759
  }
17464
17760
  async function apiPut(pathname, payload, parsed, runtime, token) {
17465
- const baseUrl = apiBaseUrl(parsed, runtime);
17466
17761
  const headers = {
17467
17762
  "content-type": "application/json"
17468
17763
  };
17469
17764
  if (token) {
17470
17765
  headers.authorization = `Bearer ${token}`;
17471
17766
  }
17472
- const response = await runtime.fetch(`${baseUrl}${pathname}`, {
17767
+ return await apiJsonRequest(pathname, parsed, runtime, {
17473
17768
  method: "PUT",
17474
17769
  headers,
17475
17770
  body: JSON.stringify(payload)
17476
17771
  });
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
17772
  }
17485
17773
  async function apiDelete(pathname, parsed, runtime, token) {
17486
- const baseUrl = apiBaseUrl(parsed, runtime);
17487
- const response = await runtime.fetch(`${baseUrl}${pathname}`, {
17774
+ return await apiJsonRequest(pathname, parsed, runtime, {
17488
17775
  method: "DELETE",
17489
17776
  headers: {
17490
17777
  authorization: `Bearer ${token}`
17491
17778
  }
17492
17779
  });
17493
- const text = await response.text();
17494
- const body = text ? JSON.parse(text) : {};
17780
+ }
17781
+ async function apiJsonRequest(pathname, parsed, runtime, init) {
17782
+ const baseUrl = apiBaseUrl(parsed, runtime);
17783
+ const response = await apiFetch(pathname, parsed, runtime, init);
17784
+ const body = parseJsonResponse(pathname, baseUrl, response.text);
17495
17785
  if (!response.ok) {
17496
- const error51 = body.error;
17497
- throw new CliError(error51?.message ?? `API request failed with ${response.status}.`, 1);
17786
+ throw apiErrorFromBody(pathname, baseUrl, response.status, body, response.text);
17498
17787
  }
17499
17788
  return body;
17500
17789
  }
17501
- function parseApiError(text) {
17790
+ async function apiFetch(pathname, parsed, runtime, init) {
17791
+ const baseUrl = apiBaseUrl(parsed, runtime);
17792
+ let response;
17793
+ try {
17794
+ response = await runtime.fetch(`${baseUrl}${pathname}`, init);
17795
+ } catch {
17796
+ throw new CliError([
17797
+ "Could not reach the MySkills API.",
17798
+ "",
17799
+ `API URL: ${baseUrl}`,
17800
+ "Check that the API is running, or use:",
17801
+ " myskills <command> --api-url https://myskills.sh/api"
17802
+ ].join("\n"), 1, "API_UNREACHABLE");
17803
+ }
17804
+ return {
17805
+ ok: response.ok,
17806
+ status: response.status,
17807
+ text: await response.text()
17808
+ };
17809
+ }
17810
+ function parseJsonResponse(pathname, baseUrl, text) {
17811
+ if (!text) {
17812
+ return {};
17813
+ }
17814
+ if (/^\s*</.test(text)) {
17815
+ throw htmlApiError(baseUrl);
17816
+ }
17817
+ try {
17818
+ const body = JSON.parse(text);
17819
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
17820
+ throw new Error("not object");
17821
+ }
17822
+ return body;
17823
+ } catch {
17824
+ throw new CliError(`API response for ${pathname} was not valid JSON.`, 1, "API_INVALID_JSON");
17825
+ }
17826
+ }
17827
+ function apiErrorFromBody(pathname, baseUrl, status, body, text) {
17828
+ if (status === 404 && isUnsupportedEndpointBody(body, text)) {
17829
+ const command = unsupportedCommandForPath(pathname);
17830
+ if (command) {
17831
+ return new CliError([
17832
+ `This MySkills server does not support the \`${command}\` command yet.`,
17833
+ "",
17834
+ `CLI version: ${CLI_VERSION}`,
17835
+ `API URL: ${baseUrl}`,
17836
+ "Run `myskills doctor` to inspect server capabilities."
17837
+ ].join("\n"), 1, "API_UNSUPPORTED_ENDPOINT", status);
17838
+ }
17839
+ }
17840
+ const error51 = body.error;
17841
+ return new CliError(error51?.message ?? `API request failed with ${status}.`, 1, error51?.code ?? "API_REQUEST_FAILED", status);
17842
+ }
17843
+ function apiErrorFromResponse(pathname, baseUrl, status, text) {
17844
+ if (/^\s*</.test(text)) {
17845
+ return htmlApiError(baseUrl);
17846
+ }
17502
17847
  try {
17503
17848
  const body = text ? JSON.parse(text) : {};
17504
- const error51 = body.error;
17505
- return error51?.message ?? null;
17849
+ return apiErrorFromBody(pathname, baseUrl, status, body, text);
17506
17850
  } catch {
17507
- return null;
17851
+ return new CliError(`API request failed with ${status}.`, 1, "API_REQUEST_FAILED", status);
17852
+ }
17853
+ }
17854
+ function htmlApiError(baseUrl) {
17855
+ return new CliError([
17856
+ "The API URL returned HTML instead of JSON.",
17857
+ "You may be pointing the CLI at the web app.",
17858
+ "",
17859
+ `Current API URL: ${baseUrl}`,
17860
+ "Try: myskills <command> --api-url https://myskills.sh/api"
17861
+ ].join("\n"), 1, "API_RETURNED_HTML");
17862
+ }
17863
+ function isUnsupportedEndpointBody(body, text) {
17864
+ return typeof body.message === "string" && /Route .+ not found/.test(body.message) || typeof body.error === "string" && body.error === "Not Found" || /Route .+ not found/.test(text);
17865
+ }
17866
+ function unsupportedCommandForPath(pathname) {
17867
+ if (pathname.startsWith("/v1/teams")) {
17868
+ return "teams";
17869
+ }
17870
+ if (pathname.includes("/sharing") || pathname.startsWith("/v1/admin/sharing")) {
17871
+ return "sharing";
17872
+ }
17873
+ return null;
17874
+ }
17875
+ function nodeVersionCheck() {
17876
+ const version2 = process.versions.node;
17877
+ const major = Number.parseInt(version2.split(".")[0] ?? "0", 10);
17878
+ return {
17879
+ name: "node",
17880
+ ok: major >= 20,
17881
+ message: `v${version2} (${major >= 20 ? "satisfies >=20" : "requires >=20"})`,
17882
+ details: { version: version2, engine: ">=20" }
17883
+ };
17884
+ }
17885
+ async function doctorHealthCheck(parsed, runtime) {
17886
+ const baseUrl = apiBaseUrl(parsed, runtime);
17887
+ try {
17888
+ const response = await apiFetch("/health", parsed, runtime);
17889
+ const body = parseJsonResponse("/health", baseUrl, response.text);
17890
+ return {
17891
+ name: "api_health",
17892
+ ok: response.ok,
17893
+ message: response.ok ? "ok" : `HTTP ${response.status}`,
17894
+ details: { status: response.status, body }
17895
+ };
17896
+ } catch (error51) {
17897
+ return {
17898
+ name: "api_health",
17899
+ ok: false,
17900
+ message: error51 instanceof Error ? firstLine(error51.message) : "failed"
17901
+ };
17508
17902
  }
17509
17903
  }
17904
+ async function doctorAuthCheck(parsed, runtime, resolved) {
17905
+ if (!resolved) {
17906
+ return {
17907
+ name: "auth",
17908
+ ok: true,
17909
+ message: "not logged in",
17910
+ details: { status: "not_logged_in" }
17911
+ };
17912
+ }
17913
+ try {
17914
+ const response = await apiGet("/v1/me", parsed, runtime, resolved.value);
17915
+ const user = response.user;
17916
+ return {
17917
+ name: "auth",
17918
+ ok: true,
17919
+ message: `${user.email ?? "unknown"} (${resolved.stored.kind}, ${resolved.source})`,
17920
+ details: {
17921
+ status: "logged_in",
17922
+ tokenSource: resolved.source,
17923
+ tokenKind: resolved.stored.kind,
17924
+ expiresAt: resolved.stored.expiresAt ?? null,
17925
+ user
17926
+ }
17927
+ };
17928
+ } catch (error51) {
17929
+ return {
17930
+ name: "auth",
17931
+ ok: false,
17932
+ message: error51 instanceof Error ? firstLine(error51.message) : "failed"
17933
+ };
17934
+ }
17935
+ }
17936
+ async function doctorTokenStoreCheck(runtime) {
17937
+ const info = await tokenStoreInfo(runtime);
17938
+ if (info.backend === "file" && info.filePath) {
17939
+ const permissions = await filePermissions(info.filePath);
17940
+ if (permissions && permissions !== "600") {
17941
+ return {
17942
+ name: "token_store",
17943
+ ok: false,
17944
+ message: `file permissions ${permissions}; expected 600`,
17945
+ details: { ...info, permissions }
17946
+ };
17947
+ }
17948
+ return {
17949
+ name: "token_store",
17950
+ ok: true,
17951
+ message: permissions ? `file ${info.filePath} (${permissions})` : `file ${info.filePath} (not created)`,
17952
+ details: { ...info, permissions }
17953
+ };
17954
+ }
17955
+ return {
17956
+ name: "token_store",
17957
+ ok: true,
17958
+ message: info.backend,
17959
+ details: info
17960
+ };
17961
+ }
17962
+ async function doctorInstallDirCheck(parsed, runtime) {
17963
+ const root = installRoot(parsed, runtime);
17964
+ const testFile = path2.join(root, ".myskills-app", "doctor-write-test");
17965
+ try {
17966
+ await mkdir(path2.dirname(testFile), { recursive: true });
17967
+ await writeFile(testFile, "ok\n", "utf8");
17968
+ await rm(testFile, { force: true });
17969
+ return {
17970
+ name: "install_dir",
17971
+ ok: true,
17972
+ message: `writable ${root}`,
17973
+ details: { path: root }
17974
+ };
17975
+ } catch (error51) {
17976
+ return {
17977
+ name: "install_dir",
17978
+ ok: false,
17979
+ message: error51 instanceof Error ? firstLine(error51.message) : "not writable",
17980
+ details: { path: root }
17981
+ };
17982
+ }
17983
+ }
17984
+ async function doctorCapabilitiesCheck(parsed, runtime) {
17985
+ try {
17986
+ const response = await apiGet("/v1/capabilities", parsed, runtime);
17987
+ const capabilities = response.capabilities && typeof response.capabilities === "object" && !Array.isArray(response.capabilities) ? response.capabilities : {};
17988
+ const supported = Object.entries(capabilities).filter(([, value]) => value === true).map(([key]) => key);
17989
+ const unsupported = Object.entries(capabilities).filter(([, value]) => value === false).map(([key]) => key);
17990
+ return {
17991
+ name: "capabilities",
17992
+ ok: true,
17993
+ message: `supported=${supported.join(",") || "-"} unsupported=${unsupported.join(",") || "-"}`,
17994
+ details: response
17995
+ };
17996
+ } catch (error51) {
17997
+ return {
17998
+ name: "capabilities",
17999
+ ok: true,
18000
+ message: `unknown (${error51 instanceof Error ? firstLine(error51.message) : "not available"})`
18001
+ };
18002
+ }
18003
+ }
18004
+ async function tokenStoreInfo(runtime) {
18005
+ return await runtime.tokenStore?.describe?.() ?? { backend: "memory" };
18006
+ }
18007
+ async function filePermissions(filePath) {
18008
+ try {
18009
+ return ((await stat(filePath)).mode & 511).toString(8).padStart(3, "0");
18010
+ } catch (error51) {
18011
+ if (isNodeError(error51) && error51.code === "ENOENT") {
18012
+ return null;
18013
+ }
18014
+ throw error51;
18015
+ }
18016
+ }
18017
+ function firstLine(message) {
18018
+ return message.split("\n")[0] ?? message;
18019
+ }
17510
18020
  function printScanResult(result, io) {
17511
18021
  if (result.findings.length === 0) {
17512
18022
  io.stdout(`clean files=${result.filesScanned} bytes=${result.bytesScanned}`);
@@ -17792,9 +18302,21 @@ function helpText() {
17792
18302
  " login --api-key [--api-url <url>]",
17793
18303
  " logout [--api-url <url>] [--token <token>]",
17794
18304
  " whoami [--api-url <url>] [--token <token>]",
18305
+ " auth status [--api-url <url>] [--token <token>]",
18306
+ " doctor [--api-url <url>] [--json]",
18307
+ " config get api-url",
18308
+ " config set api-url <url>",
18309
+ " config reset api-url",
18310
+ " config list",
17795
18311
  " submit --path <file-directory-or-zip> [--api-url <url>] [--token <token>]",
17796
18312
  " review submissions [--api-url <url>] [--token <token>]",
17797
- " review action <submission-id> --action <approve|publish> [--reason <text>]",
18313
+ " review action <submission-id> --action <approve|request-changes|reject|publish> [--reason <text>] [--api-url <url>] [--token <token>]",
18314
+ " submissions list [--api-url <url>] [--token <token>]",
18315
+ " submissions withdraw <submission-id> [--reason <text>] [--api-url <url>] [--token <token>]",
18316
+ " skills edit <skill-slug> [--title <text>] [--summary <text>] [--visibility <scope>] [--tag <tag>] [--reason <text>] [--api-url <url>] [--token <token>]",
18317
+ " skills archive|restore|delete <skill-slug> [--reason <text>] [--api-url <url>] [--token <token>]",
18318
+ " releases list <skill-slug> [--api-url <url>] [--token <token>]",
18319
+ " releases deprecate|unpublish|revoke|restore|delete <skill-slug>@<version> [--reason <text>] [--replacement <version>] [--api-url <url>] [--token <token>]",
17798
18320
  " teams list|skills [--api-url <url>] [--token <token>]",
17799
18321
  " teams create <team-name> [--name <team-name>] [--api-url <url>] [--token <token>]",
17800
18322
  " teams invite <team-id> --email <email> [--api-url <url>] [--token <token>]",
@@ -17820,15 +18342,26 @@ function helpText() {
17820
18342
  ].join("\n");
17821
18343
  }
17822
18344
  var CliError = class extends Error {
17823
- constructor(message, exitCode) {
18345
+ constructor(message, exitCode, code = "CLI_ERROR", status) {
17824
18346
  super(message);
17825
18347
  this.exitCode = exitCode;
18348
+ this.code = code;
18349
+ this.status = status;
17826
18350
  }
17827
18351
  exitCode;
18352
+ code;
18353
+ status;
18354
+ toJSON() {
18355
+ return {
18356
+ code: this.code,
18357
+ message: this.message,
18358
+ ...this.status !== void 0 ? { status: this.status } : {}
18359
+ };
18360
+ }
17828
18361
  };
17829
18362
 
17830
18363
  // src/config-store.ts
17831
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18364
+ import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
17832
18365
  import os from "node:os";
17833
18366
  import path3 from "node:path";
17834
18367
  function createFileConfigStore(env = process.env) {
@@ -17844,6 +18377,10 @@ function createFileConfigStore(env = process.env) {
17844
18377
  apiUrl: apiUrl.replace(/\/+$/, "")
17845
18378
  };
17846
18379
  writePayload(filePath, payload);
18380
+ },
18381
+ async resetApiUrl() {
18382
+ payload = { version: 1 };
18383
+ rmSync(filePath, { force: true });
17847
18384
  }
17848
18385
  };
17849
18386
  }
@@ -17922,10 +18459,17 @@ function createFileTokenStore(env = process.env) {
17922
18459
  return;
17923
18460
  }
17924
18461
  await writePayload2(filePath, payload);
18462
+ },
18463
+ describe() {
18464
+ return {
18465
+ backend: "file",
18466
+ filePath
18467
+ };
17925
18468
  }
17926
18469
  };
17927
18470
  }
17928
18471
  function createKeyringTokenStore(fallback) {
18472
+ const fallbackInfo = fallback.describe?.();
17929
18473
  return {
17930
18474
  async get(apiUrl) {
17931
18475
  const keyringToken = await readKeyringToken(apiUrl);
@@ -17940,6 +18484,12 @@ function createKeyringTokenStore(fallback) {
17940
18484
  async delete(apiUrl) {
17941
18485
  await deleteKeyringToken(apiUrl);
17942
18486
  await fallback.delete(apiUrl);
18487
+ },
18488
+ describe() {
18489
+ return {
18490
+ backend: "keyring",
18491
+ fallbackFilePath: fallbackInfo?.filePath
18492
+ };
17943
18493
  }
17944
18494
  };
17945
18495
  }
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.3",
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,