@mono-labs/cli 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/bin/haste.js +2 -0
- package/index.js +3 -0
- package/lib/app.js +28 -0
- package/lib/commands/build/index.js +71 -0
- package/lib/commands/build-process/index.js +412 -0
- package/lib/commands/deploy/index.js +44 -0
- package/lib/commands/destroy.js +27 -0
- package/lib/commands/dev/dev-editor.js +265 -0
- package/lib/commands/dev/index.js +22 -0
- package/lib/commands/dev/ngrok.js +88 -0
- package/lib/commands/generate/generateSeed.js +224 -0
- package/lib/commands/generate/index.js +30 -0
- package/lib/commands/init/index.js +37 -0
- package/lib/commands/loadFromRoot.js +38 -0
- package/lib/commands/prune/index.js +12 -0
- package/lib/commands/prune/prune.js +48 -0
- package/lib/commands/reset.js +31 -0
- package/lib/commands/seed/import.js +31 -0
- package/lib/commands/seed/index.js +12 -0
- package/lib/commands/squash/index.js +8 -0
- package/lib/commands/squash/squash.js +150 -0
- package/lib/commands/submit/index.js +38 -0
- package/lib/commands/test/index.js +251 -0
- package/lib/commands/update/eas.js +39 -0
- package/lib/commands/update/index.js +92 -0
- package/lib/config.js +4 -0
- package/lib/index.js +19 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/v0.js +3 -0
- package/lib/migrations/v1.js +4 -0
- package/package.json +43 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
|
|
3
|
+
import { program } from '../../app.js'
|
|
4
|
+
import { pruneRepo } from './prune.js'
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.command('prune')
|
|
8
|
+
.description('Prune local branches that are not on origin')
|
|
9
|
+
|
|
10
|
+
.action(() => {
|
|
11
|
+
pruneRepo()
|
|
12
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
|
|
3
|
+
const log = (...args) => console.log(...args)
|
|
4
|
+
const err = (...args) => console.error(...args)
|
|
5
|
+
export function pruneRepo() {
|
|
6
|
+
try {
|
|
7
|
+
// Fetch and prune remote branches
|
|
8
|
+
log('Fetching latest branch data from origin...')
|
|
9
|
+
execSync('git fetch --prune', { stdio: 'inherit' })
|
|
10
|
+
|
|
11
|
+
// Get local branches (trim whitespace)
|
|
12
|
+
const localBranches = execSync("git branch --format '%(refname:short)'")
|
|
13
|
+
.toString()
|
|
14
|
+
.trim()
|
|
15
|
+
.split('\n')
|
|
16
|
+
.map((branch) => branch.trim().replaceAll("'", ''))
|
|
17
|
+
|
|
18
|
+
// Get remote branches (remove "origin/" prefix)
|
|
19
|
+
const remoteBranches = execSync('git branch -r')
|
|
20
|
+
.toString()
|
|
21
|
+
.trim()
|
|
22
|
+
.split('\n')
|
|
23
|
+
.map((branch) => branch.replace(/^\s*origin\//, '').trim())
|
|
24
|
+
|
|
25
|
+
// Find local branches that are NOT in remote branches
|
|
26
|
+
const branchesToDelete = localBranches.filter((branch) => !remoteBranches.includes(branch))
|
|
27
|
+
if (branchesToDelete.length === 0) {
|
|
28
|
+
log('No local branches to delete.')
|
|
29
|
+
process.exit(0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Delete untracked local branches
|
|
33
|
+
log('Deleting local branches that are not on origin...')
|
|
34
|
+
branchesToDelete.forEach((branch) => {
|
|
35
|
+
log(`Attempting to delete: ${branch}`)
|
|
36
|
+
try {
|
|
37
|
+
execSync(`git branch -D ${branch}`, { stdio: 'inherit' })
|
|
38
|
+
log(`Deleted: ${branch}`)
|
|
39
|
+
} catch (error) {
|
|
40
|
+
error(`Failed to delete branch ${branch}:`, error.message)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
log('Cleanup complete!')
|
|
45
|
+
} catch (error) {
|
|
46
|
+
err('An error occurred:', error.message)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
|
|
3
|
+
import { program } from '../app.js'
|
|
4
|
+
|
|
5
|
+
function createChild(command) {
|
|
6
|
+
const child = spawn(command, {
|
|
7
|
+
stdio: ['inherit', 'pipe', 'pipe'], // Read from terminal, but capture output
|
|
8
|
+
shell: true,
|
|
9
|
+
env: {
|
|
10
|
+
...process.env,
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
child.stdout.on('data', (data) => {
|
|
15
|
+
process.stdout.write(data) // pipe to main stdout
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
child.stderr.on('data', (data) => {
|
|
19
|
+
process.stderr.write(data) // pipe errors
|
|
20
|
+
})
|
|
21
|
+
child.on('message', (data) => {
|
|
22
|
+
console.log(`Message from child process: ${data}`)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
program
|
|
26
|
+
.command('reset')
|
|
27
|
+
.description('Execute eas build command')
|
|
28
|
+
.option('-s, --soft', 'Pull from live')
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
createChild(`git reset ${options.soft ? '--soft' : '--mixed '} HEAD~1`)
|
|
31
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
export function importAllDynamoBatches(folderPath, useRemote = false) {
|
|
6
|
+
const files = fs
|
|
7
|
+
.readdirSync(folderPath)
|
|
8
|
+
.filter((file) => file.startsWith('dynamodb-seed-') && file.endsWith('.json'))
|
|
9
|
+
|
|
10
|
+
files.sort() // Optional: ensures files run in order
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const fullPath = path.resolve(path.join(folderPath, file))
|
|
14
|
+
console.log(`📝 Importing: ${fullPath}`)
|
|
15
|
+
console.log('Using remote DynamoDB:', useRemote)
|
|
16
|
+
const baseCommand = useRemote
|
|
17
|
+
? `aws dynamodb batch-write-item --request-items file://${fullPath}`
|
|
18
|
+
: `aws dynamodb batch-write-item --endpoint-url http://localhost:8000 --request-items file://${fullPath}`
|
|
19
|
+
try {
|
|
20
|
+
console.log('baseCommand:', baseCommand)
|
|
21
|
+
console.log(`Executing command: ${baseCommand}`)
|
|
22
|
+
execSync(baseCommand, {
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
})
|
|
25
|
+
console.log(`✅ Successfully imported ${file}\n`)
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error(`❌ Error with ${file}:`, err.message)
|
|
28
|
+
break // or continue if you want to skip failed files
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
|
|
3
|
+
import { program } from '../../app.js'
|
|
4
|
+
import { importAllDynamoBatches } from './import.js'
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.command('seed')
|
|
8
|
+
.description('Execute eas build command')
|
|
9
|
+
.option('-p, --live', 'Pull from live')
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
importAllDynamoBatches('./docker/seed', options.live)
|
|
12
|
+
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync, spawnSync } from 'child_process'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import inquirer from 'inquirer'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
|
|
8
|
+
async function maybeEditFile() {
|
|
9
|
+
const { shouldEdit } = await inquirer.prompt([
|
|
10
|
+
{
|
|
11
|
+
type: 'confirm',
|
|
12
|
+
name: 'shouldEdit',
|
|
13
|
+
message: 'Do you want to edit the commit message in your Git editor?',
|
|
14
|
+
default: true,
|
|
15
|
+
},
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
if (shouldEdit) {
|
|
19
|
+
execSync('git commit --amend')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function confirmDangerousSquash() {
|
|
24
|
+
const { shouldEdit } = await inquirer.prompt([
|
|
25
|
+
{
|
|
26
|
+
type: 'confirm',
|
|
27
|
+
name: 'shouldEdit',
|
|
28
|
+
message:
|
|
29
|
+
"You're about to squash on a previously squashed commit. You will need to manually modify the commit message. Do you want to proceed?",
|
|
30
|
+
default: true,
|
|
31
|
+
},
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
return shouldEdit
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function squash() {
|
|
38
|
+
const status = execSync('git status --porcelain').toString()
|
|
39
|
+
if (status.trim() !== '') {
|
|
40
|
+
console.error('You have unstaged changes. Please commit or stash them before proceeding.')
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
const log = execSync(`git log --reverse --pretty=format:"%h %s" -n ${20}`).toString()
|
|
44
|
+
console.log('Log:\n', log)
|
|
45
|
+
|
|
46
|
+
const commits = log
|
|
47
|
+
.split('\n')
|
|
48
|
+
.reverse()
|
|
49
|
+
.map((line, idx) => {
|
|
50
|
+
console.log('message', line.slice(8).split('- '))
|
|
51
|
+
return {
|
|
52
|
+
name: `${idx} [${line.slice(0, 7)}] : ${line.slice(8)}`,
|
|
53
|
+
value: idx, // Just the commit message
|
|
54
|
+
message: line.slice(8).split('- ').join('\n- '),
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
console.log(commits)
|
|
58
|
+
|
|
59
|
+
const { count } = await inquirer.prompt([
|
|
60
|
+
{
|
|
61
|
+
type: 'list',
|
|
62
|
+
name: 'count',
|
|
63
|
+
message: 'Select commit to squash down into:',
|
|
64
|
+
choices: commits,
|
|
65
|
+
},
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
if (count === 0) {
|
|
69
|
+
console.log('You may not squash only one commit.')
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
let totalMerged = 0
|
|
73
|
+
const commitMessageGen = commits
|
|
74
|
+
.slice(0, count + 1)
|
|
75
|
+
.map((msg) => {
|
|
76
|
+
return msg.message
|
|
77
|
+
})
|
|
78
|
+
.map((msg) => `${msg}`)
|
|
79
|
+
.join('\n')
|
|
80
|
+
|
|
81
|
+
let shouldContinue = true
|
|
82
|
+
const match = commitMessageGen.match(/Merging (\d+)\s+into singular commit/)
|
|
83
|
+
if (match) {
|
|
84
|
+
shouldContinue = await confirmDangerousSquash()
|
|
85
|
+
console.log('Unable to merge with merged commits')
|
|
86
|
+
if (!shouldContinue) return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('Total merged:', totalMerged)
|
|
90
|
+
console.log(count + 1 + totalMerged)
|
|
91
|
+
const commitMessage = `Merging ${count + 1 + totalMerged} into singular commit\n${commitMessageGen}`
|
|
92
|
+
|
|
93
|
+
console.log(`Commit message:\n${commitMessage}`)
|
|
94
|
+
|
|
95
|
+
// Write message to temp file
|
|
96
|
+
const tempMessagePath = path.join(os.tmpdir(), 'git_squash_message.txt')
|
|
97
|
+
fs.writeFileSync(tempMessagePath, commitMessage)
|
|
98
|
+
|
|
99
|
+
if (shouldContinue) {
|
|
100
|
+
const { confirmSquash } = await inquirer.prompt([
|
|
101
|
+
{
|
|
102
|
+
type: 'confirm',
|
|
103
|
+
name: 'confirmSquash',
|
|
104
|
+
message: 'Do you want to squash these commits?',
|
|
105
|
+
default: true, // or false if you want "No" as the default
|
|
106
|
+
},
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
shouldContinue = confirmSquash
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!shouldContinue) return
|
|
113
|
+
// proceed with
|
|
114
|
+
|
|
115
|
+
// GIT_EDITOR override for rebase message
|
|
116
|
+
const isWin = process.platform === 'win32'
|
|
117
|
+
const editorScript = path.join(os.tmpdir(), isWin ? 'git-editor.bat' : 'git-editor.sh')
|
|
118
|
+
const messagePath = path.join(os.tmpdir(), 'commit-message.txt')
|
|
119
|
+
|
|
120
|
+
fs.writeFileSync(messagePath, commitMessage)
|
|
121
|
+
|
|
122
|
+
if (isWin) {
|
|
123
|
+
fs.writeFileSync(editorScript, `@echo off\r\ncopy /Y "${messagePath}" %1 > nul\r\n`)
|
|
124
|
+
} else {
|
|
125
|
+
fs.writeFileSync(editorScript, `#!/bin/sh\ncat "${messagePath}" > "$1"\n`)
|
|
126
|
+
fs.chmodSync(editorScript, '755')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('Starting interactive rebase...')
|
|
130
|
+
|
|
131
|
+
spawnSync('git', ['reset', '--soft', `HEAD~${count + 1}`], { stdio: 'inherit' })
|
|
132
|
+
|
|
133
|
+
spawnSync('git', ['commit', '-m', commitMessage], { stdio: 'inherit' })
|
|
134
|
+
|
|
135
|
+
const rebaseResult = spawnSync('git', ['rebase', `HEAD~${count}`], {
|
|
136
|
+
stdio: 'inherit',
|
|
137
|
+
env: { ...process.env, GIT_EDITOR: editorScript },
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
console.log(`Rebase result: ${rebaseResult.status}`)
|
|
141
|
+
|
|
142
|
+
if (rebaseResult.status !== 0) {
|
|
143
|
+
console.error('Rebase failed. Resolve conflicts and try again.')
|
|
144
|
+
process.exit(rebaseResult.status)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await maybeEditFile()
|
|
148
|
+
|
|
149
|
+
console.log('✅ Rebase complete.')
|
|
150
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
|
|
3
|
+
import { program } from '../../app.js'
|
|
4
|
+
import { generateEnvValues } from '../../app.js'
|
|
5
|
+
import { STAGING_URL } from '../../config.js'
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.command('submit')
|
|
9
|
+
.description('Execute eas build command')
|
|
10
|
+
.option('--android', 'Build to target preview profile')
|
|
11
|
+
.option('--ios', 'Build to target production profile')
|
|
12
|
+
.action((str, options) => {
|
|
13
|
+
console.log('its me')
|
|
14
|
+
console.log('test')
|
|
15
|
+
//console.log(options);
|
|
16
|
+
console.log(str)
|
|
17
|
+
|
|
18
|
+
let envObj = generateEnvValues(true, '', false)
|
|
19
|
+
|
|
20
|
+
envObj.EXPO_PUBLIC_API_URL = `${STAGING_URL}`
|
|
21
|
+
envObj.EXPO_FORCE_PROD = 'true'
|
|
22
|
+
envObj.EAS_BUILD_PROFILE = 'production'
|
|
23
|
+
|
|
24
|
+
const command = `workspace app eas submit ${str.android ? `--platform android` : `--platform ios`}`
|
|
25
|
+
console.log('Running command:', command)
|
|
26
|
+
const child = spawn('yarn', [command], {
|
|
27
|
+
stdio: 'inherit',
|
|
28
|
+
shell: true, // required if using shell-style commands or cross-platform support
|
|
29
|
+
env: {
|
|
30
|
+
...envObj,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
child.on('exit', (code) => {
|
|
35
|
+
console.log(`Process exited with code ${code}`)
|
|
36
|
+
process.exit(code ?? 0)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { program } from '../../app.js'
|
|
4
|
+
import { generateEnvValues } from '../../app.js'
|
|
5
|
+
import { STAGING_URL } from '../../config.js'
|
|
6
|
+
import {
|
|
7
|
+
getHasteConfig,
|
|
8
|
+
getHasteFiles,
|
|
9
|
+
getRootDirectory,
|
|
10
|
+
getRootHasteJson,
|
|
11
|
+
getRootJson,
|
|
12
|
+
} from '../loadFromRoot.js'
|
|
13
|
+
|
|
14
|
+
console.log('getRootDirectory', getRootDirectory())
|
|
15
|
+
console.log('rootJson', getRootJson())
|
|
16
|
+
const objHaste = getRootHasteJson()
|
|
17
|
+
const files = getHasteConfig()
|
|
18
|
+
|
|
19
|
+
console.log('Haste files', files)
|
|
20
|
+
|
|
21
|
+
const fileKeys = Object.keys(files)
|
|
22
|
+
|
|
23
|
+
fileKeys.forEach((key) => {
|
|
24
|
+
const commandName = key
|
|
25
|
+
|
|
26
|
+
console.log('Haste file key', commandName, files[key])
|
|
27
|
+
program
|
|
28
|
+
.command(commandName)
|
|
29
|
+
.description('Execute eas build command')
|
|
30
|
+
.option('--android', 'Build to target preview profile')
|
|
31
|
+
.option('--ios', 'Build to target production profile')
|
|
32
|
+
.option('--stage', 'Set environment to staging')
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
const configObject = files[commandName] || {}
|
|
35
|
+
await runHasteCommand(configObject, options)
|
|
36
|
+
|
|
37
|
+
// const devConfig = configObject.environments ? configObject.environments.dev : {}
|
|
38
|
+
// const productionConfig = configObject.environments ? configObject.environments.production : {}
|
|
39
|
+
// let envObj = {}
|
|
40
|
+
// if (options.stage) {
|
|
41
|
+
// envObj = { ...devConfig }
|
|
42
|
+
// } else {
|
|
43
|
+
// envObj = { ...productionConfig }
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
// // You can decide what happens when each flag is passed.
|
|
47
|
+
// // Example: run `eas build` with different profiles.
|
|
48
|
+
// if (options.android) {
|
|
49
|
+
// console.log('→ Building Android (preview profile)…')
|
|
50
|
+
// //run('eas', ['build', '--platform', 'android', '--profile', 'preview'])
|
|
51
|
+
// } else if (options.ios) {
|
|
52
|
+
// console.log('→ Building iOS (production profile)…')
|
|
53
|
+
// //run('eas', ['build', '--platform', 'ios', '--profile', 'production'])
|
|
54
|
+
// } else {
|
|
55
|
+
// // console.log('No target specified. Use --android or --ios.')
|
|
56
|
+
// // program.help()
|
|
57
|
+
// console.log('objHaste.actions', configObject)
|
|
58
|
+
// const preactions = configObject['preactions'] || []
|
|
59
|
+
// const actions = configObject.actions || []
|
|
60
|
+
// console.log('preactions', preactions)
|
|
61
|
+
// for (const item of preactions) {
|
|
62
|
+
// console.log(`→ Running pre-action: ${item}`)
|
|
63
|
+
// await run(item, [], envObj)
|
|
64
|
+
// }
|
|
65
|
+
// for (const item of actions) {
|
|
66
|
+
// console.log(`→ Running action: ${item}`)
|
|
67
|
+
// run(item, [], envObj)
|
|
68
|
+
// }
|
|
69
|
+
// }
|
|
70
|
+
// })
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// const commandName = 'test'
|
|
75
|
+
// program
|
|
76
|
+
// .command(commandName)
|
|
77
|
+
// .description('Execute eas build command')
|
|
78
|
+
// .option('--android', 'Build to target preview profile')
|
|
79
|
+
// .option('--ios', 'Build to target production profile')
|
|
80
|
+
// .option('--stage', 'Set environment to staging')
|
|
81
|
+
// .action((options) => {
|
|
82
|
+
// const configObject = objHaste[commandName] || {}
|
|
83
|
+
|
|
84
|
+
// const devConfig = configObject.environments ? configObject.environments.dev : {}
|
|
85
|
+
// const productionConfig = configObject.environments ? configObject.environments.production : {}
|
|
86
|
+
// let envObj = {}
|
|
87
|
+
// if (options.stage) {
|
|
88
|
+
// envObj = { ...devConfig }
|
|
89
|
+
// } else {
|
|
90
|
+
// envObj = { ...productionConfig }
|
|
91
|
+
// }
|
|
92
|
+
|
|
93
|
+
// // You can decide what happens when each flag is passed.
|
|
94
|
+
// // Example: run `eas build` with different profiles.
|
|
95
|
+
// if (options.android) {
|
|
96
|
+
// console.log('→ Building Android (preview profile)…')
|
|
97
|
+
// //run('eas', ['build', '--platform', 'android', '--profile', 'preview'])
|
|
98
|
+
// } else if (options.ios) {
|
|
99
|
+
// console.log('→ Building iOS (production profile)…')
|
|
100
|
+
// //run('eas', ['build', '--platform', 'ios', '--profile', 'production'])
|
|
101
|
+
// } else {
|
|
102
|
+
// // console.log('No target specified. Use --android or --ios.')
|
|
103
|
+
// // program.help()
|
|
104
|
+
// console.log('objHaste.actions', objHaste)
|
|
105
|
+
// for (const item of configObject.actions) {
|
|
106
|
+
// console.log(`→ Running action: ${item}`)
|
|
107
|
+
// run(item, [], envObj)
|
|
108
|
+
// }
|
|
109
|
+
// }
|
|
110
|
+
// })
|
|
111
|
+
|
|
112
|
+
const totalClosedActions = 0
|
|
113
|
+
// Utility to spawn and pipe child output
|
|
114
|
+
async function run(cmd, args, envObj = {}, count = 1) {
|
|
115
|
+
const child = spawn(cmd, args, {
|
|
116
|
+
stdio: 'inherit',
|
|
117
|
+
shell: true,
|
|
118
|
+
env: {
|
|
119
|
+
...process.env,
|
|
120
|
+
...envObj,
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
const isLast = count === totalClosedActions + 1
|
|
124
|
+
|
|
125
|
+
const exitAction = () => {
|
|
126
|
+
totalClosedActions++
|
|
127
|
+
process.exit(code)
|
|
128
|
+
}
|
|
129
|
+
if (!isLast) {
|
|
130
|
+
child.on('exit', (code) => {
|
|
131
|
+
exitAction()
|
|
132
|
+
})
|
|
133
|
+
child.on('sigint', () => {
|
|
134
|
+
exitAction()
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
if (isLast) {
|
|
138
|
+
child.on('exit', (code) => {
|
|
139
|
+
if (count < totalClosedActions) exitAction()
|
|
140
|
+
})
|
|
141
|
+
child.on('sigint', () => {
|
|
142
|
+
if (count < totalClosedActions) {
|
|
143
|
+
exitAction()
|
|
144
|
+
process.exit(code)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Track background processes so we can kill them on exit
|
|
151
|
+
const bgChildren = new Set()
|
|
152
|
+
|
|
153
|
+
/** Run a command and resolve when it exits (attach stdio so you can see output). */
|
|
154
|
+
function runForeground(cmd, envObj = {}) {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
// run the whole string via the shell so quotes/pipes work
|
|
157
|
+
const child = spawn(cmd, {
|
|
158
|
+
shell: true,
|
|
159
|
+
stdio: 'inherit',
|
|
160
|
+
env: { ...process.env, ...envObj },
|
|
161
|
+
})
|
|
162
|
+
child.on('error', reject)
|
|
163
|
+
child.on('exit', (code, signal) => {
|
|
164
|
+
if (signal) return reject(new Error(`${cmd} exited via signal ${signal}`))
|
|
165
|
+
if (code === 0) return resolve()
|
|
166
|
+
reject(new Error(`${cmd} exited with code ${code}`))
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Start a command fully detached (no stdio). */
|
|
172
|
+
function runBackground(cmd, envObj = {}) {
|
|
173
|
+
const isWin = process.platform === 'win32'
|
|
174
|
+
|
|
175
|
+
const child = spawn(cmd, {
|
|
176
|
+
shell: true,
|
|
177
|
+
stdio: 'ignore', // no output in this terminal
|
|
178
|
+
env: { ...process.env, ...envObj },
|
|
179
|
+
// On POSIX, detach so we can signal the whole group; on Windows, DON'T.
|
|
180
|
+
detached: !isWin,
|
|
181
|
+
windowsHide: isWin, // prevent new console window on Windows
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// Only unref on POSIX when detached; on Windows keep it referenced.
|
|
185
|
+
if (!isWin) child.unref()
|
|
186
|
+
|
|
187
|
+
bgChildren.add(child)
|
|
188
|
+
child.on('exit', () => bgChildren.delete(child))
|
|
189
|
+
child.on('error', () => bgChildren.delete(child))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Kill all background children (called on SIGINT/SIGTERM or when foreground ends). */
|
|
193
|
+
function killAllBackground() {
|
|
194
|
+
for (const child of Array.from(bgChildren)) {
|
|
195
|
+
try {
|
|
196
|
+
if (process.platform === 'win32') {
|
|
197
|
+
spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { shell: true, stdio: 'ignore' })
|
|
198
|
+
} else {
|
|
199
|
+
process.kill(-child.pid, 'SIGTERM') // whole group
|
|
200
|
+
}
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
bgChildren.clear()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
process.on('SIGINT', () => {
|
|
207
|
+
console.log('\nSIGINT')
|
|
208
|
+
killAllBackground()
|
|
209
|
+
process.exit(130)
|
|
210
|
+
})
|
|
211
|
+
process.on('SIGTERM', () => {
|
|
212
|
+
killAllBackground()
|
|
213
|
+
process.exit(143)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
/** In your commander .action handler */
|
|
217
|
+
async function runHasteCommand(configObject, options) {
|
|
218
|
+
const devConfig = configObject.environments?.dev ?? {}
|
|
219
|
+
const productionConfig = configObject.environments?.production ?? {}
|
|
220
|
+
const envObj = options.stage ? devConfig : productionConfig
|
|
221
|
+
|
|
222
|
+
const preactions = configObject.preactions ?? []
|
|
223
|
+
const actions = configObject.actions ?? []
|
|
224
|
+
|
|
225
|
+
// 1) Run preactions SEQUENTIALLY (each waits for previous to finish)
|
|
226
|
+
for (const cmd of preactions) {
|
|
227
|
+
console.log(`→ preaction: ${cmd}`)
|
|
228
|
+
await runForeground(cmd, envObj)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 2) Run actions: background all but the last; attach to the last
|
|
232
|
+
if (actions.length === 0) return
|
|
233
|
+
|
|
234
|
+
const bg = actions.slice(0, -1)
|
|
235
|
+
const fg = actions[actions.length - 1]
|
|
236
|
+
|
|
237
|
+
for (const cmd of bg) {
|
|
238
|
+
console.log(`→ background action: ${cmd}`)
|
|
239
|
+
runBackground(cmd, envObj)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log(`→ foreground action (attached): ${fg}`)
|
|
243
|
+
console.log('envObj', envObj)
|
|
244
|
+
try {
|
|
245
|
+
console.log('envObj', envObj)
|
|
246
|
+
await runForeground(fg, envObj)
|
|
247
|
+
} finally {
|
|
248
|
+
// When the foreground ends, clean up background processes too (optional)
|
|
249
|
+
killAllBackground()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
|
|
3
|
+
export function getEASChannels() {
|
|
4
|
+
const channelsData = execSync(
|
|
5
|
+
'yarn eas channel:list --non-interactive --json',
|
|
6
|
+
{ stdio: ['pipe', 'pipe', 'ignore'] }, // Ignore stderr
|
|
7
|
+
).toString()
|
|
8
|
+
|
|
9
|
+
// Extract valid JSON from any extra noise
|
|
10
|
+
const jsonStart = channelsData.indexOf('[')
|
|
11
|
+
const jsonEnd = channelsData.lastIndexOf(']') + 1
|
|
12
|
+
|
|
13
|
+
if (jsonStart === -1 || jsonEnd === -1) {
|
|
14
|
+
throw new Error('JSON output not found in command output')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const jsonSlice = channelsData.slice(jsonStart, jsonEnd)
|
|
18
|
+
const channels = JSON.parse(jsonSlice)
|
|
19
|
+
return channels
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getEASBranches() {
|
|
23
|
+
const channelsData = execSync(
|
|
24
|
+
'yarn eas branch:list --non-interactive --json',
|
|
25
|
+
{ stdio: ['pipe', 'pipe', 'ignore'] }, // Ignore stderr
|
|
26
|
+
).toString()
|
|
27
|
+
|
|
28
|
+
// Extract valid JSON from any extra noise
|
|
29
|
+
const jsonStart = channelsData.indexOf('[')
|
|
30
|
+
const jsonEnd = channelsData.lastIndexOf(']') + 1
|
|
31
|
+
|
|
32
|
+
if (jsonStart === -1 || jsonEnd === -1) {
|
|
33
|
+
throw new Error('JSON output not found in command output')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const jsonSlice = channelsData.slice(jsonStart, jsonEnd)
|
|
37
|
+
const channels = JSON.parse(jsonSlice)
|
|
38
|
+
return channels
|
|
39
|
+
}
|