@skillmarkdown/cli 0.1.6 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,6 +74,7 @@ By default, `login` uses the project’s built-in development config. You can ov
74
74
 
75
75
  - `SKILLMD_GITHUB_CLIENT_ID`
76
76
  - `SKILLMD_FIREBASE_API_KEY`
77
+ - `SKILLMD_FIREBASE_PROJECT_ID`
77
78
 
78
79
  See `.env.example` for the expected keys.
79
80
  Maintainers: built-in defaults are defined in `src/lib/auth-defaults.ts`.
@@ -94,6 +95,10 @@ skillmd login --reauth
94
95
  skillmd logout
95
96
  ```
96
97
 
98
+ `skillmd login --status` includes the authenticated Firebase project so you can confirm whether the active session is for `skillmarkdown` or `skillmarkdown-development`.
99
+
100
+ When a saved session exists, `skillmd login` verifies the stored refresh token. If it is invalid/expired, the CLI automatically starts a new login flow. If verification is inconclusive (for example network timeout), the command exits non-zero and keeps the current session.
101
+
97
102
  ## Development
98
103
 
99
104
  - Local testing guide (includes manual `login` auth checks): `docs/testing.md`
@@ -26,16 +26,34 @@ function parseFlags(args) {
26
26
  }
27
27
  return { status, reauth, valid: true };
28
28
  }
29
- function printSessionStatus(session) {
29
+ function formatSessionProject(session, currentConfigProjectId) {
30
+ if (!session.projectId) {
31
+ if (currentConfigProjectId) {
32
+ return { label: `unknown (current config: ${currentConfigProjectId})`, mismatch: false };
33
+ }
34
+ return { label: "unknown", mismatch: false };
35
+ }
36
+ return {
37
+ label: session.projectId,
38
+ mismatch: Boolean(currentConfigProjectId && session.projectId !== currentConfigProjectId),
39
+ };
40
+ }
41
+ function printSessionStatus(session, currentConfigProjectId) {
30
42
  if (!session) {
31
43
  console.log("Not logged in.");
32
44
  return 1;
33
45
  }
46
+ const project = formatSessionProject(session, currentConfigProjectId);
34
47
  if (session.email) {
35
- console.log(`Logged in with GitHub as ${session.email}.`);
36
- return 0;
48
+ console.log(`Logged in with GitHub as ${session.email} (project: ${project.label}).`);
49
+ }
50
+ else {
51
+ console.log(`Logged in with GitHub (uid: ${session.uid}, project: ${project.label}).`);
52
+ }
53
+ if (project.mismatch && currentConfigProjectId) {
54
+ console.log(`Current CLI config targets project '${currentConfigProjectId}'. ` +
55
+ "Run 'skillmd login --reauth' to switch projects.");
37
56
  }
38
- console.log(`Logged in with GitHub (uid: ${session.uid}).`);
39
57
  return 0;
40
58
  }
41
59
  function requireConfig(env) {
@@ -56,21 +74,43 @@ async function runLoginCommand(args, options = {}) {
56
74
  }
57
75
  const readSessionFn = options.readSession ?? auth_session_1.readAuthSession;
58
76
  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
- }
77
+ const clearSessionFn = options.clearSession ?? auth_session_1.clearAuthSession;
72
78
  try {
73
79
  const config = requireConfig(options.env ?? process.env);
80
+ if (status) {
81
+ return printSessionStatus(readSessionFn(), config.firebaseProjectId);
82
+ }
83
+ const existingSession = readSessionFn();
84
+ if (existingSession && !reauth) {
85
+ const verifyRefreshTokenFn = options.verifyRefreshToken ?? firebase_auth_1.verifyFirebaseRefreshToken;
86
+ try {
87
+ const validation = await verifyRefreshTokenFn(config.firebaseApiKey, existingSession.refreshToken);
88
+ if (validation.valid) {
89
+ const project = formatSessionProject(existingSession, config.firebaseProjectId);
90
+ if (existingSession.email) {
91
+ console.log(`Already logged in as ${existingSession.email} (project: ${project.label}). ` +
92
+ "Run 'skillmd logout' first.");
93
+ }
94
+ else {
95
+ console.log(`Already logged in (uid: ${existingSession.uid}, project: ${project.label}). ` +
96
+ "Run 'skillmd logout' first.");
97
+ }
98
+ if (project.mismatch) {
99
+ console.log(`Current CLI config targets project '${config.firebaseProjectId}'. ` +
100
+ "Run 'skillmd login --reauth' to switch projects.");
101
+ }
102
+ return 0;
103
+ }
104
+ clearSessionFn();
105
+ console.log("Existing session is no longer valid. Starting re-authentication.");
106
+ }
107
+ catch (error) {
108
+ const message = error instanceof Error ? error.message : "Unknown error";
109
+ console.error(`skillmd login: unable to verify existing session (${message}). ` +
110
+ "Keeping current session. Run 'skillmd login --reauth' to force reauthentication.");
111
+ return 1;
112
+ }
113
+ }
74
114
  const requestDeviceCodeFn = options.requestDeviceCode ?? github_device_flow_1.requestDeviceCode;
75
115
  const pollForAccessTokenFn = options.pollForAccessToken ?? github_device_flow_1.pollForAccessToken;
76
116
  const signInFn = options.signInWithGitHubAccessToken ?? firebase_auth_1.signInWithGitHubAccessToken;
@@ -85,12 +125,13 @@ async function runLoginCommand(args, options = {}) {
85
125
  uid: firebaseSession.localId,
86
126
  email: firebaseSession.email,
87
127
  refreshToken: firebaseSession.refreshToken,
128
+ projectId: config.firebaseProjectId,
88
129
  });
89
130
  if (firebaseSession.email) {
90
- console.log(`Login successful. Signed in as ${firebaseSession.email}.`);
131
+ console.log(`Login successful. Signed in as ${firebaseSession.email} (project: ${config.firebaseProjectId}).`);
91
132
  }
92
133
  else {
93
- console.log("Login successful.");
134
+ console.log(`Login successful (project: ${config.firebaseProjectId}).`);
94
135
  }
95
136
  return 0;
96
137
  }
@@ -54,12 +54,14 @@ function getLoginEnvConfig(env = process.env, options = {}) {
54
54
  const dotEnv = loadDotEnv(getDefaultUserEnvPath(options));
55
55
  const githubClientId = pickValue(env.SKILLMD_GITHUB_CLIENT_ID, dotEnv.SKILLMD_GITHUB_CLIENT_ID, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.githubClientId);
56
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) {
57
+ const firebaseProjectId = pickValue(env.SKILLMD_FIREBASE_PROJECT_ID, dotEnv.SKILLMD_FIREBASE_PROJECT_ID, auth_defaults_1.DEFAULT_LOGIN_AUTH_CONFIG.firebaseProjectId);
58
+ if (!githubClientId || !firebaseApiKey || !firebaseProjectId) {
58
59
  throw new Error("missing login configuration");
59
60
  }
60
61
  return {
61
62
  githubClientId,
62
63
  firebaseApiKey,
64
+ firebaseProjectId,
63
65
  };
64
66
  }
65
67
  function getDefaultUserEnvPath(options = {}) {
@@ -5,4 +5,5 @@ exports.DEFAULT_LOGIN_AUTH_CONFIG = void 0;
5
5
  exports.DEFAULT_LOGIN_AUTH_CONFIG = Object.freeze({
6
6
  githubClientId: "Ov23lixkdtyLp35IFaBG",
7
7
  firebaseApiKey: "AIzaSyAkaZRmpCvZasFjeRAfW_b0V0nUcGOTjok",
8
+ firebaseProjectId: "skillmarkdown",
8
9
  });
@@ -28,6 +28,12 @@ function readAuthSession(sessionPath = SESSION_PATH) {
28
28
  if (parsed.email !== undefined && typeof parsed.email !== "string") {
29
29
  return null;
30
30
  }
31
+ if (parsed.projectId !== undefined && typeof parsed.projectId !== "string") {
32
+ return null;
33
+ }
34
+ if (typeof parsed.projectId === "string" && parsed.projectId.length === 0) {
35
+ return null;
36
+ }
31
37
  return parsed;
32
38
  }
33
39
  catch {
@@ -1,8 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.signInWithGitHubAccessToken = signInWithGitHubAccessToken;
4
+ exports.verifyFirebaseRefreshToken = verifyFirebaseRefreshToken;
4
5
  const http_1 = require("./http");
5
6
  const FIREBASE_HTTP_TIMEOUT_MS = 10000;
7
+ async function parseJsonApiResponse(response, apiLabel) {
8
+ const text = await response.text();
9
+ try {
10
+ return JSON.parse(text);
11
+ }
12
+ catch {
13
+ throw new Error(`${apiLabel} returned non-JSON response (${response.status})`);
14
+ }
15
+ }
6
16
  async function signInWithGitHubAccessToken(apiKey, githubAccessToken) {
7
17
  const response = await (0, http_1.fetchWithTimeout)(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=${encodeURIComponent(apiKey)}`, {
8
18
  method: "POST",
@@ -16,14 +26,7 @@ async function signInWithGitHubAccessToken(apiKey, githubAccessToken) {
16
26
  postBody: `access_token=${encodeURIComponent(githubAccessToken)}&providerId=github.com`,
17
27
  }),
