@bussolabs/closeyourit-cli 0.17.0 → 0.18.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.
package/README.md CHANGED
@@ -79,6 +79,12 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
79
79
  | `errors list --project <id\|key> [--status] [--page]` | List error groups. |
80
80
  | `errors show <id> --project <id\|key>` | Show one error group. |
81
81
  | `errors resolve\|reopen\|mute\|unmute <id> --project <id\|key>` | Triage an error group. |
82
+ | `vulnerabilities list [--project <id\|key>] [--severity] [--status] [--package] [--page]` | List known dependency vulnerabilities across every visible project, worst first. |
83
+ | `vulnerabilities show <id>` | Show one: advisory, package, manifest, fixing version. |
84
+ | `vulnerabilities ignore <id> [--note]` / `reopen <id>` | Accept living with the risk, or change your mind (requires vulnerabilities.triage). |
85
+ | `vulnerabilities promote <id>` | Open a ticket from it — automatic promotion only fires on high/critical. |
86
+ | `vulnerabilities rescan --project <id\|key>` | Rescan now, without waiting for the nightly run. |
87
+ | `vulnerabilities runtimes [--project <id\|key>] [--page]` | Support state of the declared runtimes (Ruby, Node, Flutter…). |
82
88
  | `tokens list --project <id\|key>` | List ingest tokens. |
83
89
  | `tokens create --project <id\|key> --name <name> --environment-id <id>` | Create a token (secret shown once). |
84
90
  | `tokens provision --project <id\|key> --name <name> --environment-id <code\|id> --to-project <id\|key> --to-environment-id <code\|id> --secret-name <NAME>` | Generate and deliver a token without revealing its value. GitHub sync is enabled by default; use `--no-sync-github` for vault-only delivery. |
@@ -14,12 +14,17 @@ class Login extends base_1.BaseCommand {
14
14
  };
