@bussolabs/closeyourit-cli 0.17.0 → 0.17.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.
@@ -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;
@@ -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;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ // Genera un documento OpenCLI (https://opencli.org) a partire dal manifest oclif.
3
+ //
4
+ // Perché: `opencli.json` scritto a mano driftava — dichiarava una versione e un elenco di comandi
5
+ // fermi a mesi prima. Qui la fonte è il manifest oclif, che è sempre allineato ai comandi reali,
6
+ // così il file non può più restare indietro. La parte di I/O (leggere il manifest, scrivere il
7
+ // file) vive in `scripts/generate-opencli.mjs`; qui c'è solo la trasformazione pura, testabile.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.buildOpenCliDocument = buildOpenCliDocument;
10
+ // Versione della specifica OpenCLI a cui il documento si conforma.
11
+ const OPENCLI_VERSION = '0.1';
12
+ const SCHEMA_URL = 'https://opencli.org/draft.json';
13
+ // oclif separa i segmenti dell'id con ':' nel manifest (indipendentemente dal topicSeparator).
14
+ const ID_SEPARATOR = ':';
15
+ // Opzioni globali: iniettate da oclif su ogni comando, quindi dichiarate una volta sola sul
16
+ // comando radice come `recursive`. Sono invarianti strutturali del runtime, non comandi che
17
+ // driftano, perciò vivono qui come costante e non nel manifest.
18
+ const GLOBAL_OPTIONS = [
19
+ { name: '--help', aliases: ['-h'], description: 'Show help.', recursive: true },
20
+ { name: '--version', aliases: ['-V'], description: 'Show CLI version.' },
21
+ { name: '--json', description: 'Format output as json.', recursive: true },
22
+ ];
23
+ // Codici di uscita del CLI: convenzione stabile, condivisa da tutti i comandi.
24
+ const EXIT_CODES = [
25
+ { code: 0, description: 'Success' },
26
+ { code: 1, description: 'Runtime or API error' },
27
+ { code: 2, description: 'Invalid usage (bad flags/arguments)' },
28
+ ];
29
+ const upper = (name) => name.toUpperCase();
30
+ const mapArgument = (arg) => {
31
+ const out = { name: upper(arg.name) };
32
+ if (arg.required)
33
+ out.required = true;
34
+ if (arg.description)
35
+ out.description = arg.description;
36
+ if (arg.options && arg.options.length > 0)
37
+ out.acceptedValues = arg.options;
38
+ if (arg.hidden)
39
+ out.hidden = true;
40
+ return out;
41
+ };
42
+ const mapFlag = (flag) => {
43
+ const out = { name: `--${flag.name}` };
44
+ if (flag.char)
45
+ out.aliases = [`-${flag.char}`];
46
+ if (flag.description)
47
+ out.description = flag.description;
48
+ if (flag.required)
49
+ out.required = true;
50
+ if (flag.hidden)
51
+ out.hidden = true;
52
+ if (flag.type === 'option') {
53
+ const argument = { name: upper(flag.name), required: true };
54
+ if (flag.options && flag.options.length > 0)
55
+ argument.acceptedValues = flag.options;
56
+ out.arguments = [argument];
57
+ }
58
+ return out;
59
+ };
60
+ const substituteBin = (text, bin) => text.replaceAll('<%= config.bin %>', bin);
61
+ const mapExamples = (examples, bin) => {
62
+ if (!examples || examples.length === 0)
63
+ return undefined;
64
+ return examples.map((ex) => substituteBin(typeof ex === 'string' ? ex : ex.command, bin));
65
+ };
66
+ // Applica al nodo-comando i dati del suo comando oclif. Il flag globale --json (helpGroup GLOBAL)
67
+ // non viene ripetuto: è già dichiarato ricorsivo sul comando radice.
68
+ const applyCommand = (node, cmd, bin) => {
69
+ node.description = cmd.summary ?? cmd.description;
70
+ if (cmd.hidden)
71
+ node.hidden = true;
72
+ if (cmd.aliases && cmd.aliases.length > 0)
73
+ node.aliases = cmd.aliases;
74
+ const flags = Object.values(cmd.flags ?? {}).filter((f) => f.helpGroup !== 'GLOBAL');
75
+ if (flags.length > 0)
76
+ node.options = flags.map((f) => mapFlag(f));
77
+ const args = Object.values(cmd.args ?? {});
78
+ if (args.length > 0)
79
+ node.arguments = args.map((a) => mapArgument(a));
80
+ const examples = mapExamples(cmd.examples, bin);
81
+ if (examples)
82
+ node.examples = examples;
83
+ };
84
+ const buildTree = (manifest) => {
85
+ const root = { name: '', path: [], children: new Map() };
86
+ for (const id of Object.keys(manifest.commands)) {
87
+ const segments = id.split(ID_SEPARATOR);
88
+ let node = root;
89
+ const path = [];
90
+ for (const segment of segments) {
91
+ path.push(segment);
92
+ let child = node.children.get(segment);
93
+ if (!child) {
94
+ child = { name: segment, path: [...path], children: new Map() };
95
+ node.children.set(segment, child);
96
+ }
97
+ node = child;
98
+ }
99
+ node.command = manifest.commands[id];
100
+ }
101
+ return root;
102
+ };
103
+ const toOpenCliCommand = (node, bin, topics) => {
104
+ const out = { name: node.name };
105
+ if (node.command) {
106
+ applyCommand(out, node.command, bin);
107
+ }
108
+ else {
109
+ // Topic contenitore: nessun comando proprio, la descrizione arriva da package.json (se c'è).
110
+ const topic = topics[node.path.join(ID_SEPARATOR)];
111
+ if (topic?.description)
112
+ out.description = topic.description;
113
+ }
114
+ if (node.children.size > 0) {
115
+ out.commands = [...node.children.values()]
116
+ .sort((a, b) => a.name.localeCompare(b.name))
117
+ .map((child) => toOpenCliCommand(child, bin, topics));
118
+ }
119
+ return out;
120
+ };
121
+ /** Costruisce il documento OpenCLI completo dal manifest oclif e dai metadati di package.json. */
122
+ function buildOpenCliDocument(input) {
123
+ const { manifest, bin, topics = {} } = input;
124
+ const info = { title: bin, version: manifest.version };
125
+ if (input.summary)
126
+ info.summary = input.summary;
127
+ if (input.description)
128
+ info.description = input.description;
129
+ if (input.license && (input.license.name || input.license.identifier || input.license.url)) {
130
+ info.license = input.license;
131
+ }
132
+ const tree = buildTree(manifest);
133
+ const command = {
134
+ name: bin,
135
+ ...(input.description ? { description: input.description } : {}),
136
+ options: GLOBAL_OPTIONS,
137
+ exitCodes: EXIT_CODES,
138
+ commands: [...tree.children.values()]
139
+ .sort((a, b) => a.name.localeCompare(b.name))
140
+ .map((child) => toOpenCliCommand(child, bin, topics)),
141
+ };
142
+ return {
143
+ $schema: SCHEMA_URL,
144
+ opencli: OPENCLI_VERSION,
145
+ info,
146
+ conventions: { groupOptions: true, optionSeparator: ' ' },
147
+ command,
148
+ };
149
+ }