@capraconsulting/cals-cli 3.14.0 → 3.15.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
@@ -13,52 +13,61 @@ It is recommended to use `npx` over global install to ensure you
13
13
  always run the latest version. If you install it globally remember
14
14
  to update it before running.
15
15
 
16
- ## Build
16
+ ## Commands
17
17
 
18
- Build and verify:
18
+ ### Authentication
19
19
 
20
- ```sh
21
- $ make # or "make build"
20
+ Set your GitHub token (will be stored in the OS keychain):
21
+
22
+ ```bash
23
+ cals auth
22
24
  ```
23
25
 
24
- ## Contributing
26
+ ### List repositories
25
27
 
26
- This project uses [semantic release](https://semantic-release.gitbook.io/semantic-release/)
27
- to automate releases and follows
28
- [Git commit guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit)
29
- from the Angular project.
28
+ ```bash
29
+ cals repos --org capralifecycle
30
+ cals repos --org capralifecycle --compact
31
+ cals repos --org capralifecycle --csv
32
+ ```
33
+
34
+ ### List repository groups
35
+
36
+ ```bash
37
+ cals groups --org capralifecycle
38
+ ```
39
+
40
+ ### Generate clone commands
30
41
 
31
- Version numbers depend on the commit type and footers: https://github.com/semantic-release/commit-analyzer/blob/75c9c87c88772d7ded4ca9614852b42519e41931/lib/default-release-rules.js#L7-L12
42
+ Generate clone commands (pipe to bash to execute):
32
43
 
33
- ## Goals of CLI
44
+ ```bash
45
+ cals clone --org capralifecycle --all | bash
46
+ cals clone --org capralifecycle mygroup | bash
47
+ ```
34
48
 
35
- - Provide an uniform way of consistently doing repeatable CALS tasks
36
- - Provide simple guidelines to improve the experience for developers
37
- - A tool that everybody uses and gets ownership of
38
- - Automate repeatable CALS tasks as we go
49
+ ### Sync repositories
39
50
 
40
- ## Ideas and future work
51
+ Pull latest changes for all repositories in a directory managed by a `.cals.yaml` manifest:
41
52
 
42
- - Automate onboarding of people
43
- - Granting access to various resources: AWS, GitHub, Confluence, JIRA, Slack, ...
44
- - Automate offboarding of people
45
- - Automate generation of new projects/resources
46
- - Creating GitHub repos, giving permissions etc
47
- - Slack channels
48
- - AWS account and structure
49
- - Checklist for manual processes
50
- - AWS infrastructure management, e.g. scripts such as https://github.com/capralifecycle/rvr-aws-infrastructure/blob/master/rvr/create-stack.sh
51
- - `cals aws ...`
53
+ ```bash
54
+ cals sync
55
+ cals sync --clone # Prompt to clone missing repos
56
+ ```
52
57
 
53
- ### Snyk management
58
+ ## Build
54
59
 
55
- https://snyk.docs.apiary.io/reference/projects
60
+ Build and verify:
56
61
 
57
- - [ ] Automatically set up project in Snyk
58
- - [x] Report of which repos are in Snyk and which is not
59
- - [ ] Detect active vs disabled projects in Snyk (no way through API now?)
60
- - [x] Report issues in Snyk grouped by our projects
62
+ ```sh
63
+ $ make # or "make build"
64
+ ```
61
65
 
62
66
  ## Contributing
63
67
 
64
- This project doesn't currently accept contributions. For inquiries, please contact the maintainers at [Slack](https://liflig.slack.com/archives/C02T4KTPYS2).
68
+ This project uses [semantic release](https://semantic-release.gitbook.io/semantic-release/)
69
+ to automate releases and follows
70
+ [Git commit guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit)
71
+ from the Angular project.
72
+
73
+ For inquiries, please contact the maintainers at [Slack](https://liflig.slack.com/archives/C02T4KTPYS2).
package/lib/cals-cli.mjs CHANGED
@@ -3,23 +3,23 @@ import * as process from 'node:process';
3
3
  import process__default from 'node:process';
4
4
  import yargs from 'yargs';
5
5
  import { hideBin } from 'yargs/helpers';
6
+ import keytar from 'keytar';
7
+ import readline from 'node:readline';
8
+ import chalk from 'chalk';
6
9
  import fs from 'node:fs';
7
10
  import path from 'node:path';
11
+ import os from 'node:os';
12
+ import cachedir from 'cachedir';
8
13
  import { Buffer } from 'node:buffer';
9
14
  import { performance } from 'node:perf_hooks';
10
15
  import { Octokit } from '@octokit/rest';
11
16
  import pLimit from 'p-limit';
12
- import keytar from 'keytar';
13
- import os from 'node:os';
14
- import cachedir from 'cachedir';
15
- import readline from 'node:readline';
16
- import chalk from 'chalk';
17
17
  import { findUp } from 'find-up';
18
18
  import yaml from 'js-yaml';
19
19
  import AJV from 'ajv';
20
20
  import { execa } from 'execa';
21
21
 
22
- var version = "3.14.0";
22
+ var version = "3.15.0";
23
23
  var engines = {
24
24
  node: ">=22.14.0"
25
25
  };
@@ -46,6 +46,239 @@ class GitHubTokenCliProvider {
46
46
  }
47
47
  }
48
48
 
49
+ const CLEAR_WHOLE_LINE = 0;
50
+ function clearLine(stdout) {
51
+ readline.clearLine(stdout, CLEAR_WHOLE_LINE);
52
+ readline.cursorTo(stdout, 0);
53
+ }
54
+ async function readInput(options) {
55
+ process__default.stdout.write(options.prompt);
56
+ // For silent mode, read character by character with raw mode to hide input
57
+ if (options.silent && process__default.stdin.isTTY) {
58
+ return new Promise((resolve, reject) => {
59
+ let input = "";
60
+ process__default.stdin.setRawMode(true);
61
+ process__default.stdin.resume();
62
+ process__default.stdin.setEncoding("utf8");
63
+ const timer = options.timeout
64
+ ? setTimeout(() => {
65
+ cleanup();
66
+ reject(new Error("Input timed out"));
67
+ }, options.timeout)
68
+ : null;
69
+ const cleanup = () => {
70
+ process__default.stdin.setRawMode(false);
71
+ process__default.stdin.pause();
72
+ process__default.stdin.removeListener("data", onData);
73
+ if (timer)
74
+ clearTimeout(timer);
75
+ };
76
+ const onData = (char) => {
77
+ if (char === "\r" || char === "\n") {
78
+ cleanup();
79
+ process__default.stdout.write("\n");
80
+ resolve(input);
81
+ }
82
+ else if (char === "\u0003") {
83
+ // Ctrl+C
84
+ cleanup();
85
+ process__default.exit(1);
86
+ }
87
+ else if (char === "\u007F" || char === "\b") {
88
+ // Backspace
89
+ input = input.slice(0, -1);
90
+ }
91
+ else {
92
+ input += char;
93
+ }
94
+ };
95
+ process__default.stdin.on("data", onData);
96
+ });
97
+ }
98
+ // Normal (non-silent) mode
99
+ const rl = readline.createInterface({
100
+ input: process__default.stdin,
101
+ output: process__default.stdout,
102
+ });
103
+ return new Promise((resolve, reject) => {
104
+ const timer = options.timeout
105
+ ? setTimeout(() => {
106
+ rl.close();
107
+ reject(new Error("Input timed out"));
108
+ }, options.timeout)
109
+ : null;
110
+ rl.question("", (answer) => {
111
+ if (timer)
112
+ clearTimeout(timer);
113
+ rl.close();
114
+ resolve(answer);
115
+ });
116
+ });
117
+ }
118
+ class Reporter {
119
+ stdout = process__default.stdout;
120
+ stderr = process__default.stderr;
121
+ format = chalk;
122
+ error(msg) {
123
+ clearLine(this.stderr);
124
+ this.stderr.write(`${this.format.red("error")} ${msg}\n`);
125
+ }
126
+ log(msg) {
127
+ clearLine(this.stdout);
128
+ this.stdout.write(`${msg}\n`);
129
+ }
130
+ warn(msg) {
131
+ clearLine(this.stderr);
132
+ this.stderr.write(`${this.format.yellow("warning")} ${msg}\n`);
133
+ }
134
+ info(msg) {
135
+ clearLine(this.stdout);
136
+ this.stdout.write(`${this.format.blue("info")} ${msg}\n`);
137
+ }
138
+ }
139
+
140
+ class CacheProvider {
141
+ constructor(config) {
142
+ this.config = config;
143
+ }
144
+ mustValidate = false;
145
+ config;
146
+ defaultCacheTime = 1800;
147
+ /**
148
+ * Retrieve cache if existent, ignoring the time.
149
+ *
150
+ * The caller is responsible for handling proper validation,
151
+ */
152
+ retrieveJson(cachekey) {
153
+ const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
154
+ if (!fs.existsSync(cachefile)) {
155
+ return undefined;
156
+ }
157
+ const data = fs.readFileSync(cachefile, "utf-8");
158
+ return {
159
+ cacheTime: fs.statSync(cachefile).mtime.getTime(),
160
+ data: (data === "undefined" ? undefined : JSON.parse(data)),
161
+ };
162
+ }
163
+ /**
164
+ * Save data to cache.
165
+ */
166
+ storeJson(cachekey, data) {
167
+ const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
168
+ if (!fs.existsSync(this.config.cacheDir)) {
169
+ fs.mkdirSync(this.config.cacheDir, { recursive: true });
170
+ }
171
+ fs.writeFileSync(cachefile, data === undefined ? "undefined" : JSON.stringify(data));
172
+ }
173
+ async json(cachekey, block, cachetime = this.defaultCacheTime) {
174
+ const cacheItem = this.mustValidate
175
+ ? undefined
176
+ : this.retrieveJson(cachekey);
177
+ const expire = new Date(Date.now() - cachetime * 1000).getTime();
178
+ if (cacheItem !== undefined && cacheItem.cacheTime > expire) {
179
+ return cacheItem.data;
180
+ }
181
+ const result = await block();
182
+ this.storeJson(cachekey, result);
183
+ return result;
184
+ }
185
+ /**
186
+ * Delete all cached data.
187
+ */
188
+ cleanup() {
189
+ fs.rmSync(this.config.cacheDir, { recursive: true, force: true });
190
+ }
191
+ }
192
+
193
+ class Config {
194
+ cwd = path.resolve(process__default.cwd());
195
+ configFile = path.join(os.homedir(), ".cals-config.json");
196
+ cacheDir = cachedir("cals-cli");
197
+ configCached = undefined;
198
+ get config() {
199
+ const existingConfig = this.configCached;
200
+ if (existingConfig !== undefined) {
201
+ return existingConfig;
202
+ }
203
+ const config = this.readConfig();
204
+ this.configCached = config;
205
+ return config;
206
+ }
207
+ readConfig() {
208
+ if (!fs.existsSync(this.configFile)) {
209
+ return {};
210
+ }
211
+ try {
212
+ return JSON.parse(fs.readFileSync(this.configFile, "utf-8"));
213
+ }
214
+ catch (e) {
215
+ console.error("Failed", e);
216
+ throw new Error("Failed to read config");
217
+ }
218
+ }
219
+ getConfig(key) {
220
+ return this.config[key];
221
+ }
222
+ requireConfig(key) {
223
+ const result = this.config[key];
224
+ if (result === undefined) {
225
+ throw Error(`Configuration for ${key} missing. Add manually to ${this.configFile}`);
226
+ }
227
+ return result;
228
+ }
229
+ updateConfig(key, value) {
230
+ const updatedConfig = {
231
+ ...this.readConfig(),
232
+ [key]: value, // undefined will remove
233
+ };
234
+ fs.writeFileSync(this.configFile, JSON.stringify(updatedConfig, null, " "));
235
+ this.configCached = updatedConfig;
236
+ }
237
+ }
238
+
239
+ function createReporter() {
240
+ return new Reporter();
241
+ }
242
+ function createCacheProvider(config, argv) {
243
+ const cache = new CacheProvider(config);
244
+ if (argv.noCache === true) {
245
+ cache.mustValidate = true;
246
+ }
247
+ return cache;
248
+ }
249
+ function createConfig() {
250
+ return new Config();
251
+ }
252
+
253
+ async function authenticate({ reporter, token, tokenProvider, }) {
254
+ if (token === undefined) {
255
+ reporter.info("Need API token to talk to GitHub");
256
+ reporter.info("https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook");
257
+ const inputToken = await readInput({
258
+ prompt: "Enter GitHub token: ",
259
+ silent: true,
260
+ });
261
+ token = inputToken;
262
+ }
263
+ await tokenProvider.setToken(token);
264
+ reporter.info("Token saved to keychain");
265
+ }
266
+ const command$5 = {
267
+ command: "auth [token]",
268
+ describe: "Authenticate with GitHub",
269
+ builder: (yargs) => yargs.positional("token", {
270
+ describe: "GitHub token (prompted if not provided)",
271
+ type: "string",
272
+ }),
273
+ handler: async (argv) => {
274
+ await authenticate({
275
+ reporter: createReporter(),
276
+ token: argv.token,
277
+ tokenProvider: new GitHubTokenCliProvider(),
278
+ });
279
+ },
280
+ };
281
+
49
282
  class GitHubService {
50
283
  octokit;
51
284
  cache;
@@ -274,221 +507,45 @@ function includesTopic(repo, topic) {
274
507
  return repo.repositoryTopics.edges.some((it) => it.node.topic.name === topic);
275
508
  }
276
509
 
277
- class CacheProvider {
278
- constructor(config) {
279
- this.config = config;
280
- }
281
- mustValidate = false;
282
- config;
283
- defaultCacheTime = 1800;
284
- /**
285
- * Retrieve cache if existent, ignoring the time.
286
- *
287
- * The caller is responsible for handling proper validation,
288
- */
289
- retrieveJson(cachekey) {
290
- const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
291
- if (!fs.existsSync(cachefile)) {
292
- return undefined;
293
- }
294
- const data = fs.readFileSync(cachefile, "utf-8");
295
- return {
296
- cacheTime: fs.statSync(cachefile).mtime.getTime(),
297
- data: (data === "undefined" ? undefined : JSON.parse(data)),
298
- };
299
- }
300
- /**
301
- * Save data to cache.
302
- */
303
- storeJson(cachekey, data) {
304
- const cachefile = path.join(this.config.cacheDir, `${cachekey}.json`);
305
- if (!fs.existsSync(this.config.cacheDir)) {
306
- fs.mkdirSync(this.config.cacheDir, { recursive: true });
307
- }
308
- fs.writeFileSync(cachefile, data === undefined ? "undefined" : JSON.stringify(data));
309
- }
310
- async json(cachekey, block, cachetime = this.defaultCacheTime) {
311
- const cacheItem = this.mustValidate
312
- ? undefined
313
- : this.retrieveJson(cachekey);
314
- const expire = new Date(Date.now() - cachetime * 1000).getTime();
315
- if (cacheItem !== undefined && cacheItem.cacheTime > expire) {
316
- return cacheItem.data;
317
- }
318
- const result = await block();
319
- this.storeJson(cachekey, result);
320
- return result;
321
- }
322
- /**
323
- * Delete all cached data.
324
- */
325
- cleanup() {
326
- fs.rmSync(this.config.cacheDir, { recursive: true, force: true });
327
- }
328
- }
329
-
330
- class Config {
331
- cwd = path.resolve(process__default.cwd());
332
- configFile = path.join(os.homedir(), ".cals-config.json");
333
- cacheDir = cachedir("cals-cli");
334
- configCached = undefined;
335
- get config() {
336
- const existingConfig = this.configCached;
337
- if (existingConfig !== undefined) {
338
- return existingConfig;
339
- }
340
- const config = this.readConfig();
341
- this.configCached = config;
342
- return config;
343
- }
344
- readConfig() {
345
- if (!fs.existsSync(this.configFile)) {
346
- return {};
347
- }
348
- try {
349
- return JSON.parse(fs.readFileSync(this.configFile, "utf-8"));
350
- }
351
- catch (e) {
352
- console.error("Failed", e);
353
- throw new Error("Failed to read config");
354
- }
355
- }
356
- getConfig(key) {
357
- return this.config[key];
358
- }
359
- requireConfig(key) {
360
- const result = this.config[key];
361
- if (result === undefined) {
362
- throw Error(`Configuration for ${key} missing. Add manually to ${this.configFile}`);
363
- }
364
- return result;
365
- }
366
- updateConfig(key, value) {
367
- const updatedConfig = {
368
- ...this.readConfig(),
369
- [key]: value, // undefined will remove
370
- };
371
- fs.writeFileSync(this.configFile, JSON.stringify(updatedConfig, null, " "));
372
- this.configCached = updatedConfig;
373
- }
374
- }
375
-
376
- const CLEAR_WHOLE_LINE = 0;
377
- function clearLine(stdout) {
378
- readline.clearLine(stdout, CLEAR_WHOLE_LINE);
379
- readline.cursorTo(stdout, 0);
380
- }
381
- async function readInput(options) {
382
- const rl = readline.createInterface({
383
- input: process__default.stdin,
384
- output: process__default.stdout,
385
- });
386
- if (options.silent) {
387
- rl._writeToOutput = () => { };
388
- }
389
- return new Promise((resolve, reject) => {
390
- const timer = options.timeout
391
- ? setTimeout(() => {
392
- rl.close();
393
- reject(new Error("Input timed out"));
394
- }, options.timeout)
395
- : null;
396
- rl.question(options.prompt, (answer) => {
397
- if (timer)
398
- clearTimeout(timer);
399
- rl.close();
400
- if (options.silent) {
401
- process__default.stdout.write("\n");
402
- }
403
- resolve(answer);
404
- });
405
- });
406
- }
407
- class Reporter {
408
- stdout = process__default.stdout;
409
- stderr = process__default.stderr;
410
- format = chalk;
411
- error(msg) {
412
- clearLine(this.stderr);
413
- this.stderr.write(`${this.format.red("error")} ${msg}\n`);
414
- }
415
- log(msg) {
416
- clearLine(this.stdout);
417
- this.stdout.write(`${msg}\n`);
418
- }
419
- warn(msg) {
420
- clearLine(this.stderr);
421
- this.stderr.write(`${this.format.yellow("warning")} ${msg}\n`);
422
- }
423
- info(msg) {
424
- clearLine(this.stdout);
425
- this.stdout.write(`${this.format.blue("info")} ${msg}\n`);
426
- }
427
- }
428
-
429
- function createReporter() {
430
- return new Reporter();
431
- }
432
- function createCacheProvider(config, argv) {
433
- const cache = new CacheProvider(config);
434
- if (argv.validateCache === true) {
435
- cache.mustValidate = true;
436
- }
437
- return cache;
438
- }
439
- function createConfig() {
440
- return new Config();
441
- }
442
-
443
- async function generateCloneCommands({ reporter, config, github, org, ...opt }) {
444
- if (!opt.listGroups && !opt.all && opt.group === undefined) {
510
+ async function generateCloneCommands({ config, github, org, ...opt }) {
511
+ if (!opt.all && opt.group === undefined) {
445
512
  yargs(hideBin(process__default.argv)).showHelp();
446
513
  return;
447
514
  }
448
515
  const repos = await github.getOrgRepoList({ org });
449
516
  const groups = getGroupedRepos(repos);
450
- if (opt.listGroups) {
451
- groups.forEach((it) => {
452
- reporter.log(it.name);
453
- });
454
- return;
455
- }
456
- groups.forEach((group) => {
517
+ for (const group of groups) {
457
518
  if (opt.group !== undefined && opt.group !== group.name) {
458
- return;
519
+ continue;
459
520
  }
460
521
  group.items
461
522
  .filter((it) => opt.includeArchived || !it.isArchived)
462
523
  .filter((it) => opt.name === undefined || it.name.includes(opt.name))
463
524
  .filter((it) => opt.topic === undefined || includesTopic(it, opt.topic))
464
- .filter((it) => !opt.excludeExisting ||
465
- !fs.existsSync(path.resolve(config.cwd, it.name)))
525
+ .filter((it) => !opt.skipCloned || !fs.existsSync(path.resolve(config.cwd, it.name)))
466
526
  .forEach((repo) => {
467
527
  // The output of this is used to pipe into e.g. bash.
468
- // We cannot use reporter.log as it adds additional characters.
469
528
  process__default.stdout.write(`[ ! -e "${repo.name}" ] && git clone ${repo.sshUrl}\n`);
470
529
  });
471
- });
530
+ }
472
531
  }
473
532
  const command$4 = {
474
- command: "generate-clone-commands",
475
- describe: "Generate shell commands to clone GitHub repos for an organization",
533
+ command: "clone [group]",
534
+ describe: "Generate git clone commands (pipe to bash to execute)",
476
535
  builder: (yargs) => yargs
477
536
  .positional("group", {
478
- describe: "Group to generate commands for",
537
+ describe: "Clone only repos in this group",
538
+ type: "string",
479
539
  })
480
540
  .options("org", {
481
- demandOption: true,
482
- describe: "Specify GitHub organization",
541
+ alias: "o",
542
+ default: "capralifecycle",
543
+ requiresArg: true,
544
+ describe: "GitHub organization",
483
545
  type: "string",
484
546
  })
485
547
  .option("all", {
486
- describe: "Use all groups",
487
- type: "boolean",
488
- })
489
- .option("list-groups", {
490
- alias: "l",
491
- describe: "List available groups",
548
+ describe: "Clone all repos",
492
549
  type: "boolean",
493
550
  })
494
551
  .option("include-archived", {
@@ -499,37 +556,61 @@ const command$4 = {
499
556
  .option("name", {
500
557
  describe: "Filter to include the specified name",
501
558
  type: "string",
559
+ requiresArg: true,
502
560
  })
503
561
  .option("topic", {
504
562
  alias: "t",
505
563
  describe: "Filter by specific topic",
506
564
  type: "string",
565
+ requiresArg: true,
507
566
  })
508
- .option("exclude-existing", {
509
- alias: "x",
510
- describe: "Exclude if existing in working directory",
567
+ .option("skip-cloned", {
568
+ alias: "s",
569
+ describe: "Skip repos already cloned in working directory",
511
570
  type: "boolean",
512
571
  }),
513
572
  handler: async (argv) => {
514
573
  const config = createConfig();
515
574
  return generateCloneCommands({
516
- reporter: createReporter(),
517
575
  config,
518
576
  github: await createGitHubService({
519
577
  cache: createCacheProvider(config, argv),
520
578
  }),
521
579
  all: !!argv.all,
522
- listGroups: !!argv["list-groups"],
523
580
  includeArchived: !!argv["include-archived"],
524
581
  name: argv.name,
525
582
  topic: argv.topic,
526
- excludeExisting: !!argv["exclude-existing"],
583
+ skipCloned: !!argv["skip-cloned"],
527
584
  group: argv.group,
528
585
  org: argv.org,
529
586
  });
530
587
  },
531
588
  };
532
589
 
590
+ const command$3 = {
591
+ command: "groups",
592
+ describe: "List available repository groups in a GitHub organization",
593
+ builder: (yargs) => yargs.options("org", {
594
+ alias: "o",
595
+ default: "capralifecycle",
596
+ requiresArg: true,
597
+ describe: "GitHub organization",
598
+ type: "string",
599
+ }),
600
+ handler: async (argv) => {
601
+ const config = createConfig();
602
+ const reporter = createReporter();
603
+ const github = await createGitHubService({
604
+ cache: createCacheProvider(config, argv),
605
+ });
606
+ const repos = await github.getOrgRepoList({ org: argv.org });
607
+ const groups = getGroupedRepos(repos);
608
+ for (const group of groups) {
609
+ reporter.log(group.name);
610
+ }
611
+ },
612
+ };
613
+
533
614
  function getReposMissingGroup(repos) {
534
615
  return repos.filter((it) => getGroup(it) === null);
535
616
  }
@@ -612,13 +693,15 @@ async function listRepos({ reporter, github, includeArchived, name = undefined,
612
693
  });
613
694
  }
614
695
  }
615
- const command$3 = {
616
- command: "list-repos",
617
- describe: "List Git repos for a GitHub organization",
696
+ const command$2 = {
697
+ command: "repos",
698
+ describe: "List repositories in a GitHub organization",
618
699
  builder: (yargs) => yargs
619
700
  .options("org", {
620
- required: true,
621
- describe: "Specify GitHub organization",
701
+ alias: "o",
702
+ default: "capralifecycle",
703
+ requiresArg: true,
704
+ describe: "GitHub organization",
622
705
  type: "string",
623
706
  })
624
707
  .option("include-archived", {
@@ -638,11 +721,13 @@ const command$3 = {
638
721
  .option("name", {
639
722
  describe: "Filter to include the specified name",
640
723
  type: "string",
724
+ requiresArg: true,
641
725
  })
642
726
  .option("topic", {
643
727
  alias: "t",
644
728
  describe: "Filter by specific topic",
645
729
  type: "string",
730
+ requiresArg: true,
646
731
  }),
647
732
  handler: async (argv) => {
648
733
  const config = createConfig();
@@ -661,34 +746,6 @@ const command$3 = {
661
746
  },
662
747
  };
663
748
 
664
- async function setToken({ reporter, token, tokenProvider, }) {
665
- if (token === undefined) {
666
- reporter.info("Need API token to talk to GitHub");
667
- reporter.info("https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook");
668
- const inputToken = await readInput({
669
- prompt: "Enter new GitHub API token: ",
670
- silent: true,
671
- });
672
- token = inputToken;
673
- }
674
- await tokenProvider.setToken(token);
675
- reporter.info("Token saved");
676
- }
677
- const command$2 = {
678
- command: "set-token",
679
- describe: "Set GitHub token for API calls",
680
- builder: (yargs) => yargs.positional("token", {
681
- describe: "Token. If not provided it will be requested as input. Can be generated at https://github.com/settings/tokens/new?scopes=repo:status,read:repo_hook",
682
- }),
683
- handler: async (argv) => {
684
- await setToken({
685
- reporter: createReporter(),
686
- token: argv.token,
687
- tokenProvider: new GitHubTokenCliProvider(),
688
- });
689
- },
690
- };
691
-
692
749
  var type = "object";
693
750
  var properties = {
694
751
  projects: {
@@ -1200,7 +1257,7 @@ async function askMoveConfirm() {
1200
1257
  return false;
1201
1258
  }
1202
1259
  }
1203
- async function sync({ reporter, github, cals, rootdir, askClone, askMove, }) {
1260
+ async function sync({ reporter, github, cals, rootdir, clone, move, }) {
1204
1261
  const { expectedRepos, definitionRepo } = await getExpectedRepos(reporter, github, cals, rootdir);
1205
1262
  const unknownDirs = [];
1206
1263
  const foundRepos = [];
@@ -1260,8 +1317,8 @@ async function sync({ reporter, github, cals, rootdir, askClone, askMove, }) {
1260
1317
  for (const it of movedRepos) {
1261
1318
  reporter.info(` ${it.actualRelpath} -> ${getRelpath(it)}`);
1262
1319
  }
1263
- if (!askMove) {
1264
- reporter.info("To move these repos on disk add --ask-move option");
1320
+ if (!move) {
1321
+ reporter.info("To move these repos on disk add --move option");
1265
1322
  }
1266
1323
  else {
1267
1324
  const shouldMove = await askMoveConfirm();
@@ -1293,8 +1350,8 @@ async function sync({ reporter, github, cals, rootdir, askClone, askMove, }) {
1293
1350
  for (const it of missingRepos) {
1294
1351
  reporter.info(` ${it.id}`);
1295
1352
  }
1296
- if (!askClone) {
1297
- reporter.info("To clone these repos add --ask-clone option for dialog");
1353
+ if (!clone) {
1354
+ reporter.info("To clone these repos add --clone option");
1298
1355
  }
1299
1356
  else {
1300
1357
  reporter.info("You must already have working credentials for GitHub set up for clone to work");
@@ -1342,16 +1399,17 @@ const command$1 = {
1342
1399
  command: "sync",
1343
1400
  describe: "Sync repositories for working directory",
1344
1401
  builder: (yargs) => yargs
1345
- .option("ask-clone", {
1402
+ .option("clone", {
1346
1403
  alias: "c",
1347
- describe: "Ask to clone new missing repos",
1404
+ describe: "Prompt to clone missing repos",
1348
1405
  type: "boolean",
1349
1406
  })
1350
- .option("ask-move", {
1351
- describe: "Ask to actual move renamed repos",
1407
+ .option("move", {
1408
+ alias: "m",
1409
+ describe: "Prompt to move renamed repos",
1352
1410
  type: "boolean",
1353
1411
  })
1354
- .usage(`cals github sync
1412
+ .usage(`cals sync
1355
1413
 
1356
1414
  Synchronize all checked out GitHub repositories within the working directory
1357
1415
  grouped by the project in the resource definition file. The command can also
@@ -1400,39 +1458,42 @@ will be stored there.`),
1400
1458
  github,
