fg-devkit 1.12.6 → 1.12.8

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.
@@ -22,6 +22,8 @@ import {
22
22
  collectToolchainPolicyViolations,
23
23
  resolveExpectedPackageManager,
24
24
  } from './dependency-policy-toolchain.mjs'
25
+ import { findPnpmVersionMismatchesInRepo } from './pnpm-toolchain-sync.mjs'
26
+ import { collectNodeToolchainViolations } from './node-toolchain-sync.mjs'
25
27
  import { resolveDependencyPolicyPath } from './resolve-dependency-policy-path.mjs'
26
28
 
27
29
  const cwd = process.cwd()
@@ -98,6 +100,12 @@ if (expectedPackageManager !== undefined) {
98
100
  }
99
101
  }
100
102
 
103
+ const fgDevkitRoot = path.resolve(scriptDir, '..')
104
+ mismatches.push(...collectNodeToolchainViolations(cwd, policy, fgDevkitRoot))
105
+ if (expectedPackageManager !== undefined) {
106
+ mismatches.push(...findPnpmVersionMismatchesInRepo(cwd, expectedPackageManager))
107
+ }
108
+
101
109
  // Intersection: only packages that exist in both policy and consumer are compared.
102
110
  for (const [section, dependencies] of Object.entries(policy.managedDependencies ?? {})) {
103
111
  const current = pkg[section] ?? {}
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Shared helpers for fleet-wide pnpm / Corepack toolchain policy.
2
+ * Shared helpers for fleet-wide pnpm / Corepack / Node toolchain policy.
3
3
  */
4
4
 
5
5
  import fs from 'node:fs'
6
6
  import path from 'node:path'
7
7
 
8
8
  const PNPM_PACKAGE_MANAGER_PATTERN = /^pnpm@\d+\.\d+\.\d+(-[\w.]+)?$/i
9
+ const NODE_NVMRC_PATTERN = /^\d+(\.\d+(\.\d+)?)?$/
9
10
 
10
11
  /** @param {unknown} value */
11
12
  export function isValidPnpmPackageManager(value) {
@@ -67,16 +68,195 @@ export function resolveExpectedPackageManager(policy, fgDevkitRoot) {
67
68
  * @returns {string[]}
68
69
  */
69
70
  export function collectToolchainPolicyViolations(policy, labelPrefix = '[policy] toolchain.') {
71
+ const violations = []
70
72
  const packageManager = policy?.toolchain?.packageManager
71
- if (packageManager === undefined) {
73
+ if (packageManager !== undefined && !isValidPnpmPackageManager(packageManager)) {
74
+ violations.push(
75
+ `${labelPrefix}packageManager: must be pnpm@x.y.z, got ${JSON.stringify(packageManager)}`,
76
+ )
77
+ }
78
+ violations.push(...collectNodeToolchainPolicyViolations(policy, labelPrefix))
79
+ return violations
80
+ }
81
+
82
+ /**
83
+ * @param {unknown} value
84
+ */
85
+ export function isValidNodeNvmrc(value) {
86
+ return typeof value === 'string' && NODE_NVMRC_PATTERN.test(value.trim().replace(/^v/, ''))
87
+ }
88
+
89
+ /**
90
+ * @param {string} value
91
+ * @returns {string}
92
+ */
93
+ export function normalizeNodeVersionLine(value) {
94
+ return value.trim().replace(/^v/, '')
95
+ }
96
+
97
+ /**
98
+ * @param {string} nvmrc
99
+ * @returns {string}
100
+ */
101
+ export function deriveJenkinsImageTag(nvmrc) {
102
+ return `node${normalizeNodeVersionLine(nvmrc).split('.')[0]}`
103
+ }
104
+
105
+ /**
106
+ * @param {string} version Bare major or semver (e.g. 22 or 22.13.0).
107
+ * @returns {string}
108
+ */
109
+ export function formatNodeNvmrc(version) {
110
+ const bare = normalizeNodeVersionLine(version)
111
+ if (!NODE_NVMRC_PATTERN.test(bare)) {
112
+ throw new Error(`Invalid Node nvmrc version: ${JSON.stringify(version)}`)
113
+ }
114
+ return bare
115
+ }
116
+
117
+ /**
118
+ * @typedef {{ nvmrc: string, engines?: string, jenkinsImage?: string, dockerTag?: string }} NodeToolchainPolicy
119
+ */
120
+
121
+ /**
122
+ * Docker image tag after `node:` (e.g. 22-alpine). Bookworm and other variants normalize to policy/default alpine.
123
+ *
124
+ * @param {string} nvmrc
125
+ * @param {string} [dockerTag]
126
+ * @returns {string}
127
+ */
128
+ export function resolveDockerNodeTag(nvmrc, dockerTag) {
129
+ if (typeof dockerTag === 'string' && dockerTag.trim()) {
130
+ return dockerTag.trim()
131
+ }
132
+ return `${formatNodeNvmrc(nvmrc).split('.')[0]}-alpine`
133
+ }
134
+
135
+ /**
136
+ * @param {string} fgDevkitRoot
137
+ * @returns {NodeToolchainPolicy | undefined}
138
+ */
139
+ export function readFgDevkitNodePolicy(fgDevkitRoot) {
140
+ const nvmrcPath = path.join(fgDevkitRoot, '.nvmrc')
141
+ if (!fs.existsSync(nvmrcPath)) {
142
+ return undefined
143
+ }
144
+ const nvmrc = formatNodeNvmrc(fs.readFileSync(nvmrcPath, 'utf8'))
145
+
146
+ const pkgPath = path.join(fgDevkitRoot, 'package.json')
147
+ let engines
148
+ if (fs.existsSync(pkgPath)) {
149
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
150
+ if (typeof pkg.engines?.node === 'string') {
151
+ engines = pkg.engines.node
152
+ }
153
+ }
154
+
155
+ return {
156
+ nvmrc,
157
+ engines,
158
+ jenkinsImage: deriveJenkinsImageTag(nvmrc),
159
+ dockerTag: resolveDockerNodeTag(nvmrc),
160
+ }
161
+ }
162
+
163
+ /**
164
+ * @param {Record<string, unknown>} policy
165
+ * @param {string} [fgDevkitRoot]
166
+ * @returns {NodeToolchainPolicy | undefined}
167
+ */
168
+ export function resolveExpectedNodePolicy(policy, fgDevkitRoot) {
169
+ const fromPolicy = policy?.toolchain?.node
170
+ if (fromPolicy && typeof fromPolicy === 'object' && typeof fromPolicy.nvmrc === 'string') {
171
+ const nvmrc = formatNodeNvmrc(fromPolicy.nvmrc)
172
+ const engines =
173
+ fromPolicy.engines === null || fromPolicy.engines === undefined
174
+ ? undefined
175
+ : String(fromPolicy.engines)
176
+ const jenkinsImage =
177
+ typeof fromPolicy.jenkinsImage === 'string'
178
+ ? fromPolicy.jenkinsImage
179
+ : deriveJenkinsImageTag(nvmrc)
180
+ const dockerTag =
181
+ typeof fromPolicy.dockerTag === 'string'
182
+ ? fromPolicy.dockerTag
183
+ : resolveDockerNodeTag(nvmrc)
184
+ return { nvmrc, engines, jenkinsImage, dockerTag }
185
+ }
186
+ if (fgDevkitRoot) {
187
+ return readFgDevkitNodePolicy(fgDevkitRoot)
188
+ }
189
+ return undefined
190
+ }
191
+
192
+ /**
193
+ * @param {Record<string, unknown>} policy
194
+ * @param {string} fgDevkitRoot
195
+ * @returns {{ policy: Record<string, unknown>; changed: boolean }}
196
+ */
197
+ export function syncPolicyNodeFromFgDevkit(policy, fgDevkitRoot) {
198
+ const fromFgDevkit = readFgDevkitNodePolicy(fgDevkitRoot)
199
+ if (!fromFgDevkit) {
200
+ return { policy, changed: false }
201
+ }
202
+ const next = {
203
+ ...policy,
204
+ toolchain: {
205
+ ...(policy.toolchain ?? {}),
206
+ node: {
207
+ nvmrc: fromFgDevkit.nvmrc,
208
+ ...(fromFgDevkit.engines ? { engines: fromFgDevkit.engines } : {}),
209
+ jenkinsImage: fromFgDevkit.jenkinsImage,
210
+ dockerTag: fromFgDevkit.dockerTag,
211
+ },
212
+ },
213
+ }
214
+ const current = policy.toolchain?.node
215
+ const changed =
216
+ !current ||
217
+ current.nvmrc !== next.toolchain.node.nvmrc ||
218
+ current.engines !== next.toolchain.node.engines ||
219
+ current.jenkinsImage !== next.toolchain.node.jenkinsImage ||
220
+ current.dockerTag !== next.toolchain.node.dockerTag
221
+ return { policy: changed ? next : policy, changed }
222
+ }
223
+
224
+ /**
225
+ * @param {Record<string, unknown>} policy
226
+ * @param {string} [labelPrefix]
227
+ * @returns {string[]}
228
+ */
229
+ export function collectNodeToolchainPolicyViolations(policy, labelPrefix = '[policy] toolchain.') {
230
+ const node = policy?.toolchain?.node
231
+ if (node === undefined || node === null) {
72
232
  return []
73
233
  }
74
- if (!isValidPnpmPackageManager(packageManager)) {
75
- return [
76
- `${labelPrefix}packageManager: must be pnpm@x.y.z, got ${JSON.stringify(packageManager)}`,
77
- ]
234
+ if (typeof node !== 'object') {
235
+ return [`${labelPrefix}node: must be an object`]
236
+ }
237
+
238
+ const violations = []
239
+ if (!isValidNodeNvmrc(node.nvmrc)) {
240
+ violations.push(
241
+ `${labelPrefix}node.nvmrc: must be a version like "22" or "22.13.0", got ${JSON.stringify(node.nvmrc)}`,
242
+ )
243
+ }
244
+ if (
245
+ node.engines !== undefined &&
246
+ node.engines !== null &&
247
+ (typeof node.engines !== 'string' || node.engines.trim() === '')
248
+ ) {
249
+ violations.push(`${labelPrefix}node.engines: must be a non-empty string or null`)
250
+ }
251
+ if (
252
+ node.jenkinsImage !== undefined &&
253
+ (typeof node.jenkinsImage !== 'string' || !/^node\d+$/.test(node.jenkinsImage))
254
+ ) {
255
+ violations.push(
256
+ `${labelPrefix}node.jenkinsImage: must be like "node22", got ${JSON.stringify(node.jenkinsImage)}`,
257
+ )
78
258
  }
79
- return []
259
+ return violations
80
260
  }
81
261
 
82
262
  /**
@@ -11,6 +11,8 @@
11
11
  * pnpm exec fg-devkit fleet-vulns [projects-root] [--summary-only] [--delay-ms=<ms>]
12
12
  * pnpm exec fg-devkit fleet-bump-fg-devkit [projects-root] [--dry-run]
13
13
  * pnpm exec fg-devkit bump-pnpm-toolchain [version] [projects-root] [--dry-run] [--skip-fleet] [--skip-install]
14
+ * pnpm exec fg-devkit sync-node-toolchain [policy-file] [--dry-run]
15
+ * pnpm exec fg-devkit bump-node-toolchain [nvmrc-version] [projects-root] [--dry-run] [--skip-fleet] [--no-engines]
14
16
  * pnpm exec fg-devkit policy-upgrade [policy-file] [ncu-args...]
15
17
  * pnpm exec fg-devkit policy-upgrade-commit [ncu-args...]
16
18
  * pnpm exec fg-devkit audit-override-commit [pnpm-audit-args...]
@@ -462,6 +464,15 @@ const DIRECT_COMMANDS = {
462
464
  'fleet-vulns': 'fleet-vulns.mjs',
463
465
  'fleet-bump-fg-devkit': 'fleet-bump-fg-devkit.mjs',
464
466
  'bump-pnpm-toolchain': 'bump-pnpm-toolchain.mjs',
467
+ 'sync-node-toolchain': 'sync-node-toolchain.mjs',
468
+ 'sync-pnpm-toolchain': 'sync-pnpm-toolchain.mjs',
469
+ 'bump-node-toolchain': 'bump-node-toolchain.mjs',
470
+ 'fleet-git-pull': 'fleet-git-pull.mjs',
471
+ 'fleet-git-pull-scoped': 'fleet-git-pull-scoped.mjs',
472
+ 'fleet-git-commit-push': 'fleet-git-commit-push.mjs',
473
+ 'fleet-git-commit-push-scoped': 'fleet-git-commit-push-scoped.mjs',
474
+ 'fleet-git-discard': 'fleet-git-discard.mjs',
475
+ 'fleet-git-discard-scoped': 'fleet-git-discard-scoped.mjs',
465
476
  'policy-upgrade': 'policy-upgrade.mjs',
466
477
  'policy-upgrade-commit': 'policy-upgrade-commit.mjs',
467
478
  'audit-override-commit': 'audit-override-commit.mjs',
@@ -486,7 +497,7 @@ function printHelp() {
486
497
 
487
498
  Commands:
488
499
  check-deps [policy-file] Validate package.json + pins + toolchain against policy
489
- sync-deps [policy-file] Align declared versions and packageManager to policy
500
+ sync-deps [policy-file] Align declared versions, packageManager, and Node toolchain to policy
490
501
  policy-outdated [policy-file] Show outdated policy packages via pnpm outdated
491
502
  policy-vulns [policy-file] Check all policy packages together (including transitives) via pnpm audit
492
503
  repo-vulns Check all repo dependencies for vulnerabilities via pnpm audit
@@ -496,7 +507,30 @@ Commands:
496
507
  Bump fg-devkit, run sync-deps, then pnpm install across Vue repos
497
508
  bump-pnpm-toolchain [version] [projects-root] [options]
498
509
  Maintainer: bump pnpm to latest (or version), sync fg-devkit + fleet
499
- Options: --dry-run, --skip-fleet, --skip-install
510
+ Fleet: all pnpm repos (Vue+fg-devkit get full sync-deps/check-deps)
511
+ Options: --dry-run, --skip-fleet, --skip-install, --pick, --all, --only=a,b
512
+ sync-node-toolchain [policy-file] [--dry-run]
513
+ Align .nvmrc, engines, Dockerfiles, and Jenkinsfile to policy.node
514
+ sync-pnpm-toolchain [policy-file] [--dry-run]
515
+ Align packageManager and all pnpm@ pins to policy.packageManager
516
+ bump-node-toolchain [version] [projects-root] [options]
517
+ Maintainer: set policy.node and sync Node toolchain across fleet
518
+ Options: --dry-run, --skip-fleet, --no-engines, --engines=RANGE, --pick, --all, --only=a,b
519
+ fleet-git-pull [projects-root] [--dry-run]
520
+ Maintainer: git pull (current branch) for all git repos under projects root
521
+ fleet-git-pull-scoped [projects-root] [options]
522
+ Maintainer: git pull (current branch) for chosen repositories
523
+ Options: --dry-run, --fg-devkit-only, --pick, --all, --only=a,b
524
+ fleet-git-commit-push --message="..." [projects-root] [--dry-run]
525
+ Maintainer: stage, commit, and push all git repos under projects root
526
+ fleet-git-commit-push-scoped --message="..." [projects-root] [options]
527
+ Maintainer: stage, commit, and push chosen repositories
528
+ Options: --dry-run, --fg-devkit-only, --pick, --all, --only=a,b
529
+ fleet-git-discard [projects-root] [--dry-run]
530
+ Maintainer: discard tracked git changes in all git repos under projects root
531
+ fleet-git-discard-scoped [projects-root] [options]
532
+ Maintainer: discard tracked git changes in chosen repositories
533
+ Options: --dry-run, --fg-devkit-only, --pick, --all, --only=a,b
500
534
  policy-upgrade [policy-file] [ncu-args...]
501
535
  Upgrade policy packages via npm-check-updates (default interactive)
502
536
  policy-upgrade-commit [ncu-args...]
@@ -825,6 +859,13 @@ async function runInteractiveMenu() {
825
859
  { name: 'Check vulnerabilities across all Vue repos', value: 'm6' },
826
860
  { name: 'Bump fg-devkit in all Vue repos, run sync-deps, and pnpm install', value: 'm7' },
827
861
  { name: 'Bump pnpm toolchain (latest)', value: 'm8' },
862
+ { name: 'Bump Node toolchain (fleet sync)', value: 'm9' },
863
+ { name: 'Git pull all repositories', value: 'm10' },
864
+ { name: 'Git pull (choose repositories)', value: 'm11' },
865
+ { name: 'Stage, commit and push all repositories', value: 'm12' },
866
+ { name: 'Stage, commit and push (choose repositories)', value: 'm13' },
867
+ { name: 'Discard tracked changes in all repositories', value: 'm14' },
868
+ { name: 'Discard tracked changes (choose repositories)', value: 'm15' },
828
869
  { name: 'Cancel', value: 'cancel' },
829
870
  ],
830
871
  default: 'm1',
@@ -845,7 +886,8 @@ async function runInteractiveMenu() {
845
886
  message: 'Bump pnpm toolchain',
846
887
  choices: [
847
888
  { name: 'fg-devkit only (policy, scaffold, install)', value: 'fg-devkit' },
848
- { name: 'fg-devkit + all Vue repos under projects root', value: 'fleet' },
889
+ { name: 'fg-devkit + pick pnpm repos under projects root', value: 'fleet' },
890
+ { name: 'Dry run + pick pnpm repos', value: 'dry-run-fleet' },
849
891
  { name: 'Cancel', value: 'cancel' },
850
892
  ],
851
893
  default: 'fg-devkit',
@@ -856,8 +898,159 @@ async function runInteractiveMenu() {
856
898
  if (scope === 'fg-devkit') {
857
899
  bumpArgs.push('--skip-fleet')
858
900
  }
901
+ if (scope === 'fleet') {
902
+ bumpArgs.push('--pick')
903
+ }
904
+ if (scope === 'dry-run-fleet') {
905
+ bumpArgs.push('--dry-run', '--pick')
906
+ }
859
907
  return runSelectedAction(() => runNode('bump-pnpm-toolchain.mjs', bumpArgs))
860
908
  }
909
+ if (choice === 'm9') {
910
+ const scope = await selectPrompt({
911
+ message: 'Bump Node toolchain',
912
+ choices: [
913
+ { name: 'fg-devkit only (policy + local files)', value: 'fg-devkit' },
914
+ { name: 'fg-devkit + pick projects under projects root', value: 'fleet' },
915
+ { name: 'Dry run + pick projects', value: 'dry-run-fleet' },
916
+ { name: 'Cancel', value: 'cancel' },
917
+ ],
918
+ default: 'fleet',
919
+ loop: false,
920
+ })
921
+ if (scope === 'cancel') continue
922
+ const bumpArgs = []
923
+ if (scope === 'fg-devkit') {
924
+ bumpArgs.push('--skip-fleet')
925
+ }
926
+ if (scope === 'fleet') {
927
+ bumpArgs.push('--pick')
928
+ }
929
+ if (scope === 'dry-run-fleet') {
930
+ bumpArgs.push('--dry-run', '--pick')
931
+ }
932
+ return runSelectedAction(() => runNode('bump-node-toolchain.mjs', bumpArgs))
933
+ }
934
+ if (choice === 'm10') {
935
+ const scope = await selectPrompt({
936
+ message: 'Git pull all repositories',
937
+ choices: [
938
+ { name: 'Pull all git repos under projects root', value: 'pull' },
939
+ { name: 'Dry run (all repos)', value: 'dry-run' },
940
+ { name: 'Cancel', value: 'cancel' },
941
+ ],
942
+ default: 'pull',
943
+ loop: false,
944
+ })
945
+ if (scope === 'cancel') continue
946
+ const pullArgs = scope === 'dry-run' ? ['--dry-run'] : []
947
+ return runSelectedAction(() => runNode('fleet-git-pull.mjs', pullArgs))
948
+ }
949
+ if (choice === 'm11') {
950
+ const scope = await selectPrompt({
951
+ message: 'Git pull (choose repositories)',
952
+ choices: [
953
+ { name: 'fg-devkit only', value: 'fg-devkit' },
954
+ { name: 'Choose repositories under projects root', value: 'pick' },
955
+ { name: 'Dry run + choose repositories', value: 'dry-run-pick' },
956
+ { name: 'Cancel', value: 'cancel' },
957
+ ],
958
+ default: 'pick',
959
+ loop: false,
960
+ })
961
+ if (scope === 'cancel') continue
962
+ const pullArgs = []
963
+ if (scope === 'fg-devkit') {
964
+ pullArgs.push('--fg-devkit-only')
965
+ }
966
+ if (scope === 'pick') {
967
+ pullArgs.push('--pick')
968
+ }
969
+ if (scope === 'dry-run-pick') {
970
+ pullArgs.push('--dry-run', '--pick')
971
+ }
972
+ return runSelectedAction(() => runNode('fleet-git-pull-scoped.mjs', pullArgs))
973
+ }
974
+ if (choice === 'm12') {
975
+ const scope = await selectPrompt({
976
+ message: 'Stage, commit and push all repositories',
977
+ choices: [
978
+ { name: 'Stage, commit and push all repos', value: 'run' },
979
+ { name: 'Dry run (all repos)', value: 'dry-run' },
980
+ { name: 'Cancel', value: 'cancel' },
981
+ ],
982
+ default: 'run',
983
+ loop: false,
984
+ })
985
+ if (scope === 'cancel') continue
986
+ const commitArgs = scope === 'dry-run' ? ['--dry-run'] : []
987
+ return runSelectedAction(() => runNode('fleet-git-commit-push.mjs', commitArgs))
988
+ }
989
+ if (choice === 'm13') {
990
+ const scope = await selectPrompt({
991
+ message: 'Stage, commit and push (choose repositories)',
992
+ choices: [
993
+ { name: 'fg-devkit only', value: 'fg-devkit' },
994
+ { name: 'Choose repositories under projects root', value: 'pick' },
995
+ { name: 'Dry run + choose repositories', value: 'dry-run-pick' },
996
+ { name: 'Cancel', value: 'cancel' },
997
+ ],
998
+ default: 'pick',
999
+ loop: false,
1000
+ })
1001
+ if (scope === 'cancel') continue
1002
+ const commitArgs = []
1003
+ if (scope === 'fg-devkit') {
1004
+ commitArgs.push('--fg-devkit-only')
1005
+ }
1006
+ if (scope === 'pick') {
1007
+ commitArgs.push('--pick')
1008
+ }
1009
+ if (scope === 'dry-run-pick') {
1010
+ commitArgs.push('--dry-run', '--pick')
1011
+ }
1012
+ return runSelectedAction(() => runNode('fleet-git-commit-push-scoped.mjs', commitArgs))
1013
+ }
1014
+ if (choice === 'm14') {
1015
+ const scope = await selectPrompt({
1016
+ message: 'Discard tracked changes in all repositories',
1017
+ choices: [
1018
+ { name: 'Discard tracked changes in all repos', value: 'run' },
1019
+ { name: 'Dry run (all repos)', value: 'dry-run' },
1020
+ { name: 'Cancel', value: 'cancel' },
1021
+ ],
1022
+ default: 'run',
1023
+ loop: false,
1024
+ })
1025
+ if (scope === 'cancel') continue
1026
+ const discardArgs = scope === 'dry-run' ? ['--dry-run'] : []
1027
+ return runSelectedAction(() => runNode('fleet-git-discard.mjs', discardArgs))
1028
+ }
1029
+ if (choice === 'm15') {
1030
+ const scope = await selectPrompt({
1031
+ message: 'Discard tracked changes (choose repositories)',
1032
+ choices: [
1033
+ { name: 'fg-devkit only', value: 'fg-devkit' },
1034
+ { name: 'Choose repositories under projects root', value: 'pick' },
1035
+ { name: 'Dry run + choose repositories', value: 'dry-run-pick' },
1036
+ { name: 'Cancel', value: 'cancel' },
1037
+ ],
1038
+ default: 'pick',
1039
+ loop: false,
1040
+ })
1041
+ if (scope === 'cancel') continue
1042
+ const discardArgs = []
1043
+ if (scope === 'fg-devkit') {
1044
+ discardArgs.push('--fg-devkit-only')
1045
+ }
1046
+ if (scope === 'pick') {
1047
+ discardArgs.push('--pick')
1048
+ }
1049
+ if (scope === 'dry-run-pick') {
1050
+ discardArgs.push('--dry-run', '--pick')
1051
+ }
1052
+ return runSelectedAction(() => runNode('fleet-git-discard-scoped.mjs', discardArgs))
1053
+ }
861
1054
  return 1
862
1055
  }
863
1056
  }
@@ -942,6 +1135,24 @@ async function main() {
942
1135
  process.exitCode = 1
943
1136
  return
944
1137
  }
1138
+ if (sub === 'bump-node-toolchain' && !isFgDevkitMaintainerContext()) {
1139
+ console.error('[fg-devkit] bump-node-toolchain is only available in the fg-devkit repository root.')
1140
+ process.exitCode = 1
1141
+ return
1142
+ }
1143
+ if (
1144
+ (sub === 'fleet-git-pull' ||
1145
+ sub === 'fleet-git-pull-scoped' ||
1146
+ sub === 'fleet-git-commit-push' ||
1147
+ sub === 'fleet-git-commit-push-scoped' ||
1148
+ sub === 'fleet-git-discard' ||
1149
+ sub === 'fleet-git-discard-scoped') &&
1150
+ !isFgDevkitMaintainerContext()
1151
+ ) {
1152
+ console.error(`[fg-devkit] ${sub} is only available in the fg-devkit repository root.`)
1153
+ process.exitCode = 1
1154
+ return
1155
+ }
945
1156
  process.exitCode = runNode(script, rest)
946
1157
  return
947
1158
  }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Maintainer: stage all changes, commit, and push on the current branch for chosen repositories.
4
+ *
5
+ * Usage (from fg-devkit repository root):
6
+ * pnpm exec fg-devkit fleet-git-commit-push-scoped --message="chore: sync toolchain" [options]
7
+ *
8
+ * Options:
9
+ * --dry-run Print planned actions without staging, committing, or pushing
10
+ * --fg-devkit-only Only process the fg-devkit repository
11
+ * --pick Interactive checkbox to choose repositories (default when TTY)
12
+ * --all Run on all git repositories under projects root without prompting
13
+ * --only=a,b Run only on matching repos (basename or path under projects root)
14
+ */
15
+
16
+ import fs from 'node:fs'
17
+ import path from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+
20
+ import { collectFleetGitRepos } from './fleet-project-repos.mjs'
21
+ import { parseFleetSelectionFlags, resolveFleetRepos } from './fleet-repo-picker.mjs'
22
+ import { filterCommitPushPositionalArgs, resolveCommitMessage } from './git-fleet-common.mjs'
23
+ import { runFleetGitCommitPush } from './git-commit-push-fleet-common.mjs'
24
+
25
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
26
+ const fgDevkitRoot = path.resolve(scriptDir, '..')
27
+ const fgDevkitPkgPath = path.join(fgDevkitRoot, 'package.json')
28
+ const LOG_PREFIX = '[fleet-git-commit-push-scoped]'
29
+
30
+ const cliArgs = process.argv.slice(2)
31
+ const dryRun = cliArgs.includes('--dry-run')
32
+ const fgDevkitOnly = cliArgs.includes('--fg-devkit-only')
33
+ const fleetArgs = filterCommitPushPositionalArgs(cliArgs)
34
+ const { pick, all, only, filteredArgs: positionalArgs } = parseFleetSelectionFlags(fleetArgs)
35
+ const projectsRootArg = positionalArgs[0]
36
+ let projectsRoot = projectsRootArg
37
+ ? path.resolve(process.cwd(), projectsRootArg)
38
+ : path.resolve(fgDevkitRoot, '..')
39
+
40
+ function assertFgDevkitMaintainerContext() {
41
+ if (path.resolve(process.cwd()) !== fgDevkitRoot) {
42
+ console.error(`${LOG_PREFIX} Run from the fg-devkit repository root.`)
43
+ process.exit(1)
44
+ }
45
+ try {
46
+ const pkg = JSON.parse(fs.readFileSync(fgDevkitPkgPath, 'utf8'))
47
+ if (pkg?.name !== 'fg-devkit') {
48
+ console.error(`${LOG_PREFIX} package.json name must be fg-devkit.`)
49
+ process.exit(1)
50
+ }
51
+ } catch {
52
+ console.error(`${LOG_PREFIX} Could not read fg-devkit package.json.`)
53
+ process.exit(1)
54
+ }
55
+ }
56
+
57
+ async function main() {
58
+ assertFgDevkitMaintainerContext()
59
+
60
+ if (!fgDevkitOnly && !fs.existsSync(projectsRoot)) {
61
+ console.error(`${LOG_PREFIX} projects root not found: ${projectsRoot}`)
62
+ process.exit(1)
63
+ }
64
+
65
+ const commitMessage = await resolveCommitMessage(cliArgs, LOG_PREFIX)
66
+
67
+ let repos
68
+ if (fgDevkitOnly) {
69
+ repos = [fgDevkitRoot]
70
+ projectsRoot = path.dirname(fgDevkitRoot)
71
+ } else {
72
+ const allRepos = collectFleetGitRepos(projectsRoot)
73
+ repos = await resolveFleetRepos(allRepos, projectsRoot, {
74
+ pick,
75
+ all,
76
+ only,
77
+ message: 'Select repositories to stage, commit, and push',
78
+ })
79
+ }
80
+
81
+ console.log(`${LOG_PREFIX} projectsRoot=${projectsRoot}`)
82
+ console.log(`${LOG_PREFIX} repositories=${repos.length}`)
83
+
84
+ const exitCode = runFleetGitCommitPush({
85
+ repos,
86
+ projectsRoot,
87
+ commitMessage,
88
+ dryRun,
89
+ logPrefix: LOG_PREFIX,
90
+ })
91
+ process.exit(exitCode)
92
+ }
93
+
94
+ main().catch((err) => {
95
+ console.error(LOG_PREFIX, err)
96
+ process.exit(1)
97
+ })
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Maintainer: stage all changes, commit, and push on the current branch for every git repo under projects root.
4
+ *
5
+ * Usage (from fg-devkit repository root):
6
+ * pnpm exec fg-devkit fleet-git-commit-push --message="chore: sync toolchain" [projects-root] [--dry-run]
7
+ * pnpm exec fg-devkit fleet-git-commit-push -m "chore: sync toolchain"
8
+ */
9
+
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+
14
+ import { collectFleetGitRepos } from './fleet-project-repos.mjs'
15
+ import { filterCommitPushPositionalArgs, resolveCommitMessage } from './git-fleet-common.mjs'
16
+ import { runFleetGitCommitPush } from './git-commit-push-fleet-common.mjs'
17
+
18
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
19
+ const fgDevkitRoot = path.resolve(scriptDir, '..')
20
+ const fgDevkitPkgPath = path.join(fgDevkitRoot, 'package.json')
21
+ const LOG_PREFIX = '[fleet-git-commit-push]'
22
+
23
+ const cliArgs = process.argv.slice(2)
24
+ const dryRun = cliArgs.includes('--dry-run')
25
+ const positionalArgs = filterCommitPushPositionalArgs(cliArgs)
26
+ const projectsRootArg = positionalArgs.find((a) => !a.startsWith('-'))
27
+ const projectsRoot = projectsRootArg
28
+ ? path.resolve(process.cwd(), projectsRootArg)
29
+ : path.resolve(fgDevkitRoot, '..')
30
+
31
+ function assertFgDevkitMaintainerContext() {
32
+ if (path.resolve(process.cwd()) !== fgDevkitRoot) {
33
+ console.error(`${LOG_PREFIX} Run from the fg-devkit repository root.`)
34
+ process.exit(1)
35
+ }
36
+ try {
37
+ const pkg = JSON.parse(fs.readFileSync(fgDevkitPkgPath, 'utf8'))
38
+ if (pkg?.name !== 'fg-devkit') {
39
+ console.error(`${LOG_PREFIX} package.json name must be fg-devkit.`)
40
+ process.exit(1)
41
+ }
42
+ } catch {
43
+ console.error(`${LOG_PREFIX} Could not read fg-devkit package.json.`)
44
+ process.exit(1)
45
+ }
46
+ }
47
+
48
+ async function main() {
49
+ assertFgDevkitMaintainerContext()
50
+
51
+ if (!fs.existsSync(projectsRoot)) {
52
+ console.error(`${LOG_PREFIX} projects root not found: ${projectsRoot}`)
53
+ process.exit(1)
54
+ }
55
+
56
+ const commitMessage = await resolveCommitMessage(cliArgs, LOG_PREFIX)
57
+ const repos = collectFleetGitRepos(projectsRoot)
58
+
59
+ console.log(`${LOG_PREFIX} projectsRoot=${projectsRoot}`)
60
+ console.log(`${LOG_PREFIX} repositories=${repos.length}`)
61
+
62
+ const exitCode = runFleetGitCommitPush({
63
+ repos,
64
+ projectsRoot,
65
+ commitMessage,
66
+ dryRun,
67
+ logPrefix: LOG_PREFIX,
68
+ })
69
+ process.exit(exitCode)
70
+ }
71
+
72
+ main().catch((err) => {
73
+ console.error(LOG_PREFIX, err)
74
+ process.exit(1)
75
+ })