18
28
  }, { 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
- }
29
+ const payload = await parseJsonApiResponse(response, "Firebase auth API");
27
30
  if (!response.ok) {
28
31
  const message = payload.error?.message || "Firebase signInWithIdp request failed";
29
32
  throw new Error(`Firebase auth error: ${message}`);
@@ -37,3 +40,30 @@ async function signInWithGitHubAccessToken(apiKey, githubAccessToken) {
37
40
  refreshToken: payload.refreshToken,
38
41
  };
39
42
  }
43
+ async function verifyFirebaseRefreshToken(apiKey, refreshToken) {
44
+ const response = await (0, http_1.fetchWithTimeout)(`https://securetoken.googleapis.com/v1/token?key=${encodeURIComponent(apiKey)}`, {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/x-www-form-urlencoded",
48
+ },
49
+ body: new URLSearchParams({
50
+ grant_type: "refresh_token",
51
+ refresh_token: refreshToken,
52
+ }),
53
+ }, { timeoutMs: FIREBASE_HTTP_TIMEOUT_MS });
54
+ const payload = await parseJsonApiResponse(response, "Firebase token API");
55
+ if (response.ok) {
56
+ if (!payload.refresh_token || !payload.access_token) {
57
+ throw new Error("Firebase token API response was missing required fields");
58
+ }
59
+ return { valid: true };
60
+ }
61
+ const errorMessage = payload.error?.message ?? "";
62
+ if (response.status === 400 &&
63
+ (errorMessage === "INVALID_REFRESH_TOKEN" ||
64
+ errorMessage === "TOKEN_EXPIRED" ||
65
+ errorMessage === "PROJECT_NUMBER_MISMATCH")) {
66
+ return { valid: false };
67
+ }
68
+ throw new Error(`Firebase token verification failed (${response.status}): ${errorMessage || "unknown error"}`);
69
+ }
@@ -53,11 +53,12 @@ function sleep(ms) {
53
53
  setTimeout(resolve, ms);
54
54
  });
55
55
  }
56
- async function pollForAccessToken(clientId, deviceCode, intervalSeconds, expiresInSeconds) {
56
+ async function pollForAccessToken(clientId, deviceCode, intervalSeconds, expiresInSeconds, options = {}) {
57
57
  const startedAt = Date.now();
58
58
  let pollInterval = Math.max(1, intervalSeconds);
59
+ const sleepFn = options.sleep ?? sleep;
59
60
  while (Date.now() - startedAt < expiresInSeconds * 1000) {
60
- await sleep(pollInterval * 1000);
61
+ await sleepFn(pollInterval * 1000);
61
62
  const payload = await postGitHubForm(GITHUB_ACCESS_TOKEN_URL, new URLSearchParams({
62
63
  client_id: clientId,
63
64
  device_code: deviceCode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillmarkdown/cli",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for scaffolding SKILL.md-based AI skills",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",