@leverege/build-tools 2.102.0 → 2.103.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.
@@ -58,6 +58,16 @@
58
58
  "runtimeExecutable": "node",
59
59
  "console": "integratedTerminal"
60
60
  },
61
+ {
62
+ "name": "chart-to-registry rsrc-srvr",
63
+ "type": "node",
64
+ "request": "launch",
65
+ "program": "${workspaceFolder}/src/chart-to-registry.mjs",
66
+ "args": [],
67
+ "cwd": "/Users/john/Work/leverege/cicd/@plt/resource-server",
68
+ "runtimeExecutable": "node",
69
+ "console": "integratedTerminal"
70
+ },
61
71
  {
62
72
  "name": "ingestor in monorepo",
63
73
  "type": "node",
package/README.md CHANGED
@@ -16,7 +16,7 @@ Most tools assume the following are installed and configured:
16
16
  - `kubectl` (with a valid cluster context)
17
17
  - `helm` (v3+)
18
18
  - `fzf` (for interactive pod selection in `k8x` and `klog`)
19
- - Node.js 22+
19
+ - Node.js 22+ - prefer 24
20
20
 
21
21
  ---
22
22
 
@@ -85,13 +85,15 @@ init-my-chart [--dry-run] [--debug]
85
85
  ```
86
86
 
87
87
  **`chart-to-registry`** — Pushes Helm charts to GCP Artifact Registry with version
88
- checking and dependency resolution.
88
+ checking and dependency resolution. Warns if `appVersion` in Chart.yaml does not start
89
+ with `v` (bypass with `--ignore-appv`).
89
90
  ```bash
90
- chart-to-registry --chart <dir> [--project <id>] [--repository <name>] [--force] [--dry-run]
91
+ chart-to-registry --chart <dir> [--project <id>] [--repository <name>] [--force] [--dry-run] [--ignore-appv]
91
92
  ```
92
93
 
93
94
  **`push-my-chart`** — Publishes Helm charts to both GCS Chartmuseum (legacy) and
94
- GCP Artifact Registry.
95
+ GCP Artifact Registry. Displays chart name, version, and appVersion before pushing;
96
+ warns if `appVersion` does not start with `v`.
95
97
  ```bash
96
98
  push-my-chart [--chart <dir>] [--no-registry]
97
99
  ```
@@ -226,11 +228,20 @@ Manager, updating `.npmrc` / `.yarnrc.yml`. Skips refresh if the token is less t
226
228
  refresh-npm-token [--project <id>] [--force] [--dry-run]
227
229
  ```
228
230
 
229
- **`pkgck`** — Checks for common package configuration issues (babel, eslint, circleci,
230
- dockerfile, etc.) with visual pass/fail indicators.
231
+ **`repo-panopticon`** — Multi-repo health dashboard. Checks each repository against a suite
232
+ of indicators in a compact 80-column table. Monorepo-aware: workspace packages are
233
+ auto-discovered, grouped under their parent repo label, and checked individually.
231
234
  ```bash
232
- pkgck
235
+ repo-panopticon <dirs...> # check one or more repos / globs
236
+ repo-panopticon @plt/* @kud/* @srv/* # typical full-org sweep
237
+ repo-panopticon --no-pull @plt/* # skip git pull/fetch
238
+ repo-panopticon --limit 40 @srv/* # cap displayed rows (default 80)
239
+ repo-panopticon --concurrency 10 @srv/* # increase parallelism (default 5)
233
240
  ```
241
+ Columns: `mgr` (npm/yrn) · `pkg` (package.json version) · `git` (latest semver tag) ·
242
+ `scop` (package scope) · `cir` (CircleCI — ✘ missing · L legacy · 1 orb v1 · 2 orb v2) ·
243
+ `ESM` (`"type":"module"`) · `har` (.har hooks directory) · `lnt` (ESLint major version) ·
244
+ `bbl` (Babel presence). Repo name shown in orange when dirty or sync failed.
234
245
 
235
246
  **`pkglint`** — Verifies required documentation files (README.md, LICENSE.md) are
236
247
  present before publishing.
@@ -252,8 +263,9 @@ count-lines-of-code [--src-only]
252
263
 
253
264
  ### CI/CD Integration
254
265
 
255
- **`circle-orb-it`** — Installs or updates CircleCI configuration (Leverege orb) and
256
- GitHub Dependabot settings for a repository. Preserves existing customizations.
266
+ **`circle-orb-it`** — Installs or updates CircleCI configuration (Leverege orb v2.0.0) and
267
+ GitHub Dependabot settings for a repository. Auto-detects npm vs yarn-berry and writes
268
+ the appropriate template. Preserves existing customizations.
257
269
  ```bash
258
270
  circle-orb-it [--dry-run] [--debug]
259
271
  ```
@@ -340,6 +352,7 @@ BUILD_TOOLS_DEBUG=1 docker-to-registry
340
352
 
341
353
  | Tool | Replacement |
342
354
  |------|-------------|
355
+ | `pkgck` | `repo-panopticon` |
343
356
  | `docker-to-registry-py` | `docker-to-registry` |
344
357
  | `chart-to-museum` | `push-my-chart` |
345
358
  | `refresh-py-idx` | No longer needed |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.102.0",
3
+ "version": "2.103.0",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -16,7 +16,6 @@
16
16
  "prepack": "npm exec prepack"
17
17
  },
