@skillmarkdown/cli 0.1.5 → 0.1.6

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## Status
6
6
 
7
- Early development. v0 focuses on `skillmd init` and `skillmd validate`. Docs are intentionally lightweight and may evolve.
7
+ Early development. Current command surface includes `skillmd init`, `skillmd validate`, `skillmd login`, and `skillmd logout`. Docs are intentionally lightweight and may evolve.
8
8
 
9
9
  ## Install
10
10
 
@@ -64,9 +64,39 @@ Compare local validation with `skills-ref` (when installed):
64
64
  skillmd validate --parity
65
65
  ```
66
66
 
67
+ ### Login with GitHub (Device Flow)
68
+
69
+ ```bash
70
+ skillmd login
71
+ ```
72
+
73
+ By default, `login` uses the project’s built-in development config. You can override values with shell env vars or `~/.skillmd/.env`:
74
+
75
+ - `SKILLMD_GITHUB_CLIENT_ID`
76
+ - `SKILLMD_FIREBASE_API_KEY`
77
+
78
+ See `.env.example` for the expected keys.
79
+ Maintainers: built-in defaults are defined in `src/lib/auth-defaults.ts`.
80
+
81
+ Example override file:
82
+
83
+ ```bash
84
+ mkdir -p ~/.skillmd
85
+ cp .env.example ~/.skillmd/.env
86
+ # then edit values in ~/.skillmd/.env if needed
87
+ ```
88
+
89
+ Session helpers:
90
+
91
+ ```bash
92
+ skillmd login --status
93
+ skillmd login --reauth
94
+ skillmd logout
95
+ ```
96
+
67
97
  ## Development
68
98
 
69
- - Local testing guide: `docs/testing.md`
99
+ - Local testing guide (includes manual `login` auth checks): `docs/testing.md`
70
100
  - CI check script: `npm run ci:check`
71
101
  - Packed tarball smoke test: `npm run smoke:pack`
72
102
  - Optional npm link smoke test: `npm run smoke:link`
package/dist/cli.js CHANGED
@@ -2,13 +2,17 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const init_1 = require("./commands/init");
5
+ const login_1 = require("./commands/login");
6
+ const logout_1 = require("./commands/logout");
5
7
  const validate_1 = require("./commands/validate");
6
8
  const cli_text_1 = require("./lib/cli-text");
7
9
  const COMMAND_HANDLERS = {
8
10
  init: init_1.runInitCommand,
9
11
  validate: validate_1.runValidateCommand,
12
+ login: login_1.runLoginCommand,
13
+ logout: logout_1.runLogoutCommand,
10
14
  };
11
- function main() {
15
+ async function main() {
12
16
  const args = process.argv.slice(2);
13
17
  const command = args[0];
14
18
  if (args.length === 0) {
@@ -19,11 +23,11 @@ function main() {
19
23
  }
20
24
  const handler = COMMAND_HANDLERS[command];
21
25
  if (handler) {
22
- process.exitCode = handler(args.slice(1));
26
+ process.exitCode = await handler(args.slice(1));
23
27
  return;
24
28
  }
25
29
  console.error(`skillmd: unknown command '${command}'`);
26
30
  console.error(cli_text_1.ROOT_USAGE);
27
31
  process.exitCode = 1;
28
32
  }
29
- main();
33
+ void main();
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runLoginCommand = runLoginCommand;
4
+ const cli_text_1 = require("../lib/cli-text");
5
+ const command_output_1 = require("../lib/command-output");
6
+ const auth_config_1 = require("../lib/auth-config");
7
+ const auth_session_1 = require("../lib/auth-session");
8
+ const github_device_flow_1 = require("../lib/github-device-flow");
9
+ const firebase_auth_1 = require("../lib/firebase-auth");
10
+ function parseFlags(args) {
11
+ let status = false;
12
+ let reauth = false;
13
+ for (const arg of args) {
14
+ if (arg === "--status") {
15
+ status = true;
16
+ continue;
17
+ }
18
+ if (arg === "--reauth") {
19
+ reauth = true;
20
+ continue;
21
+ }
22
+ return { status: false, reauth: false, valid: false };
23
+ }
24
+ if (status && reauth) {
25
+ return { status: false, reauth: false, valid: false };
26
+ }
27
+ return { status, reauth, valid: true };
28
+ }
29
+ function printSessionStatus(session) {
30
+ if (!session) {
31
+ console.log("Not logged in.");
32
+ return 1;
33
+ }
34
+ if (session.email) {
35
+ console.log(`Logged in with GitHub as ${session.email}.`);
36
+ return 0;
37
+ }
38
+ console.log(`Logged in with GitHub (uid: ${session.uid}).`);
39
+ return 0;
40
+ }
41
+ function requireConfig(env) {
42
+ try {
43
+ return (0, auth_config_1.getLoginEnvConfig)(env);
44
+ }
45
+ catch (error) {
46
+ const message = error instanceof Error ? error.message : "invalid login configuration";
47
+ const wrapped = new Error(`${message}.`);
48
+ wrapped.cause = error;
49
+ throw wrapped;
50
+ }
51
+ }
52
+ async function runLoginCommand(args, options = {}) {
53
+ const { status, reauth, valid } = parseFlags(args);
54
+ if (!valid) {
55
+ return (0, command_output_1.failWithUsage)("skillmd login: unsupported argument(s)", cli_text_1.LOGIN_USAGE);
56
+ }
57
+ const readSessionFn = options.readSession ?? auth_session_1.readAuthSession;
58
+ const writeSessionFn = options.writeSession ?? auth_session_1.writeAuthSession;
59
+ if (status) {
60
+ return printSessionStatus(readSessionFn());
61
+ }
62
+ const existingSession = readSessionFn();
63
+ if (existingSession && !reauth) {
64
+ if (existingSession.email) {
65
+ console.log(`Already logged in as ${existingSession.email}. Run 'skillmd logout' first.`);
66
+ }
67
+ else {
68
+ console.log("Already logged in. Run 'skillmd logout' first.");
69
+ }
70
+ return 0;
71
+ }
72
+ try {
73
+ const config = requireConfig(options.env ?? process.env);
74
+ const requestDeviceCodeFn = options.requestDeviceCode ?? github_device_flow_1.requestDeviceCode;
75
+ const pollForAccessTokenFn = options.pollForAccessToken ?? github_device_flow_1.pollForAccessToken;
76
+ const signInFn = options.signInWithGitHubAccessToken ?? firebase_auth_1.signInWithGitHubAccessToken;
77
+ const deviceCode = await requestDeviceCodeFn(config.githubClientId);
78
+ console.log("Open this URL in your browser to authorize skillmd:");
79
+ console.log(deviceCode.verificationUriComplete ?? deviceCode.verificationUri);
80
+ console.log(`Then enter code: ${deviceCode.userCode}`);
81
+ const token = await pollForAccessTokenFn(config.githubClientId, deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
82
+ const firebaseSession = await signInFn(config.firebaseApiKey, token.accessToken);
83
+ writeSessionFn({
84
+ provider: "github",
85
+ uid: firebaseSession.localId,
86
+ email: firebaseSession.email,
87
+ refreshToken: firebaseSession.refreshToken,
88
+ });
89
+ if (firebaseSession.email) {
90
+ console.log(`Login successful. Signed in as ${firebaseSession.email}.`);
91
+ }
92
+ else {
93
+ console.log("Login successful.");
94
+ }
95
+ return 0;
96
+ }
97
+ catch (error) {
98
+ const message = error instanceof Error ? error.message : "Unknown error";
99
+ console.error(`skillmd login: ${message}`);
100
+ return 1;
101
+ }
102
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runLogoutCommand = runLogoutCommand;
4
+ const cli_text_1 = require("../lib/cli-text");
5
+ const command_output_1 = require("../lib/command-output");
6
+ const auth_session_1 = require("../lib/auth-session");
7
+ function runLogoutCommand(args, options = {}) {
8
+ if (args.length > 0) {
9
+ return (0, command_output_1.failWithUsage)("skillmd logout: unsupported argument(s)", cli_text_1.LOGOUT_USAGE);
10
+ }
11
+ const clearSessionFn = options.clearSession ?? auth_session_1.clearAuthSession;
12
+ const removed = clearSessionFn();
13
+ if (removed) {
14
+ console.log("Logged out.");
15
+ return 0;
16
+ }
17
+ console.log("No active session to log out.");
18
+ return 0;
19
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLoginEnvConfig = getLoginEnvConfig;
4
+ exports.getDefaultUserEnvPath = getDefaultUserEnvPath;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
8
+ const auth_defaults_1 = require("./auth-defaults");
9
+ const USER_ENV_RELATIVE_PATH = ".skillmd/.env";
10
+ function parseDotEnv(content) {
11
+ const values = {};
12
+ for (const rawLine of content.split(/\r?\n/)) {
13
+ const line = rawLine.trim();
14
+ if (!line || line.startsWith("#")) {
15
+ continue;
16
+ }
17
+ const equalIndex = line.indexOf("=");
18
+ if (equalIndex <= 0) {
19
+ continue;
20
+ }
21
+ const key = line.slice(0, equalIndex).trim();
22
+ if (!key) {
23
+ continue;
24
+ }
25
+ let value = line.slice(equalIndex + 1).trim();
26
+ if ((value.startsWith('"') && value.endsWith('"')) ||
27
+ (value.startsWith("'") && value.endsWith("'"))) {
28
+ value = value.slice(1, -1);
29
+ }
30
+ values[key] = value;
31
+ }
32
+ return values;
33
+ }
34
+ function loadDotEnv(dotEnvPath) {
35
+ if (!(0, node_fs_1.existsSync)(dotEnvPath)) {
36
+ return {};
37
+ }
38
+ try {
39
+ return parseDotEnv((0, node_fs_1.readFileSync)(dotEnvPath, "utf8"));
40
+ }
41
+ catch {
42
+ return {};
43
+ }
44
+ }
45
+ function pickValue(...candidates) {
46
+ for (const candidate of candidates) {
47
+ if (candidate && candidate.trim()) {
48
+ return candidate.trim();
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+ function getLoginEnvConfig(env = process.env, options = {}) {
54
+ const dotEnv = loadDotEnv(getDefaultUserEnvPath(options));
55
+ const githubClientId = pickValue(env.SKILLMD_GITHUB_CLIENT_ID, dotEnv.SKILLMD_GITHUB_CLIENT_ID, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.githubClientId);
56
+ const firebaseApiKey = pickValue(env.SKILLMD_FIREBASE_API_KEY, dotEnv.SKILLMD_FIREBASE_API_KEY, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.firebaseApiKey);
57
+ if (!githubClientId || !firebaseApiKey) {
58
+ throw new Error("missing login configuration");
59
+ }
60
+ return {
61
+ githubClientId,
62
+ firebaseApiKey,
63
+ };
64
+ }
65
+ function getDefaultUserEnvPath(options = {}) {
66
+ return (0, node_path_1.join)(options.homeDir ?? (0, node_os_1.homedir)(), USER_ENV_RELATIVE_PATH);
67
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_LOGIN_AUTH_CONFIG = void 0;
4
+ // Single source of truth for built-in auth defaults.
5
+ exports.DEFAULT_LOGIN_AUTH_CONFIG = Object.freeze({
6
+ githubClientId: "Ov23lixkdtyLp35IFaBG",
7
+ firebaseApiKey: "AIzaSyAkaZRmpCvZasFjeRAfW_b0V0nUcGOTjok",
8
+ });
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDefaultSessionPath = getDefaultSessionPath;
4
+ exports.readAuthSession = readAuthSession;
5
+ exports.writeAuthSession = writeAuthSession;
6
+ exports.clearAuthSession = clearAuthSession;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = require("node:os");
9
+ const node_path_1 = require("node:path");
10
+ const SESSION_PATH = (0, node_path_1.join)((0, node_os_1.homedir)(), ".skillmd", "auth.json");
11
+ function getDefaultSessionPath() {
12
+ return SESSION_PATH;
13
+ }
14
+ function readAuthSession(sessionPath = SESSION_PATH) {
15
+ if (!(0, node_fs_1.existsSync)(sessionPath)) {
16
+ return null;
17
+ }
18
+ try {
19
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(sessionPath, "utf8"));
20
+ if (!parsed ||
21
+ parsed.provider !== "github" ||
22
+ typeof parsed.uid !== "string" ||
23
+ parsed.uid.length === 0 ||
24
+ typeof parsed.refreshToken !== "string" ||
25
+ parsed.refreshToken.length === 0) {
26
+ return null;
27
+ }
28
+ if (parsed.email !== undefined && typeof parsed.email !== "string") {
29
+ return null;
30
+ }
31
+ return parsed;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ function writeAuthSession(session, sessionPath = SESSION_PATH) {
38
+ const parentDir = (0, node_path_1.dirname)(sessionPath);
39
+ (0, node_fs_1.mkdirSync)(parentDir, { recursive: true });
40
+ (0, node_fs_1.writeFileSync)(sessionPath, JSON.stringify(session, null, 2), { encoding: "utf8", mode: 0o600 });
41
+ (0, node_fs_1.chmodSync)(sessionPath, 0o600);
42
+ }
43
+ function clearAuthSession(sessionPath = SESSION_PATH) {
44
+ if (!(0, node_fs_1.existsSync)(sessionPath)) {
45
+ return false;
46
+ }
47
+ (0, node_fs_1.rmSync)(sessionPath, { force: true });
48
+ return true;
49
+ }
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VALIDATE_USAGE = exports.INIT_USAGE = exports.ROOT_USAGE = void 0;
4
- exports.ROOT_USAGE = "Usage: skillmd <init|validate>";
3
+ exports.LOGOUT_USAGE = exports.LOGIN_USAGE = exports.VALIDATE_USAGE = exports.INIT_USAGE = exports.ROOT_USAGE = void 0;
4
+ exports.ROOT_USAGE = "Usage: skillmd <init|validate|login|logout>";
5
5
  exports.INIT_USAGE = "Usage: skillmd init [--no-validate]";
6
6
  exports.VALIDATE_USAGE = "Usage: skillmd validate [path] [--strict] [--parity]";
7
+ exports.LOGIN_USAGE = "Usage: skillmd login [--status|--reauth]";
8
+ exports.LOGOUT_USAGE = "Usage: skillmd logout";
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.signInWithGitHubAccessToken = signInWithGitHubAccessToken;
4
+ const http_1 = require("./http");
5
+ const FIREBASE_HTTP_TIMEOUT_MS = 10000;
6
+ async function signInWithGitHubAccessToken(apiKey, githubAccessToken) {
7
+ const response = await (0, http_1.fetchWithTimeout)(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=${encodeURIComponent(apiKey)}`, {
8
+ method: "POST",
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ },
12
+ body: JSON.stringify({
13
+ requestUri: "http://localhost",
14
+ returnSecureToken: true,
15
+ returnIdpCredential: true,
16
+ postBody: `access_token=${encodeURIComponent(githubAccessToken)}&providerId=github.com`,
17
+ }),
18
+ }, { timeoutMs: FIREBASE_HTTP_TIMEOUT_MS });
19
+ const text = await response.text();
20
+ let payload;
21
+ try {
22
+ payload = JSON.parse(text);
23
+ }
24
+ catch {
25
+ throw new Error(`Firebase auth API returned non-JSON response (${response.status})`);
26
+ }
27
+ if (!response.ok) {
28
+ const message = payload.error?.message || "Firebase signInWithIdp request failed";
29
+ throw new Error(`Firebase auth error: ${message}`);
30
+ }
31
+ if (!payload.localId || !payload.refreshToken) {
32
+ throw new Error("Firebase auth response was missing required fields");
33
+ }
34
+ return {
35
+ localId: payload.localId,
36
+ email: payload.email,
37
+ refreshToken: payload.refreshToken,
38
+ };
39
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestDeviceCode = requestDeviceCode;
4
+ exports.pollForAccessToken = pollForAccessToken;
5
+ const http_1 = require("./http");
6
+ const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
7
+ const GITHUB_ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
8
+ const GITHUB_HTTP_TIMEOUT_MS = 10000;
9
+ async function postGitHubForm(url, form) {
10
+ const response = await (0, http_1.fetchWithTimeout)(url, {
11
+ method: "POST",
12
+ headers: {
13
+ Accept: "application/json",
14
+ "Content-Type": "application/x-www-form-urlencoded",
15
+ },
16
+ body: form,
17
+ }, { timeoutMs: GITHUB_HTTP_TIMEOUT_MS });
18
+ const text = await response.text();
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(text);
22
+ }
23
+ catch {
24
+ throw new Error(`GitHub API returned non-JSON response (${response.status})`);
25
+ }
26
+ if (!response.ok) {
27
+ throw new Error(`GitHub API request failed (${response.status})`);
28
+ }
29
+ return parsed;
30
+ }
31
+ async function requestDeviceCode(clientId, scope = "read:user user:email") {
32
+ const payload = await postGitHubForm(GITHUB_DEVICE_CODE_URL, new URLSearchParams({
33
+ client_id: clientId,
34
+ scope,
35
+ }));
36
+ if (!payload.device_code ||
37
+ !payload.user_code ||
38
+ !payload.verification_uri ||
39
+ !payload.expires_in) {
40
+ throw new Error("GitHub device code response was missing required fields");
41
+ }
42
+ return {
43
+ deviceCode: payload.device_code,
44
+ userCode: payload.user_code,
45
+ verificationUri: payload.verification_uri,
46
+ verificationUriComplete: payload.verification_uri_complete,
47
+ expiresIn: payload.expires_in,
48
+ interval: payload.interval ?? 5,
49
+ };
50
+ }
51
+ function sleep(ms) {
52
+ return new Promise((resolve) => {
53
+ setTimeout(resolve, ms);
54
+ });
55
+ }
56
+ async function pollForAccessToken(clientId, deviceCode, intervalSeconds, expiresInSeconds) {
57
+ const startedAt = Date.now();
58
+ let pollInterval = Math.max(1, intervalSeconds);
59
+ while (Date.now() - startedAt < expiresInSeconds * 1000) {
60
+ await sleep(pollInterval * 1000);
61
+ const payload = await postGitHubForm(GITHUB_ACCESS_TOKEN_URL, new URLSearchParams({
62
+ client_id: clientId,
63
+ device_code: deviceCode,
64
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
65
+ }));
66
+ if (payload.access_token) {
67
+ return { accessToken: payload.access_token };
68
+ }
69
+ if (!payload.error || payload.error === "authorization_pending") {
70
+ continue;
71
+ }
72
+ if (payload.error === "slow_down") {
73
+ pollInterval += 5;
74
+ continue;
75
+ }
76
+ if (payload.error === "expired_token") {
77
+ throw new Error("GitHub device code expired before authorization completed");
78
+ }
79
+ if (payload.error === "access_denied") {
80
+ throw new Error("GitHub authorization was denied by the user");
81
+ }
82
+ throw new Error(payload.error_description || `GitHub OAuth error: ${payload.error}`);
83
+ }
84
+ throw new Error("GitHub device login timed out");
85
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchWithTimeout = fetchWithTimeout;
4
+ const DEFAULT_TIMEOUT_MS = 10000;
5
+ async function fetchWithTimeout(input, init = {}, options = {}) {
6
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
7
+ const controller = new AbortController();
8
+ const userSignal = init.signal;
9
+ let abortListener;
10
+ if (userSignal) {
11
+ if (userSignal.aborted) {
12
+ controller.abort(userSignal.reason);
13
+ }
14
+ else {
15
+ abortListener = () => {
16
+ controller.abort(userSignal.reason);
17
+ };
18
+ userSignal.addEventListener("abort", abortListener, { once: true });
19
+ }
20
+ }
21
+ const timeout = setTimeout(() => {
22
+ controller.abort(new Error(`request timed out after ${timeoutMs}ms`));
23
+ }, timeoutMs);
24
+ try {
25
+ return await fetch(input, {
26
+ ...init,
27
+ signal: controller.signal,
28
+ });
29
+ }
30
+ catch (error) {
31
+ if (error instanceof Error && error.name === "AbortError" && !userSignal?.aborted) {
32
+ const timeoutError = new Error(`request timed out after ${timeoutMs}ms`);
33
+ timeoutError.cause = error;
34
+ throw timeoutError;
35
+ }
36
+ throw error;
37
+ }
38
+ finally {
39
+ clearTimeout(timeout);
40
+ if (userSignal && abortListener) {
41
+ userSignal.removeEventListener("abort", abortListener);
42
+ }
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillmarkdown/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "CLI for scaffolding SKILL.md-based AI skills",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",