@leverege/build-tools 2.101.2 → 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.
Files changed (43) hide show
  1. package/.vscode/launch.json +10 -0
  2. package/README.md +22 -9
  3. package/package.json +10 -13
  4. package/src/Utils.mjs +2 -0
  5. package/src/chart-to-registry.mjs +12 -1
  6. package/src/circle-orb-it/{circle-orb.yml → circle-npm.yml} +6 -3
  7. package/src/circle-orb-it/circle-orb-it.mjs +16 -11
  8. package/src/circle-orb-it/circle-yarn.yml +25 -0
  9. package/src/dashboard/DashboardManager.mjs +192 -17
  10. package/src/dashboard/dashboard.mjs +29 -4
  11. package/src/dashboard/json/{standard/noc-rows-12.json → devops/devops-noc.json} +801 -61
  12. package/src/dashboard/json/{standard/pvc-all-usage.json → devops/pvc-usage.json} +2 -2
  13. package/src/dashboard/json/leverege/leverege-noc.json +2364 -0
  14. package/src/dashboard/json/{standard/redis.json → leverege/leverege-rule-engine.json} +695 -646
  15. package/src/dashboard/json/leverege/leverege-services.json +4757 -0
  16. package/src/dashboard/json/recovr/recovr-locators.json +1227 -0
  17. package/src/dashboard/json/recovr/recovr-noc.json +1949 -0
  18. package/src/dashboard/json/recovr/recovr-position-srvr.json +339 -0
  19. package/src/dashboard/json/recovr/recovr-server.json +1127 -0
  20. package/src/dashboard/json/siren/siren-services.json +2066 -0
  21. package/src/dashboard/json/{standard/cloudnative-pg-20417-rev4.json → system/cloudnative-pg.json} +241 -243
  22. package/src/dashboard/json/{standard → system}/elasticsearch8.json +123 -72
  23. package/src/dashboard/json/system/stackdriver.json +1189 -0
  24. package/src/dashboard/json/{standard/traefik-op.json → system/traefik.json} +44 -56
  25. package/src/dashboard/json/{standard → system}/valkey.json +91 -86
  26. package/src/dashboard/json/{standard → tpi}/gpu-autoscaling-dashboard-ui-import.json +1 -1
  27. package/src/dashboard/json/tpi/leverege-noc.json +3624 -0
  28. package/src/helm-charts/prom-operator/helmup.plugin +1 -1
  29. package/src/helm-charts/prom-operator/prometheus-stack.yaml.ovh +15 -0
  30. package/src/helm-charts/prom-operator/rules/gke-maintenance-rules.yaml +19 -0
  31. package/src/helm-charts/prom-operator/stackdriver-exporter.yaml.ovh +2 -1
  32. package/src/helm-charts/valkey/valkey-local.yaml +1 -0
  33. package/src/helmup.sh +24 -1
  34. package/src/init-my-chart/Chartwright.mjs +1 -1
  35. package/src/init-my-chart/leaf-chart-templates/templates/extraobjects.yaml +1 -0
  36. package/src/overwhelm.mjs +5 -5
  37. package/src/push-my-chart.mjs +13 -4
  38. package/src/repo-panopticon.mjs +415 -0
  39. package/src/service-man/config/secrets/npmrc.json +1 -0
  40. package/src/service-man/config/secrets/tsdb-dense-password.json +13 -0
  41. package/src/dashboard/json/standard/stackdriver.json +0 -624
  42. package/src/pkgck.sh +0 -158
  43. /package/src/dashboard/json/{standard → devops}/k8s-api-latency.json +0 -0
@@ -8,7 +8,7 @@ showInstalling "The Prometheus Operator (kube-prometheus-stack)"
8
8
 
9
9
  # https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack
10
10
  OCI_CHART="oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack"
