@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.
- package/README.md +27 -3
- package/dist/index.js +603 -53
- 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
|
-
`
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17436
|
-
const text = await response.text();
|
|
17741
|
+
const response = await apiFetch(pathname, parsed, runtime, { headers });
|
|
17437
17742
|
if (!response.ok) {
|
|
17438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17494
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
17505
|
-
return error51?.message ?? null;
|
|
17849
|
+
return apiErrorFromBody(pathname, baseUrl, status, body, text);
|
|
17506
17850
|
} catch {
|
|
17507
|
-
return
|
|
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