18
18
  "bin": {
19
- "build-and-push": "src/build-and-push.sh",
20
19
  "build-cnpg-image": "src/build-cnpg-image.mjs",
21
20
  "build-tools": "src/build-tools.mjs",
22
21
  "change-up": "src/change-up.sh",
@@ -29,8 +28,6 @@
29
28
  "decrypt-secrets": "src/decrypt-secrets.sh",
30
29
  "dirty-git": "src/dirty-git.sh",
31
30
  "docker-to-registry": "src/docker-to-registry.mjs",
32
- "docker-to-registry-py": "src/docker-to-registry-py.mjs",
33
- "docker-to-registry.sh": "src/docker-to-registry.sh",
34
31
  "dockreate": "src/dockreate.sh",
35
32
  "encrypt-secrets": "src/encrypt-secrets.sh",
36
33
  "firebaseDeploy": "src/firebaseDeploy.mjs",
@@ -55,7 +52,7 @@
55
52
  "npm-link": "src/npm-link.sh",
56
53
  "npm-unlink": "src/npm-unlink.sh",
57
54
  "overwhelm": "src/overwhelm.mjs",
58
- "pkgck": "src/pkgck.sh",
55
+ "pkgck": "echo '*** Use repo-panopticon instead! ***",
59
56
  "pkglint": "src/pkglint.sh",
60
57
  "prepack": "src/prepack.mjs",
61
58
  "prune-git": "src/prune-git.sh",
@@ -65,10 +62,10 @@
65
62
  "push-my-chart": "src/push-my-chart.mjs",
66
63
  "refresh-npm-token": "src/refresh-npm-token.mjs",
67
64
  "refresh-py-idx": "src/refresh-py-idx.sh",
65
+ "repo-panopticon": "src/repo-panopticon.mjs",
68
66
  "send-to-slack": "src/send-to-slack.mjs",
69
67
  "service-man": "src/service-man/service-man.mjs",
70
68
  "snapshot-cnpg": "src/snapshot-cnpg.mjs",
71
- "tag-release": "src/tag-release.mjs",
72
69
  "unleash": "src/unleash.mjs",
73
70
  "upload-model": "src/upload-model.sh",
74
71
  "yarn-build": "src/yarn-build.mjs"
@@ -109,7 +106,7 @@
109
106
  "sloc": "^0.3.2",
110
107
  "superstruct": "^2.0.2",
111
108
  "tmp": "^0.2.5",
112
- "toml": "^3.0.0",
109
+ "toml": "^4.1.1",
113
110
  "yaml": "^2.9.0",
114
111
  "zx": "^8.8.5"
115
112
  },