11
- [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="83.7.0"
11
+ [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="85.0.2"
12
12
  helm upgrade $NS --install prometheus-stack $OCI_CHART \
13
13
  --values prom-operator/prometheus-stack.yaml \
14
14
  --version $PROMETHEUS_STACK_CHART_VERSION $HELM_WHAT
@@ -143,9 +143,15 @@ grafana:
143
143
  sidecar:
144
144
  dashboards:
145
145
  enabled: true
146
+ folderAnnotation: grafana_folder
147
+ provider:
148
+ foldersFromFilesStructure: true
146
149
  # Credentials are injected from the grafana-google-oauth k8s secret, which is
147
150
  # managed by the ExternalSecret in helmup.plugin once ESO is deployed.
148
151
  # On first install, the secret is a placeholder created by helmup.bootstrap.
152
+ grafana.ini:
153
+ feature_toggles:
154
+ ssoSettingsApi: false
149
155
  envFromSecret: grafana-google-oauth
150
156
  env:
151
157
  GF_SERVER_ROOT_URL: https://${PROJECT_NAME}-monitoring.${HOST}.com
@@ -310,6 +316,15 @@ alertmanager:
310
316
  - namespace
311
317
  - alertname
312
318
 
319
+ # GKE node upgrade/maintenance suppresses expected churn alerts.
320
+ # PVC, disk pressure, and other infrastructure alerts are intentionally
321
+ # excluded so genuine storage/capacity problems are never silenced.
322
+ - source_matchers:
323
+ - alertname="GKEMaintenanceInProgress"
324
+ - maintenance="true"
325
+ target_matchers:
326
+ - alertname=~"KubernetesNodeNotReady|KubernetesPodNotHealthy|KubernetesPodCrashLooping|frequent_app_restarts"
327
+
313
328
  route:
314
329
  receiver: slack # default for everything
315
330
  group_by: ["alertname", "namespace"]
@@ -0,0 +1,19 @@
1
+ apiVersion: monitoring.coreos.com/v1
2
+ kind: PrometheusRule
3
+ metadata:
4
+ name: gke-maintenance-rules
5
+ namespace: prometheus
6
+
7
+ spec:
8
+ groups:
9
+ - name: GKE Maintenance
10
+ rules:
11
+ - alert: GKEMaintenanceInProgress
12
+ expr: sum(kube_node_spec_unschedulable) > 0
13
+ for: 3m
14
+ labels:
15
+ severity: info
16
+ maintenance: "true"
17
+ annotations:
18
+ summary: "GKE node maintenance in progress"
19
+ description: "{{ $value }} node(s) are currently unschedulable — GKE upgrade or cordon in progress"
@@ -19,7 +19,8 @@ stackdriver:
19
19
  kubernetes.io/container/accelerator/duty_cycle,
20
20
  kubernetes.io/container/accelerator/memory_total,
21
21
  kubernetes.io/container/accelerator/memory_used,
22
- monitoring.googleapis.com/uptime_check/check_passed,
22
+ pubsub.googleapis.com/subscription/backlog_bytes,
23
+ pubsub.googleapis.com/subscription/dead_letter_message_count,
23
24
  pubsub.googleapis.com/subscription/num_undelivered_messages,
24
25
  pubsub.googleapis.com/subscription/oldest_unacked_message_age,
25
26
  pubsub.googleapis.com/subscription/pull_ack_request_count,
@@ -85,6 +85,7 @@ sentinel:
85
85
 
86
86
  metrics:
87
87
  enabled: true
88
+ resourcesPreset: "small"
88
89
  image:
89
90
  registry: us-docker.pkg.dev
90
91
  repository: leverege-registry/bitnami-mirror/images/redis-exporter
package/src/helmup.sh CHANGED
@@ -20,6 +20,7 @@ function giveHelp() {
20
20
  `color g "--k8s-match"` Only proceed if k8s context is correctly set
21
21
  `color g "--dry-run"` Show what helm would do without applying changes
22
22
  `color g "--debug"` Enable verbose debug output
23
+ `color g "--mutate"` Forces a deployment mutation
23
24
  `color g "--help"` Show this help message
24
25
 
25
26
  Invocation Modes:
@@ -48,6 +49,9 @@ for arg in "$@"; do
48
49
  --k8s-match)
49
50
  export OVERWHELM_ARGS="--matchk8s"
50
51
  ;;
52
+ --mutate)
53
+ MUTATE_DEPLOYMENT=1
54
+ ;;
51
55
  --help|-h)
52
56
  giveHelp && exit 0
53
57
  ;;