1401
1459
  cals,
1402
1460
  rootdir: dir,
1403
- askClone: !!argv["ask-clone"],
1404
- askMove: !!argv["ask-move"],
1461
+ clone: !!argv.clone,
1462
+ move: !!argv.move,
1405
1463
  });
1406
1464
  },
1407
1465
  };
1408
1466
 
1409
1467
  const command = {
1410
- command: "github",
1411
- describe: "Integration with GitHub",
1412
- builder: (yargs) => yargs
1413
- .command(command$4)
1414
- .command(command$3)
1415
- .command(command$2)
1416
- .command(command$1)
1417
- .demandCommand()
1418
- .usage(`cals github
1419
-
1420
- Notes:
1421
- Before doing anything against GitHub you need to configure a token
1422
- used for authentication. The following command will ask for a token
1423
- and provide a link to generate one:
1424
- $ cals github set-token
1425
-
1426
- Quick clone all repos:
1427
- $ cals github generate-clone-commands --org capralifecycle --all -x | bash
1428
-
1429
- And for a specific project:
1430
- $ cals github generate-clone-commands --org capralifecycle -x buildtools | bash
1431
-
1432
- Some responses are cached for some time. Use the --validate-cache
1433
- option to avoid stale cache.`),
1434
- handler: () => {
1435
- yargs(hideBin(process__default.argv)).showHelp();
1468
+ command: "topics",
1469
+ describe: "List customer topics in a GitHub organization",
1470
+ builder: (yargs) => yargs.options("org", {
1471
+ alias: "o",
1472
+ default: "capralifecycle",
1473
+ requiresArg: true,
1474
+ describe: "GitHub organization",
1475
+ type: "string",
1476
+ }),
1477
+ handler: async (argv) => {
1478
+ const config = createConfig();
1479
+ const reporter = createReporter();
1480
+ const github = await createGitHubService({
1481
+ cache: createCacheProvider(config, argv),
1482
+ });
1483
+ const repos = await github.getOrgRepoList({ org: argv.org });
1484
+ const topics = new Set();
1485
+ for (const repo of repos) {
1486
+ for (const edge of repo.repositoryTopics.edges) {
1487
+ const name = edge.node.topic.name;
1488
+ if (name.startsWith("customer-")) {
1489
+ topics.add(name);
1490
+ }
1491
+ }
1492
+ }
1493
+ const sorted = [...topics].sort((a, b) => a.localeCompare(b));
1494
+ for (const topic of sorted) {
1495
+ reporter.log(topic);
1496
+ }
1436
1497
  },
1437
1498
  };
1438
1499
 
@@ -1459,17 +1520,35 @@ async function main() {
1459
1520
  process__default.exit(1);
1460
1521
  }
1461
1522
  await yargs(hideBin(process__default.argv))
1462
- .usage(`cals-cli v${version} (build: ${"2026-01-30T14:13:10.419Z"})`)
1523
+ .usage(`cals v${version} (build: ${"2026-01-30T15:20:39.822Z"})
1524
+
1525
+ A CLI for managing GitHub repositories.
1526
+
1527
+ Before using, authenticate with: cals auth`)
1463
1528
  .scriptName("cals")
1464
1529
  .locale("en")
1530
+ .strict()
1531
+ .strictCommands()
1532
+ .strictOptions()
1465
1533
  .help("help")
1534
+ .command(command$5)
1535
+ .command(command$4)
1536
+ .command(command$3)
1537
+ .command(command$2)
1538
+ .command(command$1)
1466
1539
  .command(command)
1467
1540
  .version(version)
1468
1541
  .demandCommand()
1469
- .option("validate-cache", {
1470
- describe: "Only read from cache if validated against server",
1542
+ .option("no-cache", {
1543
+ describe: "Bypass cache and fetch fresh data",
1471
1544
  type: "boolean",
1472
1545
  })
1546
+ .example("cals auth", "Set GitHub token")
1547
+ .example("cals repos", "List repositories")
1548
+ .example("cals groups", "List repository groups")
1549
+ .example("cals topics", "List customer topics")
1550
+ .example("cals clone --all | bash", "Clone all repos")
1551
+ .example("cals sync", "Pull latest changes")
1473
1552
  .parse();
1474
1553
  }
1475
1554
 
@@ -1 +1 @@
1
- {"version":3,"file":"cals-cli.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"cals-cli.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,3 +1,3 @@
1
- import { type CommandModule } from "yargs";
1
+ import type { CommandModule } from "yargs";
2
2
  declare const command: CommandModule;
3
3
  export default command;
@@ -1,5 +1,5 @@
1
1
  import type { CommandModule } from "yargs";
2
- import type { DefinitionRepo } from "../../../definition/types";
2
+ import type { DefinitionRepo } from "../../definition/types";
3
3
  interface Alias {
4
4
  group: string;
5
5
  name: string;
@@ -0,0 +1,3 @@
1
+ import type { CommandModule } from "yargs";
2
+ declare const command: CommandModule;
3
+ export default command;
package/lib/index.es.js CHANGED
@@ -9,7 +9,7 @@ import { Octokit } from '@octokit/rest';
9
9
  import pLimit from 'p-limit';
10
10
  import keytar from 'keytar';
11
11
 
12
- var version = "3.14.0";
12
+ var version = "3.15.0";
13
13
 
14
14
  function uniq(array) {
15
15
  return Array.from(new Set(array));
package/lib/index.js CHANGED
@@ -9,7 +9,7 @@ import { Octokit } from '@octokit/rest';
9
9
  import pLimit from 'p-limit';
10
10
  import keytar from 'keytar';
11
11
 
12
- var version = "3.14.0";
12
+ var version = "3.15.0";
13
13
 
14
14
  function uniq(array) {
15
15
  return Array.from(new Set(array));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capraconsulting/cals-cli",
3
- "version": "3.14.0",
3
+ "version": "3.15.0",
4
4
  "description": "CLI for repeatable tasks in CALS",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
File without changes