@finebi/finebi-cli 1.3.0 → 1.4.0

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.
@@ -1,94 +1,94 @@
1
- import {createHash, randomUUID} from "node:crypto";
2
- import {existsSync} from "node:fs";
3
- import {cp, mkdir, lstat, mkdtemp, readFile, readdir, rename, rm} from "node:fs/promises";
4
- import {homedir} from "node:os";
5
- import {join, dirname, resolve} from "node:path";
6
-
7
- export function agentCatalog({home = homedir(), env = process.env} = {}) {
8
- const config = env.XDG_CONFIG_HOME || join(home, ".config");
9
- return [
10
- ["claude-code", "Claude Code", env.CLAUDE_CONFIG_DIR || join(home, ".claude")],
11
- ["codex", "Codex", env.CODEX_HOME || join(home, ".codex")],
12
- ["cursor", "Cursor", join(home, ".cursor")],
13
- ["gemini-cli", "Gemini CLI", join(home, ".gemini")],
14
- ["github-copilot", "GitHub Copilot", join(home, ".copilot")],
15
- ["opencode", "OpenCode", join(config, "opencode")],
16
- ["cline", "Cline", join(home, ".agents")],
17
- ["windsurf", "Windsurf", join(home, ".codeium", "windsurf")],
18
- ["trae", "Trae", join(home, ".trae")],
19
- ["trae-cn", "Trae CN", join(home, ".trae-cn")],
20
- ["openclaw", "OpenClaw", [".openclaw", ".clawdbot", ".moltbot"].map(p => join(home, p)).find(existsSync) || join(home, ".openclaw")],
21
- ["roo", "Roo Code", join(home, ".roo")],
22
- ["qwen-code", "Qwen Code", join(home, ".qwen")],
23
- ].map(([id, name, root]) => ({id, name, directory: join(root, "skills"), detected: existsSync(root)}));
24
- }
25
-
26
- export async function hashTree(root, prefix = "") {
27
- const files = {};
28
- for (const entry of (await readdir(join(root, prefix), {withFileTypes: true})).sort((a, b) => a.name.localeCompare(b.name))) {
29
- const name = prefix ? `${prefix}/${entry.name}` : entry.name;
30
- if (entry.isSymbolicLink()) throw new Error(`Skill contains a symbolic link: ${name}`);
31
- if (entry.isDirectory()) Object.assign(files, await hashTree(root, name));
32
- else files[name] = createHash("sha256").update(await readFile(join(root, name))).digest("hex");
33
- }
34
- return files;
35
- }
36
-
37
- function sameHashes(left, right) {
38
- return left && right && Object.keys(left).length === Object.keys(right).length
39
- && Object.keys(left).every(key => left[key] === right[key]);
40
- }
41
-
42
- export async function installSkills({source, home, env, agents, replace = false, previous = []}) {
43
- const catalog = agentCatalog({home, env});
44
- const selected = agents.map(id => {
45
- const agent = catalog.find(item => item.id === id);
46
- if (!agent) throw new Error(`Unknown Agent: ${id}`);
47
- return agent;
48
- });
49
- const sourceHashes = await hashTree(source);
50
- const results = [];
51
- for (const agent of selected) {
52
- const target = join(agent.directory, "finebi-cli-agent");
53
- let backup;
54
- let candidate;
55
- try {
56
- const stat = await lstat(target).catch(error => { if (error.code !== "ENOENT") throw error; });
57
- if (stat) {
58
- if (stat.isSymbolicLink()) throw new Error("Skill target is a symbolic link; refusing to replace it.");
59
- const actual = await hashTree(target);
60
- if (sameHashes(sourceHashes, actual)) {
61
- results.push({agent: agent.id, path: target, status: "unchanged", hashes: sourceHashes});
62
- continue;
63
- }
64
- const recorded = previous.find(item => item.agent === agent.id && item.path === target)?.hashes;
65
- if (!replace && !sameHashes(recorded, actual)) {
66
- results.push({agent: agent.id, path: target, status: "kept"});
67
- continue;
68
- }
69
- backup = join(home, ".finebi-cli", "skill-backups", `${agent.id}-${randomUUID()}`);
70
- await mkdir(dirname(backup), {recursive: true});
71
- await cp(target, backup, {recursive: true, errorOnExist: true, force: false});
72
- if (!sameHashes(actual, await hashTree(backup))) throw new Error("Skill backup integrity check failed.");
73
- }
74
- await mkdir(agent.directory, {recursive: true});
75
- candidate = await mkdtemp(join(agent.directory, ".finebi-cli-stage-"));
76
- await cp(source, candidate, {recursive: true});
77
- if (!sameHashes(sourceHashes, await hashTree(candidate))) throw new Error("Skill copy integrity check failed.");
78
- const old = `${candidate}-old`;
79
- if (stat) await rename(target, old);
80
- try { await rename(candidate, target); }
81
- catch (error) { if (stat) await rename(old, target); throw error; }
82
- candidate = undefined;
83
- // Delete only the relocated, backed-up directory inside this Agent's skills directory.
84
- if (stat && backup && dirname(resolve(old)) === resolve(agent.directory)) await rm(old, {recursive: true});
85
- if (!sameHashes(sourceHashes, await hashTree(target))) throw new Error("Installed Skill integrity check failed.");
86
- results.push({agent: agent.id, path: target, status: "installed", hashes: sourceHashes, ...(backup ? {backup} : {})});
87
- } catch (error) {
88
- results.push({agent: agent.id, path: target, status: "failed", error: error.message, ...(backup ? {backup} : {})});
89
- } finally {
90
- if (candidate && dirname(resolve(candidate)) === resolve(agent.directory)) await rm(candidate, {recursive: true, force: true});
91
- }
92
- }
93
- return results;
94
- }
1
+ import {createHash, randomUUID} from "node:crypto";
2
+ import {existsSync} from "node:fs";
3
+ import {cp, mkdir, lstat, mkdtemp, readFile, readdir, rename, rm} from "node:fs/promises";
4
+ import {homedir} from "node:os";
5
+ import {join, dirname, resolve} from "node:path";
6
+
7
+ export function agentCatalog({home = homedir(), env = process.env} = {}) {
8
+ const config = env.XDG_CONFIG_HOME || join(home, ".config");
9
+ return [
10
+ ["claude-code", "Claude Code", env.CLAUDE_CONFIG_DIR || join(home, ".claude")],
11
+ ["codex", "Codex", env.CODEX_HOME || join(home, ".codex")],
12
+ ["cursor", "Cursor", join(home, ".cursor")],
13
+ ["gemini-cli", "Gemini CLI", join(home, ".gemini")],
14
+ ["github-copilot", "GitHub Copilot", join(home, ".copilot")],
15
+ ["opencode", "OpenCode", join(config, "opencode")],
16
+ ["cline", "Cline", join(home, ".agents")],
17
+ ["windsurf", "Windsurf", join(home, ".codeium", "windsurf")],
18
+ ["trae", "Trae", join(home, ".trae")],
19
+ ["trae-cn", "Trae CN", join(home, ".trae-cn")],
20
+ ["openclaw", "OpenClaw", [".openclaw", ".clawdbot", ".moltbot"].map(p => join(home, p)).find(existsSync) || join(home, ".openclaw")],
21
+ ["roo", "Roo Code", join(home, ".roo")],
22
+ ["qwen-code", "Qwen Code", join(home, ".qwen")],
23
+ ].map(([id, name, root]) => ({id, name, directory: join(root, "skills"), detected: existsSync(root)}));
24
+ }
25
+
26
+ export async function hashTree(root, prefix = "") {
27
+ const files = {};
28
+ for (const entry of (await readdir(join(root, prefix), {withFileTypes: true})).sort((a, b) => a.name.localeCompare(b.name))) {
29
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name;
30
+ if (entry.isSymbolicLink()) throw new Error(`Skill contains a symbolic link: ${name}`);
31
+ if (entry.isDirectory()) Object.assign(files, await hashTree(root, name));
32
+ else files[name] = createHash("sha256").update(await readFile(join(root, name))).digest("hex");
33
+ }
34
+ return files;
35
+ }
36
+
37
+ function sameHashes(left, right) {
38
+ return left && right && Object.keys(left).length === Object.keys(right).length
39
+ && Object.keys(left).every(key => left[key] === right[key]);
40
+ }
41
+
42
+ export async function installSkills({source, home, env, agents, replace = false, previous = []}) {
43
+ const catalog = agentCatalog({home, env});
44
+ const selected = agents.map(id => {
45
+ const agent = catalog.find(item => item.id === id);
46
+ if (!agent) throw new Error(`Unknown Agent: ${id}`);
47
+ return agent;
48
+ });
49
+ const sourceHashes = await hashTree(source);
50
+ const results = [];
51
+ for (const agent of selected) {
52
+ const target = join(agent.directory, "finebi-cli-agent");
53
+ let backup;
54
+ let candidate;
55
+ try {
56
+ const stat = await lstat(target).catch(error => { if (error.code !== "ENOENT") throw error; });
57
+ if (stat) {
58
+ if (stat.isSymbolicLink()) throw new Error("Skill target is a symbolic link; refusing to replace it.");
59
+ const actual = await hashTree(target);
60
+ if (sameHashes(sourceHashes, actual)) {
61
+ results.push({agent: agent.id, path: target, status: "unchanged", hashes: sourceHashes});
62
+ continue;
63
+ }
64
+ const recorded = previous.find(item => item.agent === agent.id && item.path === target)?.hashes;
65
+ if (!replace && !sameHashes(recorded, actual)) {
66
+ results.push({agent: agent.id, path: target, status: "kept"});
67
+ continue;
68
+ }
69
+ backup = join(home, ".finebi-cli", "skill-backups", `${agent.id}-${randomUUID()}`);
70
+ await mkdir(dirname(backup), {recursive: true});
71
+ await cp(target, backup, {recursive: true, errorOnExist: true, force: false});
72
+ if (!sameHashes(actual, await hashTree(backup))) throw new Error("Skill backup integrity check failed.");
73
+ }
74
+ await mkdir(agent.directory, {recursive: true});
75
+ candidate = await mkdtemp(join(agent.directory, ".finebi-cli-stage-"));
76
+ await cp(source, candidate, {recursive: true});
77
+ if (!sameHashes(sourceHashes, await hashTree(candidate))) throw new Error("Skill copy integrity check failed.");
78
+ const old = `${candidate}-old`;
79
+ if (stat) await rename(target, old);
80
+ try { await rename(candidate, target); }
81
+ catch (error) { if (stat) await rename(old, target); throw error; }
82
+ candidate = undefined;
83
+ // Delete only the relocated, backed-up directory inside this Agent's skills directory.
84
+ if (stat && backup && dirname(resolve(old)) === resolve(agent.directory)) await rm(old, {recursive: true});
85
+ if (!sameHashes(sourceHashes, await hashTree(target))) throw new Error("Installed Skill integrity check failed.");
86
+ results.push({agent: agent.id, path: target, status: "installed", hashes: sourceHashes, ...(backup ? {backup} : {})});
87
+ } catch (error) {
88
+ results.push({agent: agent.id, path: target, status: "failed", error: error.message, ...(backup ? {backup} : {})});
89
+ } finally {
90
+ if (candidate && dirname(resolve(candidate)) === resolve(agent.directory)) await rm(candidate, {recursive: true, force: true});
91
+ }
92
+ }
93
+ return results;
94
+ }
@@ -10,10 +10,10 @@ const scriptDirectory = dirname(fileURLToPath(import.meta.url));
10
10
  const bundledCliEntry = resolve(scriptDirectory, "finebi-cli", "src", "index.js");
