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.
@@ -11,6 +11,7 @@ import fs from 'node:fs'
11
11
  import path from 'node:path'
12
12
  import { spawnSync } from 'node:child_process'
13
13
  import { fileURLToPath } from 'node:url'
14
+ import { isFgDevkitRepoDir } from './fleet-project-repos.mjs'
14
15
 
15
16
  const scriptDir = path.dirname(fileURLToPath(import.meta.url))
16
17
  const cliArgs = process.argv.slice(2)
@@ -54,8 +55,8 @@ for (const packageJsonPath of packageJsonFiles) {
54
55
  try {
55
56
  const pkg = readJson(packageJsonPath)
56
57
  const repoDir = path.dirname(packageJsonPath)
58
+ if (isFgDevkitRepoDir(repoDir)) continue
57
59
  if (!isPnpmProject(pkg)) continue
58
- if (pkg.name === 'fg-devkit') continue
59
60
  repos.push(repoDir)
60
61
  } catch {
61
62
  // Ignore invalid package.json files.
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Stage, commit, and push helpers for fleet maintainer scripts.
3
+ */
4
+
5
+ import { spawnSync } from 'node:child_process'
6
+
7
+ import { formatRepoLabel } from './fleet-repo-picker.mjs'
8
+ import { getCurrentGitBranch, hasGitWorkingTreeChanges } from './git-fleet-common.mjs'
9
+
10
+ /**
11
+ * @param {{
12
+ * repos: string[]
13
+ * projectsRoot: string
14
+ * commitMessage: string
15
+ * dryRun?: boolean
16
+ * logPrefix?: string
17
+ * }} options
18
+ * @returns {number}
19
+ */
20
+ export function runFleetGitCommitPush(options) {
21
+ const {
22
+ repos,
23
+ projectsRoot,
24
+ commitMessage,
25
+ dryRun = false,
26
+ logPrefix = '[fleet-git-commit-push]',
27
+ } = options
28
+
29
+ /** @type {{ label: string, branch: string }[]} */
30
+ const committed = []
31
+ /** @type {string[]} */
32
+ const skipped = []
33
+ /** @type {{ label: string, reason: string }[]} */
34
+ const failed = []
35
+
36
+ for (const repoDir of repos) {
37
+ const label = formatRepoLabel(repoDir, projectsRoot)
38
+ const branch = getCurrentGitBranch(repoDir)
39
+
40
+ if (branch === null) {
41
+ failed.push({ label, reason: 'could not determine current branch' })
42
+ continue
43
+ }
44
+
45
+ if (!hasGitWorkingTreeChanges(repoDir)) {
46
+ skipped.push(label)
47
+ continue
48
+ }
49
+
50
+ if (dryRun) {
51
+ console.log(
52
+ `${logPrefix} DRY RUN ${label}: git add -A && git commit && git push (on ${branch})`,
53
+ )
54
+ committed.push({ label, branch })
55
+ continue
56
+ }
57
+
58
+ console.log(`\n${logPrefix} ${label} (${branch})`)
59
+
60
+ const stage = spawnSync('git', ['add', '-A'], {
61
+ cwd: repoDir,
62
+ encoding: 'utf8',
63
+ env: process.env,
64
+ })
65
+ if (stage.error) {
66
+ failed.push({ label, reason: stage.error.message })
67
+ continue
68
+ }
69
+ if ((stage.status ?? 1) !== 0) {
70
+ failed.push({
71
+ label,
72
+ reason: stage.stderr?.trim() || `git add exited with code ${stage.status ?? 1}`,
73
+ })
74
+ continue
75
+ }
76
+
77
+ const commit = spawnSync('git', ['commit', '-m', commitMessage], {
78
+ cwd: repoDir,
79
+ encoding: 'utf8',
80
+ env: process.env,
81
+ })
82
+ if (commit.error) {
83
+ failed.push({ label, reason: commit.error.message })
84
+ continue
85
+ }
86
+ if ((commit.status ?? 1) !== 0) {
87
+ failed.push({
88
+ label,
89
+ reason: commit.stderr?.trim() || `git commit exited with code ${commit.status ?? 1}`,
90
+ })
91
+ continue
92
+ }
93
+
94
+ const push = spawnSync('git', ['push'], {
95
+ cwd: repoDir,
96
+ encoding: 'utf8',
97
+ env: process.env,
98
+ })
99
+ if (push.error) {
100
+ failed.push({ label, reason: push.error.message })
101
+ continue
102
+ }
103
+ if ((push.status ?? 1) !== 0) {
104
+ failed.push({
105
+ label,
106
+ reason: push.stderr?.trim() || `git push exited with code ${push.status ?? 1}`,
107
+ })
108
+ continue
109
+ }
110
+
111
+ if (commit.stdout?.trim()) {
112
+ console.log(commit.stdout.trim())
113
+ }
114
+ committed.push({ label, branch })
115
+ }
116
+
117
+ console.log(`\n${logPrefix} Summary`)
118
+ console.log(`${logPrefix} commit message: ${commitMessage}`)
119
+ console.log(`${logPrefix} repositories=${repos.length}`)
120
+
121
+ if (committed.length > 0) {
122
+ console.log(`${logPrefix} committed and pushed (${committed.length}):`)
123
+ for (const item of committed) {
124
+ console.log(` - ${item.label} (${item.branch})`)
125
+ }
126
+ }
127
+
128
+ if (skipped.length > 0) {
129
+ console.log(`${logPrefix} skipped — no changes (${skipped.length}):`)
130
+ for (const label of skipped) {
131
+ console.log(` - ${label}`)
132
+ }
133
+ }
134
+
135
+ if (failed.length > 0) {
136
+ console.log(`${logPrefix} failed (${failed.length}):`)
137
+ for (const item of failed) {
138
+ console.log(` - ${item.label}: ${item.reason}`)
139
+ }
140
+ }
141
+
142
+ if (repos.length === 0) {
143
+ console.log(`${logPrefix} No repositories to process.`)
144
+ }
145
+
146
+ return failed.length > 0 ? 1 : 0
147
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Discard tracked git changes helpers for fleet maintainer scripts.
3
+ */
4
+
5
+ import { spawnSync } from 'node:child_process'
6
+
7
+ import { formatRepoLabel } from './fleet-repo-picker.mjs'
8
+ import { getCurrentGitBranch, hasTrackedGitChanges } from './git-fleet-common.mjs'
9
+
10
+ /**
11
+ * @param {{
12
+ * repos: string[]
13
+ * projectsRoot: string
14
+ * dryRun?: boolean
15
+ * logPrefix?: string
16
+ * }} options
17
+ * @returns {number}
18
+ */
19
+ export function runFleetGitDiscard(options) {
20
+ const { repos, projectsRoot, dryRun = false, logPrefix = '[fleet-git-discard]' } = options
21
+
22
+ /** @type {{ label: string, branch: string }[]} */
23
+ const discarded = []
24
+ /** @type {string[]} */
25
+ const skipped = []
26
+ /** @type {{ label: string, reason: string }[]} */
27
+ const failed = []
28
+
29
+ for (const repoDir of repos) {
30
+ const label = formatRepoLabel(repoDir, projectsRoot)
31
+ const branch = getCurrentGitBranch(repoDir)
32
+
33
+ if (branch === null) {
34
+ failed.push({ label, reason: 'could not determine current branch' })
35
+ continue
36
+ }
37
+
38
+ if (!hasTrackedGitChanges(repoDir)) {
39
+ skipped.push(label)
40
+ continue
41
+ }
42
+
43
+ if (dryRun) {
44
+ console.log(`${logPrefix} DRY RUN ${label}: git restore --source=HEAD --staged --worktree . (${branch})`)
45
+ discarded.push({ label, branch })
46
+ continue
47
+ }
48
+
49
+ console.log(`\n${logPrefix} ${label} (${branch})`)
50
+ const restore = spawnSync('git', ['restore', '--source=HEAD', '--staged', '--worktree', '.'], {
51
+ cwd: repoDir,
52
+ encoding: 'utf8',
53
+ env: process.env,
54
+ })
55
+
56
+ if (restore.error) {
57
+ failed.push({ label, reason: restore.error.message })
58
+ continue
59
+ }
60
+ if ((restore.status ?? 1) !== 0) {
61
+ failed.push({
62
+ label,
63
+ reason: restore.stderr?.trim() || `git restore exited with code ${restore.status ?? 1}`,
64
+ })
65
+ continue
66
+ }
67
+
68
+ discarded.push({ label, branch })
69
+ }
70
+
71
+ console.log(`\n${logPrefix} Summary`)
72
+ console.log(`${logPrefix} repositories=${repos.length}`)
73
+
74
+ if (discarded.length > 0) {
75
+ console.log(`${logPrefix} discarded tracked changes (${discarded.length}):`)
76
+ for (const item of discarded) {
77
+ console.log(` - ${item.label} (${item.branch})`)
78
+ }
79
+ }
80
+
81
+ if (skipped.length > 0) {
82
+ console.log(`${logPrefix} skipped — no tracked changes (${skipped.length}):`)
83
+ for (const label of skipped) {
84
+ console.log(` - ${label}`)
85
+ }
86
+ }
87
+
88
+ if (failed.length > 0) {
89
+ console.log(`${logPrefix} failed (${failed.length}):`)
90
+ for (const item of failed) {
91
+ console.log(` - ${item.label}: ${item.reason}`)
92
+ }
93
+ }
94
+
95
+ if (repos.length === 0) {
96
+ console.log(`${logPrefix} No repositories to process.`)
97
+ }
98
+
99
+ return failed.length > 0 ? 1 : 0
100
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Shared git helpers for fleet maintainer scripts.
3
+ */
4
+
5
+ import { spawnSync } from 'node:child_process'
6
+
7
+ /**
8
+ * @param {string} repoDir
9
+ * @returns {string | null}
10
+ */
11
+ export function getCurrentGitBranch(repoDir) {
12
+ const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
13
+ cwd: repoDir,
14
+ encoding: 'utf8',
15
+ env: process.env,
16
+ })
17
+ if ((result.status ?? 1) !== 0) {
18
+ return null
19
+ }
20
+ const branch = result.stdout.trim()
21
+ if (!branch || branch === 'HEAD') {
22
+ return null
23
+ }
24
+ return branch
25
+ }
26
+
27
+ /**
28
+ * @param {string} repoDir
29
+ * @returns {boolean}
30
+ */
31
+ export function hasGitWorkingTreeChanges(repoDir) {
32
+ const result = spawnSync('git', ['status', '--porcelain'], {
33
+ cwd: repoDir,
34
+ encoding: 'utf8',
35
+ env: process.env,
36
+ })
37
+ if ((result.status ?? 1) !== 0) {
38
+ return false
39
+ }
40
+ return result.stdout.trim().length > 0
41
+ }
42
+
43
+ /**
44
+ * @param {string} repoDir
45
+ * @returns {boolean}
46
+ */
47
+ export function hasTrackedGitChanges(repoDir) {
48
+ const result = spawnSync('git', ['status', '--porcelain', '--untracked-files=no'], {
49
+ cwd: repoDir,
50
+ encoding: 'utf8',
51
+ env: process.env,
52
+ })
53
+ if ((result.status ?? 1) !== 0) {
54
+ return false
55
+ }
56
+ return result.stdout.trim().length > 0
57
+ }
58
+
59
+ /**
60
+ * @param {string[]} cliArgs
61
+ * @returns {string | null}
62
+ */
63
+ export function parseCommitMessageArg(cliArgs) {
64
+ const messageArg = cliArgs.find((arg) => arg.startsWith('--message='))
65
+ if (messageArg) {
66
+ return messageArg.slice('--message='.length)
67
+ }
68
+ const mIndex = cliArgs.indexOf('-m')
69
+ if (mIndex !== -1 && cliArgs[mIndex + 1] && !cliArgs[mIndex + 1].startsWith('-')) {
70
+ return cliArgs[mIndex + 1]
71
+ }
72
+ return null
73
+ }
74
+
75
+ /**
76
+ * @param {string[]} cliArgs
77
+ * @returns {string[]}
78
+ */
79
+ export function filterCommitPushPositionalArgs(cliArgs) {
80
+ const filtered = []
81
+ for (let i = 0; i < cliArgs.length; i++) {
82
+ const arg = cliArgs[i]
83
+ if (arg === '-m') {
84
+ i += 1
85
+ continue
86
+ }
87
+ if (arg.startsWith('--message=')) {
88
+ continue
89
+ }
90
+ filtered.push(arg)
91
+ }
92
+ return filtered
93
+ }
94
+
95
+ /**
96
+ * @param {string[]} cliArgs
97
+ * @param {string} logPrefix
98
+ * @returns {Promise<string>}
99
+ */
100
+ export async function resolveCommitMessage(cliArgs, logPrefix) {
101
+ const fromArg = parseCommitMessageArg(cliArgs)
102
+ if (fromArg?.trim()) {
103
+ return fromArg.trim()
104
+ }
105
+ if (process.stdin.isTTY) {
106
+ try {
107
+ const inquirerInput = await import('@inquirer/input')
108
+ const message = await inquirerInput.default({
109
+ message: 'Commit message',
110
+ validate: (value) => (value.trim().length > 0 ? true : 'Commit message is required'),
111
+ })
112
+ return message.trim()
113
+ } catch {
114
+ console.error(`${logPrefix} Could not load @inquirer/input for commit message prompt.`)
115
+ process.exit(1)
116
+ }
117
+ }
118
+ console.error(`${logPrefix} Commit message required: --message="..." or -m "..."`)
119
+ process.exit(1)
120
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Git pull helpers for fleet maintainer scripts.
3
+ */
4
+
5
+ import { spawnSync } from 'node:child_process'
6
+
7
+ import { formatRepoLabel } from './fleet-repo-picker.mjs'
8
+ import { getCurrentGitBranch } from './git-fleet-common.mjs'
9
+
10
+ /**
11
+ * @param {{
12
+ * repos: string[]
13
+ * projectsRoot: string
14
+ * dryRun?: boolean
15
+ * logPrefix?: string
16
+ * }} options
17
+ * @returns {number} Exit code (0 = success, 1 = any failure)
18
+ */
19
+ export function runFleetGitPull(options) {
20
+ const { repos, projectsRoot, dryRun = false, logPrefix = '[fleet-git-pull]' } = options
21
+
22
+ /** @type {{ label: string, branch: string }[]} */
23
+ const pulled = []
24
+ /** @type {{ label: string, reason: string }[]} */
25
+ const failed = []
26
+
27
+ for (const repoDir of repos) {
28
+ const label = formatRepoLabel(repoDir, projectsRoot)
29
+ const branch = getCurrentGitBranch(repoDir)
30
+
31
+ if (branch === null) {
32
+ failed.push({ label, reason: 'could not determine current branch' })
33
+ continue
34
+ }
35
+
36
+ if (dryRun) {
37
+ console.log(`${logPrefix} DRY RUN ${label}: git pull (on ${branch})`)
38
+ pulled.push({ label, branch })
39
+ continue
40
+ }
41
+
42
+ console.log(`\n${logPrefix} ${label} (${branch})`)
43
+ const pull = spawnSync('git', ['pull'], {
44
+ cwd: repoDir,
45
+ encoding: 'utf8',
46
+ env: process.env,
47
+ })
48
+
49
+ if (pull.error) {
50
+ failed.push({ label, reason: pull.error.message })
51
+ continue
52
+ }
53
+
54
+ if ((pull.status ?? 1) !== 0) {
55
+ const stderr = pull.stderr?.trim()
56
+ const stdout = pull.stdout?.trim()
57
+ const detail = stderr || stdout || `git pull exited with code ${pull.status ?? 1}`
58
+ failed.push({ label, reason: detail })
59
+ continue
60
+ }
61
+
62
+ if (pull.stdout?.trim()) {
63
+ console.log(pull.stdout.trim())
64
+ }
65
+ pulled.push({ label, branch })
66
+ }
67
+
68
+ console.log(`\n${logPrefix} Summary`)
69
+ console.log(`${logPrefix} repositories=${repos.length}`)
70
+
71
+ if (pulled.length > 0) {
72
+ console.log(`${logPrefix} pulled (${pulled.length}):`)
73
+ for (const item of pulled) {
74
+ console.log(` - ${item.label} (${item.branch})`)
75
+ }
76
+ }
77
+
78
+ if (failed.length > 0) {
79
+ console.log(`${logPrefix} failed (${failed.length}):`)
80
+ for (const item of failed) {
81
+ console.log(` - ${item.label}: ${item.reason}`)
82
+ }
83
+ }
84
+
85
+ if (repos.length === 0) {
86
+ console.log(`${logPrefix} No repositories to process.`)
87
+ }
88
+
89
+ return failed.length > 0 ? 1 : 0
90
+ }