package/src/Utils.mjs CHANGED
@@ -534,6 +534,7 @@ export const parseHelmChart = async ( helmroot = './helm' ) => {
534
534
  const chartName = chartYaml.name
535
535
  const chartLibrary = chartYaml?.type === 'library'
536
536
  const chartVersion = chartYaml.version
537
+ const chartAppVersion = chartYaml.appVersion
537
538
  const chartPackage = `${chartName}-${chartVersion}.tgz`
538
539
  const valuesText = fs.readFileSync( `${helmroot}/values.yaml`, 'utf8' )
539
540
  const valuesYaml = YAML.load( valuesText )
@@ -573,6 +574,7 @@ export const parseHelmChart = async ( helmroot = './helm' ) => {
573
574
  chartLibrary,
574
575
  chartPackage,
575
576
  chartVersion,
577
+ chartAppVersion,
576
578
  hasDependencies,
577
579
  depsOutOfSync,
578
580
  imageRegistry,
@@ -14,9 +14,10 @@ import {
14
14
  errorExit,
15
15
  establishOauthAccess,
16
16
  log,
17
- warning,
18
17
  parseHelmChart,
18
+ proceed,
19
19
  shellCmd,
20
+ warning,
20
21
  } from './Utils.mjs'
21
22
 
22
23
  const commandLineOptions = [ // Use commandLineOptions to tie into the Usage statements
@@ -46,6 +47,11 @@ const commandLineOptions = [ // Use commandLineOptions to tie into the Usage sta
46
47
  type : Boolean,
47
48
  description : '{green update artifact registry auth token and exit}',
48
49
  },
50
+ {
51
+ name : 'ignore-appv',
52
+ type : Boolean,
53
+ description : '{green ignore the Char.yaml appVersion format}',
54
+ },
49
55
  {
50
56
  name : 'dry-run',
51
57
  type : Boolean,
@@ -98,6 +104,11 @@ if ( semverLt( process.version, minNodejsVersion ) ) {
98
104
  // load up all we need to know about the chart
99
105
  const helmChart = await parseHelmChart( args.chart )
100
106
 
107
+ if ( !helmChart.chartAppVersion.startsWith( 'v' ) && !args.ignoreAppv ) {
108
+ warning( 'Chart.yaml appVersion does not begin with the letter "v"\n' )
109
+ await proceed( 'You sure about this?' )
110
+ }
111
+
101
112
  // helm push helm-package.tgz oci://REGISTRY_LOCATION/REGISTRY_PROJECT/REGISTRY_REPOSITORY/charts
102
113
  //
103
114
  const registryLocation = args.location || helmChart.registryLocation
@@ -1,25 +1,28 @@
1
1
  version: 2.1
2
2
 
3
3
  orbs:
4
- leverege: leverege/circle-nodejs-ci@1.2.6
4
+ leverege: leverege/circle-nodejs-ci@2.0.0
5
5
 
6
6
  workflows:
7
7
  build-and-verify:
8
8
  jobs:
9
9
  - leverege/install-dependencies:
10
10
  context: npm
11
- # pkg-manager: yarn-berry # uncomment for Yarn Berry repos
12
11
  - leverege/code-analysis:
12
+ context: npm
13
13
  requires:
14
14
  - leverege/install-dependencies
15
15
  - leverege/code-coverage:
16
+ context:
17
+ - npm
18
+ - leverege-gcp # provides GCP_SA_KEY_JSON for tests that need GCP access
16
19
  coverage-threshold: 70 # percentage threshold for functions, lines, and statements
17
- context: leverege-gcp # provides GCP_SA_KEY_JSON for tests that need GCP access
18
20
  enable-redis: false # set to true if tests require a live Redis connection
19
21
  cache-config: '{"type":"inMemory"}' # sets CACHE_CONFIG env var - options: inMemory, redis
20
22
  requires:
21
23
  - leverege/install-dependencies
22
24
  - leverege/generate-docs:
25
+ context: npm
23
26
  requires:
24
27
  - leverege/install-dependencies
25
28
  - leverege/mirror-to-github:
@@ -11,14 +11,19 @@ import {
11
11
  enableDebug,
12
12
  errorExit,
13
13
  getGitRootDirectory,
14
+ getIsYarnProject,
14
15
  log,
16
+ mkdirSafe,
15
17
  proceed,
18
+ warning,
16
19
  } from '../Utils.mjs'
17
20
 
18
- const ORB_VERSION = '1.2.6' // Keep this in sync with the version in circle-orb.yml
21
+ const ORB_VERSION = '2.0.0' // Keep this in sync with the versions in circle-npm.yml and circle-yarn.yml
19
22
  const CIRCLE_DIR = '.circleci'
20
23
  const CIRCLE_YML = `${CIRCLE_DIR}/config.yml`
21
- const CIRCLE_YML_TEMPLATE = path.join( BUILD_TOOLS_ROOT, 'circle-orb-it', 'circle-orb.yml' )
24
+ const CIRCLE_NPM_TEMPLATE = path.join( BUILD_TOOLS_ROOT, 'circle-orb-it', 'circle-npm.yml' )
25
+ const CIRCLE_YARN_TEMPLATE = path.join( BUILD_TOOLS_ROOT, 'circle-orb-it', 'circle-yarn.yml' )
26
+ const CIRCLE_YML_TEMPLATE = isYarn => ( isYarn ? CIRCLE_YARN_TEMPLATE : CIRCLE_NPM_TEMPLATE )
22
27
  const GITHUB_DIR = '.github'
23
28
  const DEPENDABOT_FILE = `${GITHUB_DIR}/dependabot.yml`
24
29
  const DEPENDABOT_TEMPLATE = path.join( BUILD_TOOLS_ROOT, 'circle-orb-it', 'dependabot.yml' )
@@ -52,9 +57,13 @@ if ( !gitRoot ) {
52
57
  errorExit( 'This command must be run inside a git repository' )
53
58
  }
54
59
 
60
+ const isYarn = getIsYarnProject( gitRoot )
61
+ const circleTemplate = CIRCLE_YML_TEMPLATE( isYarn )
62
+ log( chalk.blue( `\nPackage manager: ${isYarn ? 'yarn-berry' : 'npm'}` ) )
63
+
55
64
  // Warn if this looks like a Python project since the orb is Node.js only
56
65
  if ( fs.existsSync( path.join( gitRoot, 'pyproject.toml' ) ) ) {
57
- log( chalk.yellow( '\n***WARNING: pyproject.toml detected - the leverege orb is Node.js only' ) )
66
+ warning( 'pyproject.toml detected - the leverege orb is Node.js only' )
58
67
  await proceed( 'Continue anyway?' )
59
68
  }
60
69
 
@@ -119,7 +128,7 @@ if ( !writeCircleYml && !writeDependabot ) {
119
128
 
120
129
  if ( options.dryRun ) {
121
130
  if ( writeCircleYml ) {
122
- let dryRunContent = fs.readFileSync( CIRCLE_YML_TEMPLATE, 'utf-8' )
131
+ let dryRunContent = fs.readFileSync( circleTemplate, 'utf-8' )
123
132
  if ( redisEnabled ) {
124
133
  dryRunContent = dryRunContent
125
134
  .replace( 'enable-redis: false', 'enable-redis: true' )
@@ -142,10 +151,8 @@ if ( options.dryRun ) {
142
151
  // ---------- Write ----------
143
152
 
144
153
  if ( writeCircleYml ) {
145
- if ( !fs.existsSync( circleDir ) ) {
146
- fs.mkdirSync( circleDir, { recursive : true } )
147
- }
148
- let circleYmlContent = fs.readFileSync( CIRCLE_YML_TEMPLATE, 'utf-8' )
154
+ mkdirSafe( circleDir )
155
+ let circleYmlContent = fs.readFileSync( circleTemplate, 'utf-8' )
149
156
  if ( redisEnabled ) {
150
157
  circleYmlContent = circleYmlContent
151
158
  .replace( 'enable-redis: false', 'enable-redis: true' )
@@ -162,9 +169,7 @@ if ( writeCircleYml ) {
162
169
  }
163
170
 
164
171
  if ( writeDependabot ) {
165
- if ( !fs.existsSync( githubDir ) ) {
166
- fs.mkdirSync( githubDir, { recursive : true } )
167
- }
172
+ mkdirSafe( githubDir )
168
173
  fs.writeFileSync( dependabotPath, fs.readFileSync( DEPENDABOT_TEMPLATE, 'utf-8' ) )
169
174
  log( chalk.green( `Dependabot config written => ${dependabotPath}` ) )
170
175
  }
@@ -0,0 +1,25 @@
1
+ version: 2.1
2
+
3
+ orbs:
4
+ leverege: leverege/circle-nodejs-ci@2.0.0
5
+
6
+ workflows:
7
+ build-and-verify:
8
+ jobs:
9
+ - leverege/code-analysis:
10
+ context: npm
11
+ pkg-manager: yarn-berry
12
+ - leverege/code-coverage:
13
+ context:
14
+ - npm
15
+ - leverege-gcp # provides GCP_SA_KEY_JSON for tests that need GCP access
16
+ pkg-manager: yarn-berry
17
+ coverage-threshold: 70 # percentage threshold for functions, lines, and statements
18
+ enable-redis: false # set to true if tests require a live Redis connection
19
+ cache-config: '{"type":"inMemory"}' # sets CACHE_CONFIG env var - options: inMemory, redis
20
+ - leverege/generate-docs:
21
+ context: npm
22
+ pkg-manager: yarn-berry
23
+ - leverege/mirror-to-github:
24
+ context: github-mirror
25
+ # github-team: platform # available teams: platform, utilities, pitcrew, recovr
@@ -9,7 +9,9 @@ import {
9
9
  errorExit,
10
10
  log,
11
11
  parseHelmChart,
12
- shellCmd
12
+ proceed,
13
+ shellCmd,
14
+ warning,
13
15
  } from './Utils.mjs'
14
16
 
15
17
  const commandLineOptions = [
@@ -56,11 +58,18 @@ if ( args.help ) {
56
58
  const {
57
59
  chartName,
58
60
  chartVersion,
61
+ chartAppVersion,
59
62
  depsOutOfSync,
60
63
  } = await parseHelmChart( args.chart )
61
64
 
62
- log( chalk.green( `Chart => ${chalk.bold.yellow( chartName )}` ) )
63
- log( chalk.green( `Version => ${chalk.bold.yellow( chartVersion )}` ) )
65
+ log( chalk.green( `Chart => ${chalk.bold.yellow( chartName )}` ) )
66
+ log( chalk.green( `Version => ${chalk.bold.yellow( chartVersion )}` ) )
67
+ log( chalk.green( `AppVersion => ${chalk.bold.yellow( chartAppVersion )}` ) )
68
+
69
+ if ( !chartAppVersion.startsWith( 'v' ) ) {
70
+ warning( 'Chart.yaml appVersion does not begin with the letter "v"\n' )
71
+ await proceed( 'You sure about this?' )
72
+ }
64
73
 
65
74
  if ( depsOutOfSync ) {
66
75
  try {
@@ -89,7 +98,7 @@ if ( args.noRegistry ) {
89
98
 
90
99
  try {
91
100
  log( chalk.green( ' ... pushing to the Artifact Registry' ) )
92
- const registry = await shellCmd( `chart-to-registry --chart ${args.chart}` )
101
+ const registry = await shellCmd( `chart-to-registry --chart ${args.chart} --ignore-appv` )
93
102
  debug( { registry }, '<==chart-to-registry' )
94
103
  process.exit( museumStatus )
95
104
  } catch ( ex ) {
@@ -0,0 +1,415 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable security/detect-non-literal-fs-filename, no-multi-spaces, key-spacing */
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+
6
+ import { program } from 'commander'
7
+ import chalk from 'chalk'
8
+ import { glob } from 'glob'
9
+ import ora from 'ora'
10
+ import pLimit from 'p-limit'
11
+ import simpleGit from 'simple-git'
12
+
13
+ import { clear, enableDebug, log, parseJsonFile } from './Utils.mjs'
14
+
15
+ // ---------- Symbols ----------
16
+
17
+ const SYM_GOOD = chalk.green( '✔' )
18
+ const SYM_BAD = chalk.red( '✘' )
19
+
20
+ // ---------- Checks ----------
21
+ // Each check: { label: string, fn: ({ dir, pkg, isWorkspace }) => { value: string } }
22
+ // Add new checks here — the table expands automatically.
23
+
24
+ const CHECKS = [
25
+ {
26
+ label : 'cir',
27
+ fn : ( { dir } ) => {
28
+ const cfgPath = path.join( dir, '.circleci', 'config.yml' )
29
+ if ( !fs.existsSync( cfgPath ) ) return { value : SYM_BAD }
30
+ try {
31
+ const content = fs.readFileSync( cfgPath, 'utf8' )
32
+ const match = content.match( /leverege\/circle-nodejs-ci@(\d+)\.\d+\.\d+/ )
33
+ if ( !match || parseInt( match[1], 10 ) === 0 ) return { value : chalk.hex( '#FFA500' )( 'L' ) }
34
+ const major = parseInt( match[1], 10 )
35
+ return { value : major === 1 ? chalk.yellow( '1' ) : chalk.green( `${major}` ) }
36
+ } catch {
37
+ return { value : chalk.hex( '#FFA500' )( 'L' ) }
38
+ }
39
+ },
40
+ },
41
+ {
42
+ label : 'ESM',
43
+ fn : ( { pkg } ) => {
44
+ const ok = pkg?.type === 'module'
45
+ return { value : ok ? SYM_GOOD : SYM_BAD }
46
+ },
47
+ },
48
+ {
49
+ label : 'har',
50
+ fn : ( { gitRoot } ) => {
51
+ const ok = fs.existsSync( path.join( gitRoot, '.har' ) )
52
+ return { value : ok ? SYM_GOOD : SYM_BAD }
53
+ },
54
+ },
55
+ {
56
+ label : 'lnt',
57
+ fn : ( { dir, pkg, gitRoot } ) => {
58
+ if ( !pkg?.scripts?.lint ) return { value : SYM_BAD }
59
+ try {
60
+ // Prefer package-local node_modules; fall back to monorepo root (hoisted)
61
+ const local = path.join( dir, 'node_modules', 'eslint', 'package.json' )
62
+ const hoisted = path.join( gitRoot, 'node_modules', 'eslint', 'package.json' )
63
+ const eslintPkg = JSON.parse( fs.readFileSync(
64
+ fs.existsSync( local ) ? local : hoisted, 'utf8'
65
+ ) )
66
+ const major = parseInt( eslintPkg.version.split( '.' )[0], 10 )
67
+ return { value : major <= 8 ? chalk.yellow( `${major}` ) : chalk.green( `${major}` ) }
68
+ } catch {
69
+ return { value : chalk.dim( '?' ) }
70
+ }
71
+ },
72
+ },
73
+ {
74
+ label : 'bbl',
75
+ fn : ( { dir, pkg } ) => {
76
+ const configFiles = [ '.babelrc', '.babelrc.js', '.babelrc.json', 'babel.config.js', 'babel.config.json', 'babel.config.cjs' ]
77
+ const hasConfig = configFiles.some( f => fs.existsSync( path.join( dir, f ) ) )
78
+ const allDeps = { ...( pkg?.dependencies ?? {} ), ...( pkg?.devDependencies ?? {} ) }
79
+ const hasDep = Object.keys( allDeps ).some( k => k.includes( 'babel' ) )
80
+ return { value : ( hasConfig || hasDep ) ? SYM_BAD : chalk.dim( '–' ) }
81
+ },
82
+ },
83
+ ]
84
+
85
+ // ---------- Helpers ----------
86
+
87
+ const ANSI_RE = /\x1B\[[0-9;]*m/g // eslint-disable-line no-control-regex
88
+
89
+ function visLen( str ) {
90
+ return str.replace( ANSI_RE, '' ).length
91
+ }
92
+
93
+ function pad( str, width ) {
94
+ return str + ' '.repeat( Math.max( 0, width - visLen( str ) ) )
95
+ }
96
+
97
+ function center( str, width ) {
98
+ const gap = Math.max( 0, width - visLen( str ) )
99
+ const left = Math.floor( gap / 2 )
100
+ return ' '.repeat( left ) + str + ' '.repeat( gap - left )
101
+ }
102
+
103
+ const NONSEMVER = '!SEMVER'
104
+
105
+ // Strict x.y.z only — pre-release and build metadata are flagged !SEMVER
106
+ function parseVersion( raw ) {
107
+ if ( !raw ) return null
108
+ const v = raw.startsWith( 'v' ) ? raw : `v${raw}`
109
+ return /^v\d+\.\d+\.\d+$/.test( v ) ? v : NONSEMVER
110
+ }
111
+
112
+ function fmtPkgVer( ver ) {
113
+ if ( !ver ) return chalk.dim( '?' )
114
+ if ( ver === NONSEMVER ) return chalk.yellow( NONSEMVER )
115
+ return chalk.green( ver )
116
+ }
117
+
118
+ function fmtGitVer( gitVer, pkgVer ) {
119
+ if ( !gitVer ) return chalk.bgRed.white( 'UNTAGGD' )
120
+ if ( gitVer === NONSEMVER ) return chalk.yellow( NONSEMVER )
121
+ if ( gitVer === pkgVer ) return chalk.green( gitVer )
122
+ return chalk.yellow( gitVer )
123
+ }
124
+
125
+ function fmtScope( pkg ) {
126
+ const name = pkg?.name ?? ''
127
+ const match = name.match( /^(@[^/]+)/ )
128
+ if ( match ) return chalk.green( match[1].slice( 0, 4 ) ) // '@' + 3 chars: @leverege → @lev
129
+ if ( pkg?.private === true ) return chalk.yellow( 'PRVT' )
130
+ return chalk.bgRed.white( 'PUBL' )
131
+ }
132
+
133
+ // Extracts { repoName, group } from a directory path.
134
+ // For workspace packages (gitRoot !== dir), group is the monorepo's org/name path
135
+ // and repoName is the package's own directory name.
136
+ // For root repos, group is the @xxx path component and repoName is relative to it.
137
+ function repoLabel( dir, gitRoot ) {
138
+ const parts = dir.split( path.sep )
139
+ const atIdx = parts.findIndex( p => /^@\w+$/.test( p ) )
140
+
141
+ if ( gitRoot && gitRoot !== dir ) {
142
+ // Workspace package — group under the monorepo's label
143
+ const rootParts = gitRoot.split( path.sep )
144
+ const rootAtIdx = rootParts.findIndex( p => /^@\w+$/.test( p ) )
145
+ const group = rootAtIdx === -1 ?
146
+ path.basename( gitRoot ) :
147
+ rootParts.slice( rootAtIdx ).join( '/' )
148
+ return { repoName : path.basename( dir ), group }
149
+ }
150
+
151
+ if ( atIdx === -1 ) return { repoName : path.basename( dir ), group : null }
152
+ const group = parts[atIdx]
153
+ const repoName = parts.slice( atIdx + 1 ).join( '/' )
154
+ return { repoName, group }
155
+ }
156
+
157
+ // ---------- Per-repo ----------
158
+
159
+ async function checkRepo( dir, { pull } ) {
160
+ if ( !fs.existsSync( path.join( dir, 'package.json' ) ) ) return null
161
+
162
+ let gitRoot
163
+ try {
164
+ gitRoot = ( await simpleGit( dir ).revparse( [ '--show-toplevel' ] ) ).trim()
165
+ } catch {
166
+ return null
167
+ }
168
+
169
+ const isWorkspace = gitRoot !== dir
170
+ const git = simpleGit( gitRoot )
171
+ const pkg = await parseJsonFile( path.join( dir, 'package.json' ) )
172
+
173
+ // Monorepo roots are represented by their workspace packages — skip the root itself
174
+ if ( !isWorkspace && pkg?.workspaces ) return null
175
+
176
+ const pkgVer = parseVersion( pkg?.version )
177
+
178
+ let syncOk = true
179
+ if ( isWorkspace ) {
180
+ try {
181
+ const relDir = path.relative( gitRoot, dir )
182
+ const status = await git.status( [ '--', relDir ] )
183
+ syncOk = status.files.length === 0
184
+ } catch {
185
+ syncOk = false
186
+ }
187
+ } else if ( pull ) {
188
+ try {
189
+ await git.pull()
190
+ await git.fetch( [ '--tags' ] )
191
+ } catch {
192
+ syncOk = false
193
+ }
194
+ }
195
+
196
+ let gitVer = null
197
+ if ( isWorkspace ) {
198
+ // Look for workspace-style tags: pkg-name/vX.Y.Z
199
+ const pkgBaseName = ( pkg?.name ?? path.basename( dir ) ).replace( /^@[^/]+\//, '' )
200
+ try {
201
+ const tagOutput = ( await git.raw( [ 'tag', '--list', `${pkgBaseName}/v*` ] ) ).trim()
202
+ if ( tagOutput ) {
203
+ const semverTags = tagOutput.split( '\n' )
204
+ .map( ( t ) => { const m = t.trim().match( /(?:^|\/)v(\d+\.\d+\.\d+)$/ ); return m ? m[1] : null } )
205
+ .filter( Boolean )
206
+ if ( semverTags.length ) {
207
+ semverTags.sort( ( a, b ) => {
208
+ const av = a.split( '.' ).map( Number )
209
+ const bv = b.split( '.' ).map( Number )
210
+ return ( bv[0] - av[0] ) || ( bv[1] - av[1] ) || ( bv[2] - av[2] )
211
+ } )
212
+ gitVer = `v${semverTags[0]}`
213
+ } else {
214
+ gitVer = NONSEMVER
215
+ }
216
+ }
217
+ } catch { /* no tags → null = UNTAGGD */ }
218
+ } else {
219
+ // git describe gives the nearest tag reachable from HEAD, matching original pkgck behavior
220
+ try {
221
+ const tag = ( await git.raw( [ 'describe', '--abbrev=0', '--tags' ] ) ).trim()
222
+ // Handles 'v1.2.3' and workspace-style 'pkg-name/v1.2.3'
223
+ const match = tag.match( /(?:^|\/)v(\d+\.\d+\.\d+)$/ )
224
+ gitVer = match ? `v${match[1]}` : NONSEMVER
225
+ } catch { /* no tags → null = UNTAGGD */ }
226
+ }
227
+
228
+ let needsPush = ''
229
+ if ( pkgVer && pkgVer !== NONSEMVER && gitVer === pkgVer ) {
230
+ try {
231
+ const pkgBaseName = ( pkg?.name ?? path.basename( dir ) ).replace( /^@[^/]+\//, '' )
232
+ const tagToCheck = isWorkspace ? `${pkgBaseName}/${pkgVer}` : pkgVer
233
+ const remote = await git.listRemote( [ '--tags', 'origin' ] )
234
+ if ( !remote.includes( tagToCheck ) ) needsPush = chalk.yellow( '!tags' )
235
+ } catch { /* ignore */ }
236
+ }
237
+
238
+ const isYarn = fs.existsSync( path.join( gitRoot, 'yarn.lock' ) ) || fs.existsSync( path.join( gitRoot, '.yarn' ) )
239
+ const pkgMgr = isYarn ? chalk.cyan( 'yrn' ) : chalk.red( 'npm' )
240
+
241
+ const { repoName, group } = repoLabel( dir, gitRoot )
242
+ return {
243
+ repoName,
244
+ group,
245
+ pkgMgr,
246
+ pkgVer : fmtPkgVer( pkgVer ),
247
+ gitVer : fmtGitVer( gitVer, pkgVer ),
248
+ dirty : !syncOk,
249
+ scope : fmtScope( pkg ),
250
+ isWorkspace,
251
+ checks : CHECKS.map( ( { label, fn } ) => ( { label, ...fn( { dir, pkg, isWorkspace, gitRoot } ) } ) ),
252
+ needsPush,
253
+ }
254
+ }
255
+
256
+ // ---------- Table ----------
257
+
258
+ function renderTable( rows ) {
259
+ const pkgW = Math.max( 3, ...rows.map( r => visLen( r.pkgVer ) ) )
260
+ const gitW = Math.max( 7, ...rows.map( r => visLen( r.gitVer ) ) )
261
+ const scopeW = Math.max( 4, ...rows.map( r => visLen( r.scope ) ) )
262
+
263
+ // Fixed width of all columns except name. Elements: mgr pkg git scope checks (no needsPush).
264
+ // join(' ') puts 2-space sep between N+5 elements = N+4 separators.
265
+ const checkLabelW = CHECKS.reduce( ( sum, c ) => sum + c.label.length, 0 )
266
+ const fixedW = 3 + pkgW + gitW + scopeW + checkLabelW + ( CHECKS.length + 4 ) * 2
267
+
268
+ // Name area: 2-space indent + repo name. Group label prints on its own line above each section.
269
+ const hasGroups = rows.some( r => r.group )
270
+ const indent = hasGroups ? 2 : 0
271
+ const maxRepoW = Math.min( 22, Math.max( 4, ...rows.map( r => r.repoName.length ) ) )
272
+ const nameW = Math.min( indent + maxRepoW, 80 - fixedW )
273
+ const repoW = nameW - indent
274
+
275
+ const nameHdr = pad( `${' '.repeat( indent )}name`, nameW )
276
+ const hdr = [
277
+ nameHdr,
278
+ center( 'mgr', 3 ),
279
+ center( 'pkg', pkgW ),
280
+ center( 'git', gitW ),
281
+ center( 'scop', scopeW ),
282
+ ...CHECKS.map( c => center( c.label, c.label.length ) ),
283
+ ].join( ' ' )
284
+
285
+ const separator = chalk.dim( '─'.repeat( visLen( hdr ) ) )
286
+
287
+ log( '' )
288
+ log( chalk.dim( hdr ) )
289
+ log( separator )
290
+
291
+ let currentGroup
292
+ for ( const row of rows ) {
293
+ if ( row.group !== currentGroup ) {
294
+ currentGroup = row.group
295
+ if ( hasGroups ) log( chalk.cyan( row.group ?? '' ) )
296
+ }
297
+ const nameStr = ' '.repeat( indent ) + row.repoName.slice( 0, repoW )
298
+ const nameCell = row.dirty ? chalk.hex( '#FFA500' )( nameStr ) : nameStr
299
+ const line = [
300
+ pad( nameCell, nameW ),
301
+ pad( row.pkgMgr, 3 ),
302
+ pad( row.pkgVer, pkgW ),
303
+ pad( row.gitVer, gitW ),
304
+ pad( row.scope, scopeW ),
305
+ ...row.checks.map( c => center( c.value, c.label.length ) ),
306
+ ].join( ' ' ) + ( row.needsPush ? ` ${row.needsPush}` : '' )
307
+ log( line )
308
+ }
309
+
310
+ log( '' )
311
+ }
312
+
313
+ // ---------- CLI ----------
314
+
315
+ program
316
+ .name( 'package-checker' )
317
+ .description( chalk.green( 'A panopticon for your git repos!' ) )
318
+ .argument( '<dirs...>', 'directories to check' )
319
+ .option( '--no-pull', 'skip git pull and tag fetch' )
320
+ .option( '--no-clear', 'do not clear the screen before output' )
321
+ .option( '--concurrency <n>', 'max parallel repos', '5' )
322
+ .option( '--limit <n>', 'max rows to display', '80' )
323
+ .option( '--debug', 'enable debug logging' )
324
+ .addHelpText( 'after', `
325
+ ${chalk.yellow( 'Columns:' )}
326
+ ${chalk.green( 'pkg' )} version from package.json
327
+ ${chalk.green( 'git' )} latest semver git tag (green=match, yellow=mismatch, red=untagged)
328
+ ${chalk.green( 'name' )} orange if dirty: pull/fetch failed (regular repos) or local modifications present (workspace packages)
329
+ ${chalk.green( 'scope' )} package scope: @org, PRVT, or PUBL
330
+ ${chalk.green( 'cir' )} CircleCI config: ✘=missing, L=legacy, 1=orb v1, 2=orb v2
331
+ ${chalk.green( 'ESM' )} "type": "module" in package.json
332
+ ${chalk.green( 'har' )} .har directory present at repo root
333
+ ${chalk.green( 'lnt' )} ESLint version from node_modules (✘=no lint script, ?=not installed, yellow=v8, green=v9+)
334
+
335
+ ${chalk.yellow( 'Monorepos:' )}
336
+ Workspace packages are discovered automatically — passing a monorepo root
337
+ expands its workspaces field so you never need to add packages/* globs.
338
+ Git tags are looked up as pkg-name/vX.Y.Z. Pull/fetch runs at the root.
339
+
340
+ ${chalk.yellow( 'Examples:' )}
341
+ ${chalk.green( 'package-checker ~/repos/my-service' )}
342
+ ${chalk.green( 'package-checker --no-pull ~/repos/*' )}
343
+ ${chalk.green( 'package-checker --concurrency 10 @plt/* @kud/* @srv/* @tpi/*' )}
344
+ ` )
345
+ .parse()
346
+
347
+ const dirs = program.args
348
+ const options = program.opts()
349
+ if ( options.debug ) { enableDebug() }
350
+
351
+ if ( options.clear ) { clear() }
352
+
353
+ // Auto-expand workspace packages from any monorepo roots in the input.
354
+ // Supports both array and yarn-style { packages: [...] } workspaces fields.
355
+ async function expandWorkspaces( dir ) {
356
+ try {
357
+ const pkg = await parseJsonFile( path.join( dir, 'package.json' ) )
358
+ const patterns = Array.isArray( pkg?.workspaces ) ?
359
+ pkg.workspaces :
360
+ ( pkg?.workspaces?.packages ?? [] )
361
+ if ( !patterns.length ) return []
362
+ const matches = await Promise.all(
363
+ patterns.map( p => glob( p, { cwd : dir, absolute : true } ) )
364
+ )
365
+ return matches.flat().filter( d => !/test|demo/i.test( path.basename( d ) ) )
366
+ } catch {
367
+ return []
368
+ }
369
+ }
370
+
371
+ const resolvedDirs = dirs.map( dir => path.resolve( dir ) )
372
+ const wsExpansions = ( await Promise.all( resolvedDirs.map( expandWorkspaces ) ) ).flat()
373
+ const allDirs = [ ...new Set( [ ...resolvedDirs, ...wsExpansions ] ) ]
374
+
375
+ const total = allDirs.length
376
+ let completed = 0
377
+ const spinner = ora( `Checking repos (0/${total})...` ).start()
378
+
379
+ process.on( 'SIGINT', () => { spinner.stop(); process.exit( 1 ) } )
380
+
381
+ const limit = pLimit( parseInt( options.concurrency, 10 ) )
382
+ const results = await Promise.all(
383
+ allDirs.map( dir => limit( async () => {
384
+ const result = await checkRepo( dir, { pull : options.pull } )
385
+ spinner.text = `Checking repos (${++completed}/${total})...`
386
+ return result
387
+ } ) )
388
+ )
389
+
390
+ spinner.stop()
391
+
392
+ const rows = results
393
+ .filter( Boolean )
394
+ .sort( ( a, b ) => {
395
+ const ga = a.group ?? ''
396
+ const gb = b.group ?? ''
397
+ if ( ga !== gb ) return ga.localeCompare( gb )
398
+ return a.repoName.localeCompare( b.repoName )
399
+ } )
400
+
401
+ if ( rows.length === 0 ) {
402
+ log( chalk.yellow( '\nNo valid repositories found.\n' ) )
403
+ process.exit( 0 )
404
+ }
405
+
406
+ const maxRows = parseInt( options.limit, 10 )
407
+ const checked = rows.length
408
+ const displayed = rows.slice( 0, maxRows )
409
+
410
+ renderTable( displayed )
411
+
412
+ const summary = checked > displayed.length ?
413
+ `total repositories checked: ${checked} (${displayed.length} displayed)\n` :
414
+ `total repositories checked: ${checked}\n`
415
+ log( summary )
package/src/pkgck.sh DELETED
@@ -1,158 +0,0 @@
1
- #!/bin/bash
2
-
3
- C_off='\e[0m'
4
- C_yellow='\e[93m'
5
- C_red='\e[101m'
6
- C_green='\e[92m'
7
-
8
- good() {
9
- printf "\e[32m✔${C_off}"
10
- }
11
-
12
- meh() {
13
- printf "\e[36m✩${C_off}"
14
- }
15
-
16
- bad() {
17
- printf "\e[31m✘${C_off}"
18
- }
19
-
20
- semver() {
21
- local version=$1
22
- local isSemver=`echo $version | egrep -e "^v\d+\.\d+\.\d+$"`
23
- if [ -z "$version" ];
24
- then
25
- printf "UNTAGGD"
26
- elif [ $isSemver ];
27
- then
28
- printf "$version"
29
- else
30
- printf "!SEMVER"
31
- fi
32
- }
33
-
34
- check_babel () {
35
- local C_stat=$(bad)
36
- [ -f .babelrc ] && grep --silent @leverege/babel-preset-leverege-node .babelrc
37
- [ $? -eq 0 ] && C_stat=$(good)
38
- [ ! -f .babelrc ] && C_stat=$(meh)
39
- printf $C_stat
40
- }
41
-
42
- check_har () {
43
- local C_stat=$(bad)
44
- local prep="`node -p \"require('./package.json').scripts.prepare\"`"
45
- local isHar=`echo $prep | egrep -e "hook-and-release"`
46
- [ ! -z "$isHar" ] && C_stat=$(good)
47
- printf $C_stat
48
- }
49
-
50
- check_circle () {
51
- local C_stat=$(bad)
52
- [ -f ".circleci/config.yml" ] && C_stat=$(good)
53
- printf $C_stat
54
- }
55
-
56
- check_eslint () {
57
- local C_stat=$(bad)
58
- [ -f .eslintrc ] && grep --silent @leverege/eslint-config-leverege .eslintrc
59
- [ $? -eq 0 ] && C_stat=$(good)
60
- printf $C_stat
61
- }
62
-
63
- check_license () {
64
- local C_stat=$(meh)
65
- local lic="`node -p \"require('./package.json').scripts.licenseCheck == undefined\"`"
66
- [ $lic == true ] && C_stat=$(good)
67
- printf $C_stat
68
- }
69
-
70
- # Invoke as: version_ck $GITTAG $NPMVER
71
- #
72
- check_git_version() {
73
- local tag=$1
74
- local pkg=$2
75
- local clr=$C_green
76
-
77
- if [ "$tag" == 'UNTAGGD' ];
78
- then
79
- clr=$C_red
80
- elif [ "$tag" != "$pkg" ];
81
- then
82
- clr=$C_yellow
83
- fi
84
-
85
- printf "$clr$tag$C_off"
86
- }
87
-
88
- #check_nodemon() {
89
- # nodemon="`getjson --file ./package.json --key nodemon`"
90
- #}
91
-
92
- check_scope() {
93
- private="`node -p \"require('./package.json').private == true\"`"
94
- scoped="`getjson --file ./package.json --key name --regex '^@\w{3}'`"
95
- result="${C_red}PUBL${C_off}"
96
- if [[ "$scoped" == "" ]] && [[ "$private" == "true" ]];
97
- then
98
- result="${C_yellow}PRVT"
99
- elif [[ "$scoped" != "" ]];
100
- then
101
- result="${C_green}${scoped}"
102
- elif [[ "$private" == "true" ]];
103
- then
104
- result="${C_green}prv"
105
- fi
106
- printf "${result}${C_off}"
107
- }
108
-
109
- check_ESM() {
110
- local C_stat=$(bad)
111
- jstype="`getjson --file package.json --key type`"
112
- [[ "$jstype" == "module" ]] && C_stat=$(good)
113
- printf $C_stat
114
- }
115
-
116
- repos=0
117
- check_repo() {
118
- local dir=$1
119
- [ ! -d $dir/.git ] || [ ! -f $dir/package.json ] && return
120
- cd $dir &> /dev/null
121
- local gperrs=0
122
- git pull -q &> /dev/null; [ $? -ne 0 ] && let "gperrs++"
123
- git pull -q --tags &> /dev/null; [ $? -ne 0 ] && let "gperrs++"
124
-
125
- # It is necessary to use the output from printf to set localmods in order
126
- # to get the ANSI escape codes expanded.
127
- local localmods=$(good)
128
- [ $gperrs -ne 0 ] && localmods=$(bad)
129
- pkgver=$(semver "v`node -p \"require('./package.json').version\"`")
130
- gitver=$(semver `git describe --abbrev=0 2>/dev/null`)
131
- [ $? -ne 0 ] && gitver='UNTAGGD'
132
- local needspush=
133
- if [ "$pkgver" == "$gitver" ];
134
- then
135
- git ls-remote --tags origin | grep $gitver &> /dev/null
136
- [ $? -ne 0 ] && needspush='!tags'
137
- fi
138
- local babel7=$(check_babel)
139
- local hookar=$(check_har)
140
- local circle=$(check_circle)
141
- local eslint=$(check_eslint)
142
- local esmlib=$(check_ESM)
143
- local license=$(check_license)
144
- local scoped=$(check_scope)
145
- [ $(( $repos % 15)) -eq 0 ] && printf "\n%35s pkg git cln scop cir lic ESM har\n"
146
- printf "%32s: %-8s %-16s %s %8s %s %s %s %s %s\n" ${dir:0:32} $pkgver $(check_git_version $gitver $pkgver) $localmods $scoped $circle $license $esmlib $hookar "$needspush"
147
- let "repos++"
148
- cd - &> /dev/null
149
- }
150
-
151
- clear
152
-
153
- pkgs="$@"
154
- for dir in $@; do
155
- check_repo $dir
156
- done
157
-
158
- printf "\ntotal repoitories checked: $repos\n\n"