@elixpo/lixblogs-cli 1.3.1 → 1.3.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 CHANGED
@@ -29,6 +29,7 @@ node bin/lixblogs.mjs --help
29
29
  ```bash
30
30
  # Log in via device authorization
31
31
  node bin/lixblogs.mjs login
32
+ # Credentials are saved under the authenticated username and made active.
32
33
 
33
34
  # Save credentials under an explicit local profile name
34
35
  node bin/lixblogs.mjs login --profile personal
@@ -51,8 +52,10 @@ Interactive login shows one compact device card and stays quiet while Accounts
51
52
  approval is pending. Press Enter to open the verification URL locally, use
52
53
  `--open` to open it immediately, or copy the URL to any browser when connected
53
54
  to a VPS. Device authorization does not require a localhost callback or an
54
- exposed port. Profile names such as `personal`, `work`, or `test` are local
55
- credential slots; `whoami` reports the actual Accounts identity.
55
+ exposed port. The authenticated username becomes the local profile alias unless
56
+ `--profile` explicitly overrides it. Run `lixblogs login` again to add another
57
+ account, `lixblogs profiles` to list saved accounts, and
58
+ `lixblogs use <username>` to switch the active one.
56
59
 
57
60
  ### Blog lifecycle
58
61
 
@@ -148,7 +151,7 @@ command exits with code 3 and retains both versions under
148
151
  `.lixblogs-conflicts/`; it never overwrites the newer server revision.
149
152
 
150
153
  Global flags:
151
- - `--profile <name>` — named profile to use (default: `"default"`)
154
+ - `--profile <name>` — override the username-based local account alias
152
155
  - `--env <environment>` — override environment (`development` | `staging` | `production`)
153
156
  - `--scope <scope>` — request an additional/alternate OAuth scope; repeatable
154
157
  - `--open` — open the verification URL with the device code pre-filled
package/bin/lixblogs.mjs CHANGED
@@ -31,6 +31,7 @@ import { authStatus } from "../src/commands/auth/status.js";
31
31
  import { authLogout } from "../src/commands/auth/logout.js";
32
32
  import { authRevoke } from "../src/commands/auth/revoke.js";
33
33
  import { authProfiles, authUse } from "../src/commands/auth/profiles.js";
34
+ import { profileAliasFromIdentity } from "../src/commands/auth/profileAlias.js";
34
35
  import { ProfileRegistry, validateProfileId } from "../src/config/ProfileRegistry.js";
35
36
  import { AuthenticatedClient } from "../src/auth/AuthenticatedClient.js";
36
37
  import { BlogClient, BlogApiError } from "../src/api/BlogClient.js";
@@ -164,7 +165,7 @@ Usage:
164
165
  lixblogs skill install <name> [--target <directory>] [--dry-run] --yes
165
166
 
166
167
  Global flags:
167
- --profile <name> named profile to use (default: "default")
168
+ --profile <name> local account alias (defaults to the signed-in username)
168
169
  --env <environment> override environment (development|staging|production)
169
170
  --auth-provider <provider> elixpo, or mock in development/test only
170
171
  --accounts-url <url> override the Accounts discovery origin
@@ -269,7 +270,11 @@ async function getCredentialStoreOrFail(opts, profileRegistry) {
269
270
  async function runLogin(opts) {
270
271
  const config = resolveConfig({ flags: configFlags(opts) });
271
272
  const profileRegistry = new ProfileRegistry();
272
- const profileId = await selectedProfile(config, profileRegistry);
273
+ const requestedProfileId = validateProfileId(config.profile);
274
+ const scopes = opts.scope?.length ? [...opts.scope] : [...DEFAULT_SCOPES];
275
+ if (!config.profileExplicit && !scopes.includes("lixblogs:profile:read")) {
276
+ scopes.push("lixblogs:profile:read");
277
+ }
273
278
 
274
279
  let provider;
275
280
  try {
@@ -287,9 +292,15 @@ async function runLogin(opts) {
287
292
  result = await authLogin({
288
293
  provider,
289
294
  credentialStore,
290
- profileId,
291
- scopes: opts.scope?.length ? opts.scope : DEFAULT_SCOPES,
295
+ profileId: requestedProfileId,
296
+ scopes,
292
297
  openBrowser: opts.open ? openBrowser : undefined,
298
+ resolveProfileId: config.profileExplicit
299
+ ? undefined
300
+ : ({ accessToken }) => profileAliasFromIdentity({
301
+ accessToken,
302
+ apiBaseUrl: config.apiBaseUrl,
303
+ }),
293
304
  onStatus: (status) => {
294
305
  if (opts.json) {
295
306
  if (status.type !== "pending") output(opts, { event: status.type, ...status });
@@ -303,7 +314,7 @@ async function runLogin(opts) {
303
314
  url,
304
315
  code: status.userCode,
305
316
  expiresInSeconds: status.expiresInSeconds,
306
- profile: profileId,
317
+ profile: config.profileExplicit ? requestedProfileId : null,
307
318
  interactive,
308
319
  color: colorEnabled(),
309
320
  }));
@@ -331,6 +342,8 @@ async function runLogin(opts) {
331
342
  output(opts, { ok: true, profile: result.profileId });
332
343
  if (!opts.json && !opts.quiet) {
333
344
  console.log(` Credentials saved to local profile "${result.profileId}".`);
345
+ console.log(" Tip: add another account with `lixblogs login`, list accounts with `lixblogs profiles`,");
346
+ console.log(" and switch with `lixblogs use <username>`.");
334
347
  }
335
348
  }
336
349
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixblogs-cli",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Official CLI for LixBlogs — publish, manage, and inspect blogs through the supported API. Built for creators and agent automation.",
5
5
  "author": {
6
6
  "name": "Elixpo",
package/src/cli/ui.js CHANGED
@@ -27,7 +27,9 @@ export function loginChallenge({ url, code, expiresInSeconds, profile, interacti
27
27
  ` URL ${url}`,
28
28
  ` Code ${paint(code, ANSI.bold, color)}`,
29
29
  ` Expires ${Math.ceil(expiresInSeconds / 60)} min`,
30
- ` Profile ${profile} ${paint("(local credential slot)", ANSI.dim, color)}`,
30
+ profile
31
+ ? ` Profile ${profile} ${paint("(local credential slot)", ANSI.dim, color)}`
32
+ : ` Profile ${paint("your Accounts username after approval", ANSI.dim, color)}`,
31
33
  "",
32
34
  ` ${instruction}`,
33
35
  " No localhost callback or exposed port is required.",
@@ -23,6 +23,7 @@ import { redactErrorMessage } from "../../config/redact.js";
23
23
  * @param {string} params.profileId - which named profile this login is for
24
24
  * @param {string[]} params.scopes - scopes being requested
25
25
  * @param {(url: string) => Promise<void>} [params.openBrowser] - optional browser opener
26
+ * @param {(params: { accessToken: string, requestedProfileId: string }) => Promise<string>} [params.resolveProfileId]
26
27
  * @param {(ms: number) => Promise<void>} [params.sleep] - injectable for tests
27
28
  * @param {(...args: any[]) => void} [params.onStatus] - callback for UI updates (verification URL, polling status, etc.)
28
29
  * @returns {Promise<{ ok: true, profileId: string } | { ok: false, reason: string }>}
@@ -33,6 +34,7 @@ export async function authLogin({
33
34
  profileId,
34
35
  scopes,
35
36
  openBrowser,
37
+ resolveProfileId,
36
38
  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
37
39
  onStatus = () => {},
38
40
  }) {
@@ -69,14 +71,26 @@ export async function authLogin({
69
71
  }
70
72
 
71
73
  if (result.status === "approved") {
72
- await credentialStore.set(profileId, {
74
+ let resolvedProfileId = profileId;
75
+ if (resolveProfileId) {
76
+ try {
77
+ resolvedProfileId = await resolveProfileId({
78
+ accessToken: result.token.accessToken,
79
+ requestedProfileId: profileId,
80
+ });
81
+ } catch (err) {
82
+ return { ok: false, reason: redactErrorMessage(err.message) };
83
+ }
84
+ }
85
+
86
+ await credentialStore.set(resolvedProfileId, {
73
87
  accessToken: result.token.accessToken,
74
88
  refreshToken: result.token.refreshToken,
75
89
  expiresAt: Date.now() + result.token.expiresInSeconds * 1000,
76
90
  scopes: result.token.scopes,
77
91
  });
78
92
  onStatus({ type: "approved" });
79
- return { ok: true, profileId };
93
+ return { ok: true, profileId: resolvedProfileId };
80
94
  }
81
95
 
82
96
  if (result.status === "denied") {
@@ -0,0 +1,29 @@
1
+ import { validateProfileId } from "../../config/ProfileRegistry.js";
2
+
3
+ /** Resolve the authenticated Accounts username without persisting a temporary profile. */
4
+ export async function profileAliasFromIdentity({
5
+ accessToken,
6
+ apiBaseUrl,
7
+ fetchImpl = globalThis.fetch,
8
+ }) {
9
+ const endpoint = new URL("/api/v1/me", apiBaseUrl);
10
+ const response = await fetchImpl(endpoint, {
11
+ headers: {
12
+ accept: "application/json",
13
+ authorization: `Bearer ${accessToken}`,
14
+ },
15
+ });
16
+
17
+ let payload;
18
+ try {
19
+ payload = await response.json();
20
+ } catch {
21
+ throw new Error("LixBlogs could not resolve the signed-in username.");
22
+ }
23
+
24
+ if (!response.ok || typeof payload?.data?.username !== "string") {
25
+ throw new Error(payload?.error?.message || "LixBlogs could not resolve the signed-in username.");
26
+ }
27
+
28
+ return validateProfileId(payload.data.username);
29
+ }