11
11
 
12
12
  export async function run(argv = process.argv.slice(2), options = {}) {
13
- const env = {...(options.env ?? process.env)};
14
- if (!env.FINEBI_CLI_PATH?.trim() && existsSync(bundledCliEntry)) {
15
- env.FINEBI_CLI_PATH = bundledCliEntry;
16
- }
13
+ const env = {...(options.env ?? process.env)};
14
+ if (!env.FINEBI_CLI_PATH?.trim() && existsSync(bundledCliEntry)) {
15
+ env.FINEBI_CLI_PATH = bundledCliEntry;
16
+ }
17
17
  return runCore(argv, {...options, env});
18
18
  }
19
19
 
package/src/config.js CHANGED
@@ -56,23 +56,23 @@ export function writeConfigAtomically(path, config, options = {}) {
56
56
  }
57
57
  }
58
58
 
59
- export function resolveConfig(flags = {}, env = process.env, fileConfig = readConfig()) {
60
- const baseUrl = firstNonEmpty(flags.baseUrl, env.FINEBI_BASE_URL, fileConfig.baseUrl);
61
- const sameServer = sameBaseUrl(baseUrl, fileConfig.baseUrl);
62
- const explicitToken = firstNonEmpty(flags.token, env.FINEBI_AUTH_TOKEN);
63
- const savedAuth = sameServer ? fileConfig.auth : undefined;
64
- const token = firstNonEmpty(explicitToken, savedAuth?.token, sameServer ? fileConfig.token : undefined);
59
+ export function resolveConfig(flags = {}, env = process.env, fileConfig = readConfig()) {
60
+ const baseUrl = firstNonEmpty(flags.baseUrl, env.FINEBI_BASE_URL, fileConfig.baseUrl);
61
+ const sameServer = sameBaseUrl(baseUrl, fileConfig.baseUrl);
62
+ const explicitToken = firstNonEmpty(flags.token, env.FINEBI_AUTH_TOKEN);
63
+ const savedAuth = sameServer ? fileConfig.auth : undefined;
64
+ const token = firstNonEmpty(explicitToken, savedAuth?.token, sameServer ? fileConfig.token : undefined);
65
65
  return normalizeConfig({
66
66
  ...fileConfig,
67
- baseUrl,
67
+ baseUrl,
68
68
  timeoutMs: parsePositiveInt(firstNonEmpty(flags.timeoutMs, env.FINEBI_TIMEOUT_MS), fileConfig.timeoutMs),
69
69
  capabilityTtlMs: parsePositiveInt(
70
70
  firstNonEmpty(flags.capabilityTtlMs, env.FINEBI_CAPABILITY_TTL_MS),
71
71
  fileConfig.capabilityTtlMs,
72
72
  ),
73
73
  auth: {
74
- ...(explicitToken ? {} : savedAuth),
75
- token,
74
+ ...(explicitToken ? {} : savedAuth),
75
+ token,
76
76
  },
77
77
  });
78
78
  }
@@ -81,10 +81,10 @@ export function updateConfigValue(config, key, value, now = () => new Date().toI
81
81
  const next = normalizeConfig(config);
82
82
  switch (key) {
83
83
  case "base-url":
84
- if (next.baseUrl !== value) {
85
- next.capabilityCacheInvalidatedAt = now();
86
- delete next.auth;
87
- delete next.token;
84
+ if (next.baseUrl !== value) {
85
+ next.capabilityCacheInvalidatedAt = now();
86
+ delete next.auth;
87
+ delete next.token;
88
88
  }
89
89
  next.baseUrl = value;
90
90
  return next;
@@ -128,7 +128,7 @@ function defaultConfig() {
128
128
  };
129
129
  }
130
130
 
131
- export function normalizeConfig(config) {
131
+ export function normalizeConfig(config) {
132
132
  return {
133
133
  ...config,
134
134
  timeoutMs: parsePositiveInt(config.timeoutMs, DEFAULT_TIMEOUT_MS),
@@ -136,14 +136,14 @@ export function normalizeConfig(config) {
136
136
  };
137
137
  }
138
138
 
139
- function sanitizeConfig(config) {
140
- if (config.schemaVersion === 2) {
141
- return {
142
- schemaVersion: 2,
143
- currentProfile: config.currentProfile,
144
- profiles: Object.fromEntries(Object.entries(config.profiles).map(([name, profile]) => [name, sanitizeConfig(normalizeConfig(profile))])),
145
- };
146
- }
139
+ function sanitizeConfig(config) {
140
+ if (config.schemaVersion === 2) {
141
+ return {
142
+ schemaVersion: 2,
143
+ currentProfile: config.currentProfile,
144
+ profiles: Object.fromEntries(Object.entries(config.profiles).map(([name, profile]) => [name, sanitizeConfig(normalizeConfig(profile))])),
145
+ };
146
+ }
147
147
  const { token: _legacyToken, ...rest } = config;
148
148
  const auth = config.auth
149
149
  ? {
@@ -166,11 +166,11 @@ function sanitizeConfig(config) {
166
166
  ...rest,
167
167
  auth,
168
168
  };
169
- }
170
-
171
- export function sameBaseUrl(left, right) {
172
- return (left || "").replace(/\/+$/, "") === (right || "").replace(/\/+$/, "");
173
- }
169
+ }
170
+
171
+ export function sameBaseUrl(left, right) {
172
+ return (left || "").replace(/\/+$/, "") === (right || "").replace(/\/+$/, "");
173
+ }
174
174
 
175
175
  function firstNonEmpty(...values) {
176
176
  return values.find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
package/src/help.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {brandCommandValue, CLI_COMMAND_NAME, CLI_DISPLAY_NAME, commandLine} from "./branding.js";
2
2
  import {cliError} from "./errors.js";
3
- import {failure, success} from "./output.js";
4
-
5
- const GLOBAL_OPTIONS = [
6
- {name: "--profile", type: "string", description: "Environment name. Overrides FINEBI_PROFILE and the saved default; ordinary commands do not create environments or switch the default."},
7
- {name: "--base-url", type: "string", description: "Connection URL override. A different server never inherits saved credentials or capabilities."},
8
- {name: "--token", type: "string", description: "Temporary session token override; not persisted."},
9
- ];
3
+ import {failure, success} from "./output.js";
4
+
5
+ const GLOBAL_OPTIONS = [
6
+ {name: "--profile", type: "string", description: "Environment name. Overrides FINEBI_PROFILE and the saved default; ordinary commands do not create environments or switch the default."},
7
+ {name: "--base-url", type: "string", description: "Connection URL override. A different server never inherits saved credentials or capabilities."},
8
+ {name: "--token", type: "string", description: "Temporary session token override; not persisted."},
9
+ ];
10
10
 
11
11
  export function renderTopLevelHelp(manifest) {
12
12
  return success("help", {
@@ -14,8 +14,8 @@ export function renderTopLevelHelp(manifest) {
14
14
  name: resource.name,
15
15
  summary: resource.summary,
16
16
  })),
17
- dynamicCapabilitiesLoaded: Boolean(manifest),
18
- globalOptions: GLOBAL_OPTIONS,
17
+ dynamicCapabilitiesLoaded: Boolean(manifest),
18
+ globalOptions: GLOBAL_OPTIONS,
19
19
  }, { schemaVersion: manifest?.schemaVersion });
20
20
  }
21
21
 
@@ -24,7 +24,7 @@ export function renderBuiltInHelp(resourceName) {
24
24
  if (!resource) {
25
25
  return capabilityNotFound(resourceName);
26
26
  }
27
- return success("help", brandCommandValue({...resource, globalOptions: GLOBAL_OPTIONS}));
27
+ return success("help", brandCommandValue({...resource, globalOptions: GLOBAL_OPTIONS}));
28
28
  }
29
29
 
30
30
  export function renderBuiltInCommandHelp(resourceName, commandName) {
@@ -38,8 +38,8 @@ export function renderBuiltInCommandHelp(resourceName, commandName) {
38
38
  command: command.name,
39
39
  summary: command.summary,
40
40
  usage: command.usage,
41
- options: command.options || [],
42
- globalOptions: GLOBAL_OPTIONS,
41
+ options: command.options || [],
42
+ globalOptions: GLOBAL_OPTIONS,
43
43
  details: command.details || [],
44
44
  examples: command.examples || [],
45
45
  }));
@@ -52,8 +52,8 @@ export function renderResourceHelp(manifest, resourceName) {
52
52
  }
53
53
  return success("help", {
54
54
  resource: resource.name,
55
- summary: resource.summary,
56
- globalOptions: GLOBAL_OPTIONS,
55
+ summary: resource.summary,
56
+ globalOptions: GLOBAL_OPTIONS,
57
57
  commands: (resource.commands || []).map((command) => ({
58
58
  name: command.name,
59
59
  summary: command.summary,
@@ -72,8 +72,8 @@ export function renderCommandHelp(manifest, resourceName, commandName) {
72
72
  command: command.name,
73
73
  summary: command.summary,
74
74
  usage: usageFor(resource.name, command),
75
- options: commandOptionsFor(command),
76
- globalOptions: GLOBAL_OPTIONS,
75
+ options: commandOptionsFor(command),
76
+ globalOptions: GLOBAL_OPTIONS,
77
77
  display: command.display,
78
78
  requestSchema: command.requestSchema,
79
79
  responseSchema: command.responseSchema,
@@ -152,7 +152,7 @@ const BUILT_IN_RESOURCES = {
152
152
  },
153
153
  {
154
154
  name: "clear",
155
- summary: "Remove the selected environment's local configuration, credentials and capability cache; does not revoke server keys.",
155
+ summary: "Remove the selected environment's local configuration, credentials and capability cache; does not revoke server keys.",
156
156
  usage: "finebi-cli config clear",
157
157
  },
158
158
  ],
@@ -164,18 +164,18 @@ const BUILT_IN_RESOURCES = {
164
164
  {
165
165
  name: "login",
166
166
  summary: "Login with FineBI credentials or browser authorization and persist an Access Key.",
167
- usage: "finebi-cli auth login [--profile <name>] [--base-url <url>] [--browser] [--username <username>] [--from-env] [--password-stdin] [--no-use]",
168
- options: [
169
- { name: "--no-use", type: "boolean", description: "Save successful login without changing the default environment." },
167
+ usage: "finebi-cli auth login [--profile <name>] [--base-url <url>] [--browser] [--username <username>] [--from-env] [--password-stdin] [--no-use]",
168
+ options: [
169
+ { name: "--no-use", type: "boolean", description: "Save successful login without changing the default environment." },
170
170
  { name: "--username", type: "string", description: "FineBI username." },
171
171
  { name: "--from-env", type: "boolean", description: "Read base URL, username, and password from environment variables." },
172
172
  { name: "--password-stdin", type: "boolean", description: "Read password from standard input." },
173
173
  { name: "--browser", type: "boolean", description: "Authorize this CLI from a signed-in FineBI browser without sending a password." },
174
174
  ],
175
- details: [
176
- "Login creates the named environment if missing and selects it after successful authentication, unless --no-use is supplied.",
177
- "A new environment requires --base-url or FINEBI_BASE_URL; it never inherits another environment's address or identity.",
178
- "Failed authentication preserves saved environments and the default selection.",
175
+ details: [
176
+ "Login creates the named environment if missing and selects it after successful authentication, unless --no-use is supplied.",
177
+ "A new environment requires --base-url or FINEBI_BASE_URL; it never inherits another environment's address or identity.",
178
+ "Failed authentication preserves saved environments and the default selection.",
179
179
  "Environment variables for --from-env:",
180
180
  " FINEBI_BASE_URL",
181
181
  " FINEBI_USERNAME",
@@ -187,28 +187,28 @@ const BUILT_IN_RESOURCES = {
187
187
  "finebi-cli auth login --username <username>",
188
188
  "finebi-cli auth login --from-env",
189
189
  "echo <password> | finebi-cli auth login --username <username> --password-stdin",
190
- "finebi-cli auth login --browser",
191
- "finebi-cli auth login --profile test --base-url https://test.example.com/webroot/decision --browser",
190
+ "finebi-cli auth login --browser",
191
+ "finebi-cli auth login --profile test --base-url https://test.example.com/webroot/decision --browser",
192
192
  ],
193
193
  },
194
- {
195
- name: "list",
196
- summary: "List saved environments and mark the default; local metadata only, no credential validation or secret output.",
197
- usage: "finebi-cli auth list",
198
- },
199
- {
200
- name: "use",
201
- summary: "Select an existing environment as default without logging in.",
202
- usage: "finebi-cli auth use <name>",
203
- },
204
- {
205
- name: "remove",
206
- summary: "Delete one local environment and its capability cache; does not revoke server keys.",
207
- usage: "finebi-cli auth remove <name>",
208
- details: ["Use auth logout --profile <name> first to revoke its Access Key.", "Removing the default environment does not select another environment."],
209
- },
210
- {
211
- name: "info",
194
+ {
195
+ name: "list",
196
+ summary: "List saved environments and mark the default; local metadata only, no credential validation or secret output.",
197
+ usage: "finebi-cli auth list",
198
+ },
199
+ {
200
+ name: "use",
201
+ summary: "Select an existing environment as default without logging in.",
202
+ usage: "finebi-cli auth use <name>",
203
+ },
204
+ {
205
+ name: "remove",
206
+ summary: "Delete one local environment and its capability cache; does not revoke server keys.",
207
+ usage: "finebi-cli auth remove <name>",
208
+ details: ["Use auth logout --profile <name> first to revoke its Access Key.", "Removing the default environment does not select another environment."],
209
+ },
210
+ {
211
+ name: "info",
212
212
  summary: "Show stored authentication metadata with secrets masked.",
213
213
  usage: "finebi-cli auth info",
214
214
  },
@@ -368,10 +368,10 @@ function renderTopLevelHelpText(data) {
368
368
  "",
369
369
  "Use --help or -h for command details.",
370
370
  "Use --json with --help for structured output.",
371
- "Output options: --select <paths> / --max-chars <n> / --save <path> for large responses.",
372
- "Environment: --profile <name> > FINEBI_PROFILE > saved default. Global options may precede or follow the command.",
373
- );
374
- lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
371
+ "Output options: --select <paths> / --max-chars <n> / --save <path> for large responses.",
372
+ "Environment: --profile <name> > FINEBI_PROFILE > saved default. Global options may precede or follow the command.",
373
+ );
374
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
375
375
  return `${lines.join("\n")}\n`;
376
376
  }
377
377
 
@@ -387,8 +387,8 @@ function renderResourceHelpText(data) {
387
387
  "",
388
388
  "Commands:",
389
389
  ];
390
- lines.push(...formatRows(data.commands || [], "name", "summary"));
391
- lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
390
+ lines.push(...formatRows(data.commands || [], "name", "summary"));
391
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions || []));
392
392
  lines.push("", `Use ${commandLine(data.resource, "<command>", "--help")} for command details.`);
393
393
  return `${lines.join("\n")}\n`;
394
394
  }
@@ -403,13 +403,13 @@ function renderCommandHelpText(data) {
403
403
  "Usage:",
404
404
  ` ${data.usage}`,
405
405
  ];
406
- if ((data.options || []).length > 0) {
406
+ if ((data.options || []).length > 0) {
407
407
  lines.push("", "Options:");
408
408
  lines.push(...formatOptionRows(data.options));
409
- }
410
- if ((data.globalOptions || []).length > 0) {
411
- lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions));
412
- }
409
+ }
410
+ if ((data.globalOptions || []).length > 0) {
411
+ lines.push("", "Global Options:", ...formatOptionRows(data.globalOptions));
412
+ }
413
413
  if ((data.details || []).length > 0) {
414
414
  lines.push("", ...data.details);
415
415
  }
package/src/index.js CHANGED
@@ -51,8 +51,11 @@ export async function runCli(argv, context = {}) {
51
51
  const management = args[0] === "auth" && ["list", "use", "remove"].includes(args[1]);
52
52
  const builtInHelp = isHelpFlagInvocation(args) && (args.length === 1 || isBuiltInResource(args[0]));
53
53
  const optional = management || builtInHelp || args.length === 0 || args[0] === "version";
54
- const profileName = management ? null : selectProfile(configPath(context), firstNonEmpty(flags.profile, env.FINEBI_PROFILE), {
55
- allowCreate: args[0] === "auth" && args[1] === "login", optional,
54
+ const requestedProfile = firstNonEmpty(flags.profile, env.FINEBI_PROFILE);
55
+ const allowCreate = args[0] === "auth" && args[1] === "login";
56
+ const initialDefault = !requestedProfile && !existsSync(configPath(context));
57
+ const profileName = management ? null : selectProfile(configPath(context), requestedProfile, {
58
+ allowCreate, optional,
56
59
  });
57
60
  if (management && (flags.profile || env.FINEBI_PROFILE) && args[1] !== "list") {
58
61
  if (flags.profile && flags.profile !== args[2]) throw new ProfileError("Use the positional environment name for auth use/remove; --profile must not select a different environment.");
@@ -64,7 +67,12 @@ export async function runCli(argv, context = {}) {
64
67
  );
65
68
  const envelope = mutatesProfile
66
69
  ? await withProfileLoginLock(configPath(context), args[0] === "auth" && args[1] === "remove" ? args[2] : profileName,
67
- () => dispatchCli(args, context))
70
+ () => {
71
+ if (!management) selectProfile(configPath(context), profileName, {
72
+ allowCreate: allowCreate || (initialDefault && profileName === "default"),
73
+ });
74
+ return dispatchCli(args, context);
75
+ })
68
76
  : await dispatchCli(args, context);
69
77
  if (profileName) {
70
78
  envelope.meta = {...envelope.meta, profile: profileName};