@waron97/prbot 3.3.0 → 3.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.
package/src/index.js CHANGED
@@ -18,54 +18,102 @@ import { main as prMain } from './commands/pr.js';
18
18
  import { routine } from './commands/routine.js';
19
19
  import { verbot } from './commands/ver.js';
20
20
  import { CONFIG_FILE } from './config.js';
21
- import { setSilent } from './lib/logger.js';
21
+ import { error, log, setSilent } from './lib/logger.js';
22
22
  import { checkForUpdate, currentVersion } from './lib/updateCheck.js';
23
23
 
24
+ // Commands that never talk to RIP/DevOps/Trident/Keycloak. They still read
25
+ // non-secret config (e.g. ADDONS_PATH) from the same file, but must not end
26
+ // up holding credentials in their process env — a coding agent invoking one
27
+ // of these should not even technically be able to leak or reuse a secret it
28
+ // never needed.
29
+ const LOCAL_COMMANDS = new Set(['init', 'ver', 'commit', 'changelog']);
30
+ const SECRET_ENV_KEYS = [
31
+ 'KC_USER',
32
+ 'KC_PASSWORD',
33
+ 'KC_ID',
34
+ 'KC_SECRET',
35
+ 'DEVOPS_TOKEN',
36
+ 'TRIDENT_TOKEN',
37
+ ];
38
+ const OFFLINE = process.env.PRBOT_OFFLINE === '1' || process.argv.includes('--offline');
39
+
24
40
  configDotenv({ path: CONFIG_FILE, quiet: true });
25
41
 
42
+ const invokedCommand = process.argv[2];
43
+ if (LOCAL_COMMANDS.has(invokedCommand)) {
44
+ for (const key of SECRET_ENV_KEYS) delete process.env[key];
45
+ }
46
+
26
47
  let _updateAvailable = null;
27
- checkForUpdate().then((v) => {
28
- _updateAvailable = v;
29
- });
48
+ if (!OFFLINE) {
49
+ checkForUpdate().then((v) => {
50
+ _updateAvailable = v;
51
+ });
52
+ }
30
53
 
31
54
  process.on('exit', () => {
32
55
  if (_updateAvailable) {
33
- console.log(
34
- `\nUpdate available: ${currentVersion} → ${_updateAvailable}\nRun: prbot update`
35
- );
56
+ log(`\nUpdate available: ${currentVersion} → ${_updateAvailable}\nRun: prbot update`);
36
57
  }
37
58
  });
38
59
 
60
+ // Single point of convergence for every command's failure. Exit code 0 must
61
+ // mean the requested operation actually succeeded; nothing downstream of
62
+ // this should ever swallow an error into a zero exit.
63
+ function fail(err) {
64
+ error(`Error: ${err?.message ?? err}`);
65
+ process.exitCode = 1;
66
+ }
67
+
68
+ process.on('unhandledRejection', (err) => {
69
+ fail(err instanceof Error ? err : new Error(String(err)));
70
+ });
71
+ process.on('uncaughtException', (err) => {
72
+ fail(err);
73
+ });
74
+
75
+ /**
76
+ * Adds `--quiet`/`--silent` to a command and returns a helper that reads
77
+ * both, warning once if the deprecated `--silent` alias is used. `--silent`
78
+ * used to also swallow errors (so a failed export could report success);
79
+ * it no longer does — both flags only suppress informational output.
80
+ */
81
+ function withQuiet(cmd) {
82
+ return cmd
83
+ .option('-q, --quiet', 'Suppress informational output (errors still fail the command)')
84
+ .option('-s, --silent', 'Deprecated alias of --quiet; no longer swallows errors');
85
+ }
86
+
87
+ function resolveQuiet(opts) {
88
+ if (opts.silent) {
89
+ error('Warning: --silent is deprecated and no longer swallows errors; use --quiet.');
90
+ }
91
+ return Boolean(opts.quiet || opts.silent);
92
+ }
93
+
39
94
  program
40
95
  .command('pr <module>')
41
96
  .option('-b, --bump <level>')