@@ -622,6 +626,25 @@ function doHelmup() {
622
626
  local from=
623
627
  [ ! -z "$FROM_VERSIONS" ] && from="`color y \"<= from Versions.json\"`"
624
628
  printf "$doing $from\n"
629
+ if [[ $MUTATE_DEPLOYMENT -eq 1 ]];
630
+ then
631
+ cat<<MUTATION_MSG
632
+
633
+ `color y "WARNING: About to delete the $SERVICE k8s deployment object!
634
+ This will cause brief downtime while helm recreates it. The service
635
+ and any LoadBalancer/static IP will be unaffected."`
636
+
637
+ MUTATION_MSG
638
+ yesToContinue "to delete the $SERVICE deployment and proceed with upgrade"
639
+ kubectl delete deployments.apps $SERVICE
640
+ cat<<DELETE_WAIT
641
+
642
+ `color y " Waiting for $SERVICE deployment to be fully removed..."`
643
+
644
+ DELETE_WAIT
645
+ kubectl wait --for=delete deployment/$SERVICE --timeout=120s
646
+ fi
647
+
625
648
  # plugins pass HELMUP as a single quoted string - split it into args without using eval
626
649
  [[ $# -eq 1 ]] && set -- $1
627
650
  "$@" --set "helmup.culprit=$culprit" --set "helmup.deployT=$deployT"
@@ -811,7 +834,7 @@ REBOOTSTRAP
811
834
  }
812
835
 
813
836
  # ---------- helmup MAIN ----------
814
- helm repo update &>/dev/null & # see comments in the doInstall function for rationale
837
+ helm repo update leverege &>/dev/null & # see comments in the doInstall function for rationale
815
838
 
816
839
  overwhelm $OVERWHELM_ARGS
817
840
  [ $? -ne 0 ] && printf "\n\n***Aborting helmup...\n\n" && exit 1
@@ -13,7 +13,7 @@ const hbsContext = {
13
13
  hbsChartName : '',
14
14
  hbsChartVersion : '1.0.0',
15
15
  hbsChartAppVersion : 'v1.0.0',
16
- hbsArtifactRegistry : 'us-docker.pkg.dev/leverege-registry/FOOLDER/images',
16
+ hbsArtifactRegistry : 'us-docker.pkg.dev/leverege-registry/FOLDER/images',
17
17
  hbsPartOf : '',
18
18
  hbsServicePreemptible : true,
19
19
  hbsRedisEnabled : true,
@@ -0,0 +1 @@
1
+ {{ include "leverege.extraObjects" . }}
package/src/overwhelm.mjs CHANGED
@@ -250,14 +250,14 @@ const buildYaml = ( yamls, replacements ) => {
250
250
  const values = buildYaml( yams, over )
251
251
 
252
252
  // Check to see if someone is requesting a value from the yaml.
253
+ // Uses the fully merged+resolved `over` object so context-specific keys are found.
253
254
  if ( args.key ) {
254
- const section = args.section || 'default'
255
- const keys = yaml[section]
256
- debug( { keys }, '<==Replacements' )
255
+ const val = over[args.key] ?? ''
256
+ debug( { over }, '<==Replacements' )
257
257
  if ( args.export ) {
258
- log( `export ${args.key}=${keys[args.key]}` )
258
+ log( `export ${args.key}=${val}` )
259
259
  } else {
260
- log( keys[args.key] )
260
+ log( val )
261
261
  }
262
262
  if ( !args.continue ) process.exit()
263
263
  }
@@ -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 )
@@ -2,6 +2,7 @@
2
2
  "type": "managed",
3
3
  "name": "NPMRC",
4
4
  "source": "leverege-registry:DEPRECATED_NPMRC",
5
+ "k8s": { "secret": "npmrc", "key": "key" },
5
6
  "labels": {
6
7
  "env-type": "string"
7
8
  },
@@ -0,0 +1,13 @@
1
+ {
2
+ "type": "random",
3
+ "width": 48,
4
+ "immutable": true,
5
+ "labels": {
6
+ "env-type": "string"
7
+ },
8
+ "annotations": {
9
+ "description": "dense timescale db postgres password",
10
+ "owner": "platform-team",
11
+ "source": "service-man"
12
+ }
13
+ }