15
15
  async run() {
16
16
  const { flags } = await this.parse(Login);
17
- if (flags['api-url']) {
18
- this.cfg.apiUrl = flags['api-url'];
19
- (0, config_1.saveConfig)(this.cfg);
20
- }
17
+ // Persist from the on-disk config so a temporary CLOSEYOURIT_API_URL override is never written to
18
+ // config.json. Only an explicit --api-url is persisted (that is its documented purpose).
19
+ const stored = (0, config_1.loadStoredConfig)();
20
+ if (flags['api-url'])
21
+ stored.apiUrl = flags['api-url'];
22
+ // Runtime URL to authenticate against: --api-url wins, otherwise the resolved config (env override included).
23
+ const apiUrl = flags['api-url'] ?? this.cfg.apiUrl;
24
+ if (flags['api-url'])
25
+ (0, config_1.saveConfig)(stored);
21
26
  const clientName = `closeyourit-cli/${this.config.version} (${process.platform} ${process.arch})`;
22
- const authorization = await (0, device_flow_1.startDeviceFlow)(this.cfg.apiUrl, clientName);
27
+ const authorization = await (0, device_flow_1.startDeviceFlow)(apiUrl, clientName);
23
28
  if (!this.jsonEnabled()) {
24
29
  this.log('');
25
30
  this.log('To authorize this CLI, open:');
@@ -32,7 +37,7 @@ class Login extends base_1.BaseCommand {
32
37
  this.log('Opening your browser…');
33
38
  }
34
39
  (0, browser_1.openUrl)(authorization.verification_uri_complete);
35
- const result = await (0, device_flow_1.pollDeviceToken)(this.cfg.apiUrl, authorization.device_code, {
40
+ const result = await (0, device_flow_1.pollDeviceToken)(apiUrl, authorization.device_code, {
36
41
  interval: authorization.interval,
37
42
  expiresIn: authorization.expires_in,
38
43
  onPending: () => {
@@ -40,24 +45,24 @@ class Login extends base_1.BaseCommand {
40
45
  process.stderr.write('.');
41
46
  },
42
47
  });
43
- this.cfg.token = result.access_token;
44
- (0, config_1.saveConfig)(this.cfg);
45
- // Enrich the local config with identity (account + organization).
46
- this.api = new api_1.CliApi(this.cfg);
48
+ stored.token = result.access_token;
49
+ (0, config_1.saveConfig)(stored);
50
+ // Enrich the local config with identity (account + organization). Use the runtime apiUrl for the call.
51
+ this.api = new api_1.CliApi({ ...stored, apiUrl });
47
52
  const who = await this.api.get('/cli/v1/whoami');
48
53
  const account = who.data.account;
49
54
  const organization = who.data.organization;
50
55
  if (account)
51
- this.cfg.account = { id: account.id, name: account.name, email: account.email };
56
+ stored.account = { id: account.id, name: account.name, email: account.email };
52
57
  if (organization) {
53
- this.cfg.organization = { id: organization.id, name: organization.name, slug: organization.slug };
58
+ stored.organization = { id: organization.id, name: organization.name, slug: organization.slug };
54
59
  }
55
- (0, config_1.saveConfig)(this.cfg);
60
+ (0, config_1.saveConfig)(stored);
56
61
  if (!this.jsonEnabled()) {
57
62
  this.log('');
58
- this.log(`Logged in as ${this.cfg.account?.email ?? 'unknown'} · ${this.cfg.organization?.name ?? 'no organization'}`);
63
+ this.log(`Logged in as ${stored.account?.email ?? 'unknown'} · ${stored.organization?.name ?? 'no organization'}`);
59
64
  }
60
- return { account: this.cfg.account, organization: this.cfg.organization, token: result.token };
65
+ return { account: stored.account, organization: stored.organization, token: result.token };
61
66
  }
62
67
  }
63
68
  exports.default = Login;
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class VulnerabilitiesIgnore extends BaseCommand {
3
+ static aliases: string[];
4
+ static description: string;
5
+ static examples: string[];
6
+ static args: {
7
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ note: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<unknown>;
13
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../base");
5
+ class VulnerabilitiesIgnore extends base_1.BaseCommand {
6
+ static aliases = ['vulns:ignore'];
7
+ static description = 'Ignore a vulnerability: you accept living with the risk';
8
+ static examples = [
9
+ '<%= config.bin %> vulnerabilities ignore <finding-id>',
10
+ '<%= config.bin %> vulnerabilities ignore <finding-id> --note "Not reachable from our code."',
11
+ ];
12
+ static args = {
13
+ id: core_1.Args.string({ description: 'Vulnerability finding id', required: true }),
14
+ };
15
+ static flags = {
16
+ note: core_1.Flags.string({ description: 'Why you are keeping it (shown alongside the ignored row)' }),
17
+ };
18
+ async run() {
19
+ const { args, flags } = await this.parse(VulnerabilitiesIgnore);
20
+ const res = await this.api.put(`/cli/v1/vulnerabilities/${args.id}/ignore`, { triage_note: flags.note });
21
+ if (!this.jsonEnabled()) {
22
+ // An ignored row never reopens on its own: the next scan won't undo a decision already taken.
23
+ this.log(`Vulnerability ${args.id} ignored.`);
24
+ }
25
+ return res;
26
+ }
27
+ }
28
+ exports.default = VulnerabilitiesIgnore;
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class VulnerabilitiesList extends BaseCommand {
3
+ static aliases: string[];
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ page: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ severity: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ status: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ package: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
12
+ };
13
+ run(): Promise<unknown>;
14
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../base");
5
+ const output_1 = require("../../lib/output");
6
+ class VulnerabilitiesList extends base_1.BaseCommand {
7
+ static aliases = ['vulns:list'];
8
+ static description = 'List known vulnerabilities of the project dependencies (cross-project)';
9
+ static examples = [
10
+ '<%= config.bin %> vulnerabilities list',
11
+ '<%= config.bin %> vulnerabilities list --severity critical',
12
+ '<%= config.bin %> vulnerabilities list --project acme-api --status open',
13
+ '<%= config.bin %> vulnerabilities list --package nokogiri --json',
14
+ ];
15
+ static flags = {
16
+ // Optional here (the endpoint is cross-project): without it, the list spans every visible project.
17
+ project: core_1.Flags.string({ char: 'p', description: 'Filter by project id (UUID) or key' }),
18
+ severity: core_1.Flags.string({ description: 'Filter by severity (critical, high, moderate, low, unknown)' }),
19
+ status: core_1.Flags.string({ description: 'Filter by status (open, resolved, ignored)' }),
20
+ package: core_1.Flags.string({ description: 'Filter by package name (partial match)' }),
21
+ ...base_1.pageFlag,
22
+ };
23
+ async run() {
24
+ const { flags } = await this.parse(VulnerabilitiesList);
25
+ const query = new URLSearchParams({ page: String(flags.page) });
26
+ if (flags.project)
27
+ query.set('project_id', await this.resolveProjectId(flags.project));
28
+ if (flags.severity)
29
+ query.set('severity', flags.severity);
30
+ if (flags.status)
31
+ query.set('status', flags.status);
32
+ if (flags.package)
33
+ query.set('package', flags.package);
34
+ const res = await this.api.get(`/cli/v1/vulnerabilities?${query.toString()}`);
35
+ const findings = res.data ?? [];
36
+ if (!this.jsonEnabled()) {
37
+ this.log((0, output_1.renderTable)(
38
+ // ID first, like `errors list`: every other subcommand takes it as its argument, so a table
39
+ // without it forces `--json` just to triage what you are already looking at.
40
+ ['ID', 'PROJECT', 'PACKAGE', 'SEVERITY', 'STATUS', 'FIXED IN', 'ADVISORY'], findings.map((finding) => [
41
+ String(finding.id ?? ''),
42
+ String(finding.project?.key ?? ''),
43
+ String(finding.package?.coordinates ?? ''),
44
+ (0, output_1.statusCell)(finding.advisory?.severity),
45
+ (0, output_1.statusCell)(finding.status),
46
+ // No fixed version means OSV declares none: the only move is dropping or replacing the dep.
47
+ String(finding.fixed_version ?? '—'),
48
+ String(finding.advisory?.display_id ?? ''),
49
+ ])));
50
+ }
51
+ return res;
52
+ }
53
+ }
54
+ exports.default = VulnerabilitiesList;
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class VulnerabilitiesPromote extends BaseCommand {
3
+ static aliases: string[];
4
+ static description: string;
5
+ static examples: string[];
6
+ static args: {
7
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ run(): Promise<unknown>;
10
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../base");
5
+ const output_1 = require("../../lib/output");
6
+ class VulnerabilitiesPromote extends base_1.BaseCommand {
7
+ static aliases = ['vulns:promote'];
8
+ static description = 'Open a ticket from a vulnerability (automatic promotion only fires on high/critical)';
9
+ static examples = ['<%= config.bin %> vulnerabilities promote <finding-id>'];
10
+ static args = {
11
+ id: core_1.Args.string({ description: 'Vulnerability finding id', required: true }),
12
+ };
13
+ async run() {
14
+ const { args } = await this.parse(VulnerabilitiesPromote);
15
+ // Idempotent server-side: promoting twice returns R422-VULN-001, never a second ticket.
16
+ const res = await this.api.put(`/cli/v1/vulnerabilities/${args.id}/promotion`);
17
+ if (!this.jsonEnabled()) {
18
+ this.log((0, output_1.renderRecord)(res.data ?? {}));
19
+ }
20
+ return res;
21
+ }
22
+ }
23
+ exports.default = VulnerabilitiesPromote;
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class VulnerabilitiesReopen extends BaseCommand {
3
+ static aliases: string[];
4
+ static description: string;
5
+ static examples: string[];
6
+ static args: {
7
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ run(): Promise<unknown>;
10
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../base");
5
+ class VulnerabilitiesReopen extends base_1.BaseCommand {
6
+ static aliases = ['vulns:reopen'];
7
+ static description = 'Reopen an ignored vulnerability: you changed your mind';
8
+ static examples = ['<%= config.bin %> vulnerabilities reopen <finding-id>'];
9
+ static args = {
10
+ id: core_1.Args.string({ description: 'Vulnerability finding id', required: true }),
11
+ };
12
+ async run() {
13
+ const { args } = await this.parse(VulnerabilitiesReopen);
14
+ // Reopening is deleting the ignore, not a verb of its own — hence DELETE on the same resource.
15
+ const res = await this.api.delete(`/cli/v1/vulnerabilities/${args.id}/ignore`);
16
+ if (!this.jsonEnabled()) {
17
+ this.log(`Vulnerability ${args.id} reopened.`);
18
+ }
19
+ return res;
20
+ }
21
+ }
22
+ exports.default = VulnerabilitiesReopen;
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class VulnerabilitiesRescan extends BaseCommand {
3
+ static aliases: string[];
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ };
9
+ run(): Promise<unknown>;
10
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const base_1 = require("../../base");
4
+ class VulnerabilitiesRescan extends base_1.BaseCommand {
5
+ static aliases = ['vulns:rescan'];
6
+ static description = 'Rescan a project now, without waiting for the nightly run';
7
+ static examples = ['<%= config.bin %> vulnerabilities rescan --project acme-api'];
8
+ // Required here, unlike the other subcommands: a scan always targets one project.
9
+ static flags = { ...base_1.projectFlag };
10
+ async run() {
11
+ const { flags } = await this.parse(VulnerabilitiesRescan);
12
+ const projectId = await this.resolveProjectId(flags.project);
13
+ const res = await this.api.post('/cli/v1/vulnerabilities/rescan', { project_id: projectId });
14
+ if (!this.jsonEnabled()) {
15
+ this.log(`Scan queued for project ${flags.project}.`);
16
+ }
17
+ return res;
18
+ }
19
+ }
20
+ exports.default = VulnerabilitiesRescan;
@@ -0,0 +1,11 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class VulnerabilitiesRuntimes extends BaseCommand {
3
+ static aliases: string[];
4
+ static description: string;
5
+ static examples: string[];
6
+ static flags: {
7
+ page: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
8
+ project: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<unknown>;
11
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../base");
5
+ const output_1 = require("../../lib/output");
6
+ class VulnerabilitiesRuntimes extends base_1.BaseCommand {
7
+ static aliases = ['vulns:runtimes'];
8
+ static description = 'List the support state of the declared runtimes (Ruby, Node, Flutter…)';
9
+ static examples = [
10
+ '<%= config.bin %> vulnerabilities runtimes',
11
+ '<%= config.bin %> vulnerabilities runtimes --project acme-api --json',
12
+ ];
13
+ static flags = {
14
+ project: core_1.Flags.string({ char: 'p', description: 'Filter by project id (UUID) or key' }),
15
+ ...base_1.pageFlag,
16
+ };
17
+ async run() {
18
+ const { flags } = await this.parse(VulnerabilitiesRuntimes);
19
+ const query = new URLSearchParams({ page: String(flags.page) });
20
+ if (flags.project)
21
+ query.set('project_id', await this.resolveProjectId(flags.project));
22
+ const res = await this.api.get(`/cli/v1/vulnerabilities/runtimes?${query.toString()}`);
23
+ const runtimes = res.data ?? [];
24
+ if (!this.jsonEnabled()) {
25
+ this.log((0, output_1.renderTable)(['PROJECT', 'RUNTIME', 'VERSION', 'STATE', 'EOL', 'LATEST'], runtimes.map((runtime) => [
26
+ String(runtime.project?.key ?? ''),
27
+ String(runtime.name ?? ''),
28
+ String(runtime.version ?? ''),
29
+ (0, output_1.statusCell)(runtime.state),
30
+ // No EOL date means endoflife.date doesn't know the product: no alarm is possible.
31
+ String(runtime.eol_on ?? '—'),
32
+ String(runtime.latest ?? '—'),
33
+ ])));
34
+ }
35
+ return res;
36
+ }
37
+ }
38
+ exports.default = VulnerabilitiesRuntimes;
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from '../../base';
2
+ export default class VulnerabilitiesShow extends BaseCommand {
3
+ static aliases: string[];
4
+ static description: string;
5
+ static examples: string[];
6
+ static args: {
7
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
8
+ };
9
+ run(): Promise<unknown>;
10
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_1 = require("../../base");
5
+ const output_1 = require("../../lib/output");
6
+ /** A nested object of the payload, or `{}` when the field is absent. */
7
+ function nested(data, key) {
8
+ const value = data[key];
9
+ return value && typeof value === 'object' ? value : {};
10
+ }
11
+ class VulnerabilitiesShow extends base_1.BaseCommand {
12
+ static aliases = ['vulns:show'];
13
+ static description = 'Show a single vulnerability: advisory, package, manifest and the fixing version';
14
+ static examples = ['<%= config.bin %> vulnerabilities show <finding-id>'];
15
+ static args = {
16
+ id: core_1.Args.string({ description: 'Vulnerability finding id', required: true }),
17
+ };
18
+ async run() {
19
+ const { args } = await this.parse(VulnerabilitiesShow);
20
+ const res = await this.api.get(`/cli/v1/vulnerabilities/${args.id}`);
21
+ if (!this.jsonEnabled()) {
22
+ const data = res.data ?? {};
23
+ // Same blocks the promoted ticket carries: whoever reads either one decides with the same facts.
24
+ this.log((0, output_1.renderRecord)(data));
25
+ this.log((0, output_1.section)('Project', '📦'));
26
+ this.log((0, output_1.renderRecord)(nested(data, 'project')));
27
+ this.log((0, output_1.section)('Package', '🧩'));
28
+ this.log((0, output_1.renderRecord)({ ...nested(data, 'package'), manifest: nested(data, 'manifest').path ?? null }));
29
+ this.log((0, output_1.section)('Advisory', '🔒'));
30
+ this.log((0, output_1.renderRecord)(nested(data, 'advisory')));
31
+ }
32
+ return res;
33
+ }
34
+ }
35
+ exports.default = VulnerabilitiesShow;
@@ -14,7 +14,14 @@ export interface CliConfig {
14
14
  }
15
15
  export declare function configPath(): string;
16
16
  export declare function defaultApiUrl(): string;
17
+ /**
18
+ * Config exactly as stored on disk, ignoring env overrides. This is the base for mutations that get
19
+ * re-saved (login, clearToken): a temporary CLOSEYOURIT_API_URL / CLOSEYOURIT_TOKEN must never be
20
+ * written back to config.json, otherwise a one-off override would silently become permanent.
21
+ */
22
+ export declare function loadStoredConfig(): CliConfig;
23
+ /** Runtime config: env overrides win over the stored values, symmetrically for apiUrl and token. */
17
24
  export declare function loadConfig(): CliConfig;
18
25
  export declare function saveConfig(cfg: CliConfig): void;
19
- /** Remove token + identity, keep apiUrl. */
26
+ /** Remove token + identity, keep the on-disk apiUrl (never a temporary env override). */
20
27
  export declare function clearToken(): void;
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.configPath = configPath;
37
37
  exports.defaultApiUrl = defaultApiUrl;
38
+ exports.loadStoredConfig = loadStoredConfig;
38
39
  exports.loadConfig = loadConfig;
39
40
  exports.saveConfig = saveConfig;
40
41
  exports.clearToken = clearToken;
@@ -52,9 +53,16 @@ function configDir() {
52
53
  function configPath() {
53
54
  return path.join(configDir(), 'config.json');
54
55
  }
55
- function defaultApiUrl() {
56
+ /**
57
+ * API URL from CLOSEYOURIT_API_URL env (overrides the stored config apiUrl), for pointing the CLI
58
+ * at another server on the fly — symmetric with envToken(), so URL and token behave the same way.
59
+ */
60
+ function envApiUrl() {
56
61
  const fromEnv = process.env.CLOSEYOURIT_API_URL;
57
- return fromEnv && fromEnv.trim() !== '' ? fromEnv : DEFAULT_API_URL;
62
+ return fromEnv && fromEnv.trim() !== '' ? fromEnv.trim() : undefined;
63
+ }
64
+ function defaultApiUrl() {
65
+ return envApiUrl() ?? DEFAULT_API_URL;
58
66
  }
59
67
  /**
60
68
  * Token from CLOSEYOURIT_TOKEN env, for headless/CI/agent use (overrides the stored config token).
@@ -65,7 +73,12 @@ function envToken() {
65
73
  const fromEnv = process.env.CLOSEYOURIT_TOKEN;
66
74
  return fromEnv && fromEnv.trim() !== '' ? fromEnv.trim() : undefined;
67
75
  }
68
- function loadConfig() {
76
+ /**
77
+ * Config exactly as stored on disk, ignoring env overrides. This is the base for mutations that get
78
+ * re-saved (login, clearToken): a temporary CLOSEYOURIT_API_URL / CLOSEYOURIT_TOKEN must never be
79
+ * written back to config.json, otherwise a one-off override would silently become permanent.
80
+ */
81
+ function loadStoredConfig() {
69
82
  const file = configPath();
70
83
  let parsed = {};
71
84
  try {
@@ -78,12 +91,21 @@ function loadConfig() {
78
91
  parsed = {};
79
92
  }
80
93
  return {
81
- apiUrl: parsed.apiUrl && parsed.apiUrl.trim() !== '' ? parsed.apiUrl : defaultApiUrl(),
82
- token: envToken() ?? parsed.token,
94
+ apiUrl: parsed.apiUrl && parsed.apiUrl.trim() !== '' ? parsed.apiUrl : DEFAULT_API_URL,
95
+ token: parsed.token,
83
96
  account: parsed.account,
84
97
  organization: parsed.organization,
85
98
  };
86
99
  }
100
+ /** Runtime config: env overrides win over the stored values, symmetrically for apiUrl and token. */
101
+ function loadConfig() {
102
+ const stored = loadStoredConfig();
103
+ return {
104
+ ...stored,
105
+ apiUrl: envApiUrl() ?? stored.apiUrl,
106
+ token: envToken() ?? stored.token,
107
+ };
108
+ }
87
109
  function saveConfig(cfg) {
88
110
  const dir = configDir();
89
111
  fs.mkdirSync(dir, { recursive: true });
@@ -92,9 +114,9 @@ function saveConfig(cfg) {
92
114
  // Ensure perms even if the file pre-existed with a looser mode.
93
115
  fs.chmodSync(file, 0o600);
94
116
  }
95
- /** Remove token + identity, keep apiUrl. */
117
+ /** Remove token + identity, keep the on-disk apiUrl (never a temporary env override). */
96
118
  function clearToken() {
97
- const cfg = loadConfig();
119
+ const cfg = loadStoredConfig();
98
120
  delete cfg.token;
99
121
  delete cfg.account;
100
122
  delete cfg.organization;
@@ -0,0 +1,107 @@
1
+ /** Flag di un comando, così come lo espone `oclif.manifest.json`. */
2
+ export interface OclifFlag {
3
+ name: string;
4
+ type: 'boolean' | 'option';
5
+ char?: string;
6
+ description?: string;
7
+ required?: boolean;
8
+ options?: string[];
9
+ hidden?: boolean;
10
+ helpGroup?: string;
11
+ }
12
+ /** Argomento posizionale di un comando, dal manifest oclif. */
13
+ export interface OclifArg {
14
+ name: string;
15
+ description?: string;
16
+ required?: boolean;
17
+ options?: string[];
18
+ hidden?: boolean;
19
+ }
20
+ /** Comando risolto dal manifest oclif. */
21
+ export interface OclifCommand {
22
+ id: string;
23
+ description?: string;
24
+ summary?: string;
25
+ aliases?: string[];
26
+ hidden?: boolean;
27
+ flags?: Record<string, OclifFlag>;
28
+ args?: Record<string, OclifArg>;
29
+ examples?: Array<string | {
30
+ command: string;
31
+ description?: string;
32
+ }>;
33
+ }
34
+ /** Il manifest oclif (`oclif.manifest.json`). */
35
+ export interface OclifManifest {
36
+ version: string;
37
+ commands: Record<string, OclifCommand>;
38
+ }
39
+ export interface OpenCliArgument {
40
+ name: string;
41
+ required?: boolean;
42
+ description?: string;
43
+ acceptedValues?: string[];
44
+ hidden?: boolean;
45
+ }
46
+ export interface OpenCliOption {
47
+ name: string;
48
+ aliases?: string[];
49
+ description?: string;
50
+ required?: boolean;
51
+ recursive?: boolean;
52
+ hidden?: boolean;
53
+ arguments?: OpenCliArgument[];
54
+ }
55
+ export interface OpenCliExitCode {
56
+ code: number;
57
+ description?: string;
58
+ }
59
+ export interface OpenCliCommand {
60
+ name: string;
61
+ description?: string;
62
+ aliases?: string[];
63
+ hidden?: boolean;
64
+ options?: OpenCliOption[];
65
+ arguments?: OpenCliArgument[];
66
+ exitCodes?: OpenCliExitCode[];
67
+ examples?: string[];
68
+ commands?: OpenCliCommand[];
69
+ }
70
+ export interface OpenCliInfo {
71
+ title: string;
72
+ version: string;
73
+ summary?: string;
74
+ description?: string;
75
+ license?: {
76
+ name?: string;
77
+ identifier?: string;
78
+ url?: string;
79
+ };
80
+ }
81
+ export interface OpenCliDocument {
82
+ $schema: string;
83
+ opencli: string;
84
+ info: OpenCliInfo;
85
+ conventions: {
86
+ groupOptions: boolean;
87
+ optionSeparator: string;
88
+ };
89
+ command: OpenCliCommand;
90
+ }
91
+ export interface BuildOpenCliInput {
92
+ manifest: OclifManifest;
93
+ bin: string;
94
+ description?: string;
95
+ summary?: string;
96
+ license?: {
97
+ name?: string;
98
+ identifier?: string;
99
+ url?: string;
100
+ };
101
+ /** Descrizioni dei topic (contenitori senza comando proprio), indicizzate per id oclif. */
102
+ topics?: Record<string, {
103
+ description?: string;
104
+ }>;
105
+ }
106
+ /** Costruisce il documento OpenCLI completo dal manifest oclif e dai metadati di package.json. */
107
+ export declare function buildOpenCliDocument(input: BuildOpenCliInput): OpenCliDocument;