42
- .action((module, opts) => {
43
- prMain(module)
44
- .then(() => {
45
- if (opts.bump) {
46
- return verbot(module, opts.bump);
47
- }
48
- })
49
- .catch((err) => {
50
- throw err;
51
- });
97
+ .action(async (module, opts) => {
98
+ await prMain(module);
99
+ if (opts.bump) await verbot(module, opts.bump);
52
100
  });
53
101
 
54
102
  program
55
103
  .command('ver <module>')
56
104
  .option('-b, --bump <level>')
57
- .action((module, opts) => {
105
+ .action(async (module, opts) => {
58
106
  if (!opts.bump) {
59
107
  throw new Error('No bump level specified');
60
108
  }
61
- verbot(module, opts.bump);
109
+ await verbot(module, opts.bump);
62
110
  });
63
111
 
64
112
  program
65
113
  .command('init')
66
114
  .description('Create config file')
67
- .action(() => {
68
- init();
115
+ .action(async () => {
116
+ await init();
69
117
  });
70
118
 
71
119
  const collect = (val, prev) => [...(prev ?? []), val];
@@ -75,10 +123,8 @@ program
75
123
  .option('-t, --trident <code>', 'Trident issue codes (repeatable)', collect)
76
124
  .option('-j, --jira <code>', 'JIRA issue codes (repeatable)', collect)
77
125
  .option('-m, --message <text>', 'Changelog entry message')
78
- .action((prNumber, opts) => {
79
- changelog(prNumber, opts).catch((err) => {
80
- throw err;
81
- });
126
+ .action(async (prNumber, opts) => {
127
+ await changelog(prNumber, opts);
82
128
  });
83
129
 
84
130
  program
@@ -89,107 +135,86 @@ program
89
135
  .option('-b, --branch <name>', 'Branch name (default: autopr_<taskId>)')
90
136
  .option('-n, --name <text>', 'PR title (default: task name from Odoo)')
91
137
  .option('--amend', 'Amend existing PR on current branch with new trident/jira refs')
92
- .action((opts) => {
93
- autopr(opts).catch((err) => {
94
- throw err;
95
- });
138
+ .action(async (opts) => {
139
+ await autopr(opts);
96
140
  });
97
141
 
98
- program.command('commit').action((opts) => {
99
- commit(opts).catch((err) => {
100
- throw err;
101
- });
142
+ program.command('commit').action(async (opts) => {
143
+ await commit(opts);
102
144
  });
103
145
 
104
146
  const exportCmd = program.command('export');
105
147
 
106
- exportCmd
107
- .command('workflow')
108
- .option('--no-commit')
109
- .option('-b, --bump <level>', 'Version bump level (patch, minor, major)')
110
- .option('-m, --module <id>', 'Module/workflow ID to export (skips interactive selection)')
111
- .option('-s, --silent', 'Suppress all output and swallow errors')
112
- .option(
113
- '--auto-premigrate',
114
- 'Auto-generate pre-migrate script when XML ID renames are detected (no prompt)'
115
- )
116
- .action((opts) => {
117
- if (opts.silent) setSilent(true);
118
- exportWorkflow(opts).catch((err) => {
119
- if (!opts.silent) throw err;
120
- });
121
- });
148
+ withQuiet(
149
+ exportCmd
150
+ .command('workflow')
151
+ .option('--no-commit')
152
+ .option('-b, --bump <level>', 'Version bump level (patch, minor, major)')
153
+ .option('-m, --module <id>', 'Module/workflow ID to export (skips interactive selection)')
154
+ .option(
155
+ '--auto-premigrate',
156
+ 'Auto-generate pre-migrate script when XML ID renames are detected (no prompt)'
157
+ )
158
+ ).action(async (opts) => {
159
+ if (resolveQuiet(opts)) setSilent(true);
160
+ await exportWorkflow(opts);
161
+ });
122
162
 
123
163
  exportCmd.command('rip').action(() => exportRip());
124
164
 
125
- exportCmd
126
- .command('pb')
127
- .option('--no-commit')
128
- .option('-s, --silent', 'Suppress all output and swallow errors')
129
- .action((opts) => {
130
- if (opts.silent) setSilent(true);
131
- exportPb(opts).catch((err) => {
132
- if (!opts.silent) throw err;
133
- });
134
- });
165
+ withQuiet(exportCmd.command('pb').option('--no-commit')).action(async (opts) => {
166
+ if (resolveQuiet(opts)) setSilent(true);
167
+ await exportPb(opts);
168
+ });
135
169
 
136
- exportCmd
137
- .command('imperex')
138
- .option('--no-commit')
139
- .option('-s, --silent', 'Suppress all output and swallow errors')
140
- .action((opts) => {
141
- if (opts.silent) setSilent(true);
142
- exportImperex(opts).catch((err) => {
143
- if (!opts.silent) throw err;
144
- });
145
- });
170
+ withQuiet(exportCmd.command('imperex').option('--no-commit')).action(async (opts) => {
171
+ if (resolveQuiet(opts)) setSilent(true);
172
+ await exportImperex(opts);
173
+ });
146
174
 
147
- exportCmd
148
- .command('lrp')
149
- .option('--no-commit')
150
- .option('-s, --silent', 'Suppress all output and swallow errors')
151
- .action((opts) => {
152
- if (opts.silent) setSilent(true);
153
- exportLrp(opts).catch((err) => {
154
- if (!opts.silent) throw err;
155
- });
156
- });
175
+ withQuiet(exportCmd.command('lrp').option('--no-commit')).action(async (opts) => {
176
+ if (resolveQuiet(opts)) setSilent(true);
177
+ await exportLrp(opts);
178
+ });
157
179
 
158
- exportCmd
159
- .command('email-templates')
160
- .option('--no-commit')
161
- .option('-b, --bump <level>', 'Version bump level (patch, minor, major)')
162
- .option('-e, --exclude <value...>', 'exclude templates matching id, name, or template_code')
163
- .option('-m, --module <name>', 'module directory name (skip prompt)')
164
- .option('-w, --workflow <value>', 'workflow name or id (skip prompt)')
165
- .option('-s, --silent', 'Suppress all output and swallow errors')
166
- .option(
167
- '--auto-premigrate',
168
- 'Auto-generate pre-migrate script when XML ID renames are detected (no prompt)'
169
- )
170
- .action((opts) => {
171
- if (opts.silent) setSilent(true);
172
- exportEmailTemplates(opts).catch((err) => {
173
- if (!opts.silent) throw err;
174
- });
175
- });
180
+ withQuiet(
181
+ exportCmd
182
+ .command('email-templates')
183
+ .option('--no-commit')
184
+ .option('-b, --bump <level>', 'Version bump level (patch, minor, major)')
185
+ .option('-e, --exclude <value...>', 'exclude templates matching id, name, or template_code')
186
+ .option('-m, --module <name>', 'module directory name (skip prompt)')
187
+ .option('-w, --workflow <value>', 'workflow name or id (skip prompt)')
188
+ .option(
189
+ '--auto-premigrate',
190
+ 'Auto-generate pre-migrate script when XML ID renames are detected (no prompt)'
191
+ )
192
+ ).action(async (opts) => {
193
+ if (resolveQuiet(opts)) setSilent(true);
194
+ await exportEmailTemplates(opts);
195
+ });
176
196
 
177
- program.command('routine').action(() => {
178
- routine().catch((err) => {
179
- throw err;
180
- });
197
+ program.command('routine').action(async () => {
198
+ await routine();
181
199
  });
182
200
 
183
- program.command('update').action(() => {
184
- console.log('Updating prbot...');
185
- execFile('npm', ['i', '-g', '@waron97/prbot'], (error, stdout, stderr) => {
186
- if (error) {
187
- console.error(stderr || error.message);
188
- process.exit(1);
189
- }
190
- console.log(stdout);
191
- console.log('Done.');
201
+ program
202
+ .command('update [version]')
203
+ .description('Update the global prbot install (defaults to latest if no version given)')
204
+ .action(async (version) => {
205
+ const target = version ? `@waron97/prbot@${version}` : '@waron97/prbot';
206
+ log(version ? `Updating prbot to ${version}...` : 'Updating prbot to latest...');
207
+ await new Promise((resolve, reject) => {
208
+ execFile('npm', ['i', '-g', target], (err, stdout, stderr) => {
209
+ if (err) {
210
+ reject(new Error(stderr || err.message));
211
+ return;
212
+ }
213
+ log(stdout);
214
+ log('Done.');
215
+ resolve();
216
+ });
217
+ });
192
218
  });
193
- });
194
219
 
195
- program.parse();
220
+ program.parseAsync().catch(fail);
package/src/lib/auth.js CHANGED
@@ -18,7 +18,25 @@ async function getToken() {
18
18
  body: payload.toString(),
19
19
  });
20
20
 
21
- const json = await response.json();
21
+ if (!response.ok) {
22
+ // Never log the response body: on a password-grant endpoint it can
23
+ // echo back the submitted credentials.
24
+ throw new Error(
25
+ `AUTH_FAILED: Keycloak token request failed with status ${response.status}`
26
+ );
27
+ }
28
+
29
+ let json;
30
+ try {
31
+ json = await response.json();
32
+ } catch {
33
+ throw new Error('AUTH_FAILED: Keycloak response was not valid JSON');
34
+ }
35
+
36
+ if (!json || typeof json.access_token !== 'string' || !json.access_token) {
37
+ throw new Error('AUTH_FAILED: Keycloak response did not contain an access_token');
38
+ }
39
+
22
40
  return json.access_token;
23
41
  }
24
42
 
package/src/lib/logger.js CHANGED
@@ -8,8 +8,19 @@ function isSilent() {
8
8
  return silent;
9
9
  }
10
10
 
11
+ // Informational output — suppressed by --quiet/--silent.
11
12
  function log(...args) {
12
13
  if (!silent) console.log(...args);
13
14
  }
14
15
 
15
- export { setSilent, isSilent, log };
16
+ // Warnings and errors are never suppressed: --quiet reduces noise, it must
17
+ // never hide something the user needs to see to trust the exit code.
18
+ function warn(...args) {
19
+ console.warn(...args);
20
+ }
21
+
22
+ function error(...args) {
23
+ console.error(...args);
24
+ }
25
+
26
+ export { setSilent, isSilent, log, warn, error };