@wyxos/zephyr 0.2.17 → 0.2.19

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.
@@ -0,0 +1,91 @@
1
+ import process from 'node:process'
2
+ import { runCommand, runCommandCapture } from './command.mjs'
3
+
4
+ export async function getCurrentBranch(rootDir = process.cwd(), { method = 'rev-parse' } = {}) {
5
+ if (method === 'show-current') {
6
+ const { stdout } = await runCommandCapture('git', ['branch', '--show-current'], { cwd: rootDir })
7
+ return stdout.trim() || null
8
+ }
9
+
10
+ const { stdout } = await runCommandCapture('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: rootDir })
11
+ return stdout.trim() || null
12
+ }
13
+
14
+ export async function getUpstreamRef(rootDir = process.cwd()) {
15
+ try {
16
+ const { stdout } = await runCommandCapture(
17
+ 'git',
18
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'],
19
+ { cwd: rootDir }
20
+ )
21
+
22
+ return stdout.trim() || null
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ export async function ensureUpToDateWithUpstream({
29
+ branch,
30
+ upstreamRef,
31
+ rootDir = process.cwd(),
32
+ logStep = null,
33
+ logWarning = null
34
+ }) {
35
+ if (!upstreamRef) {
36
+ logWarning?.(`Branch ${branch} has no upstream configured; skipping ahead/behind checks.`)
37
+ return
38
+ }
39
+
40
+ const [remoteName, ...branchParts] = upstreamRef.split('/')
41
+ const remoteBranch = branchParts.join('/')
42
+
43
+ if (remoteName && remoteBranch) {
44
+ logStep?.(`Fetching latest updates from ${remoteName}/${remoteBranch}...`)
45
+ try {
46
+ await runCommand('git', ['fetch', remoteName, remoteBranch], { cwd: rootDir, stdio: 'ignore' })
47
+ } catch (error) {
48
+ throw new Error(`Failed to fetch ${upstreamRef}: ${error.message}`)
49
+ }
50
+ }
51
+
52
+ const aheadResult = await runCommandCapture('git', ['rev-list', '--count', `${upstreamRef}..HEAD`], {
53
+ cwd: rootDir
54
+ })
55
+ const behindResult = await runCommandCapture('git', ['rev-list', '--count', `HEAD..${upstreamRef}`], {
56
+ cwd: rootDir
57
+ })
58
+
59
+ const ahead = Number.parseInt(aheadResult.stdout || '0', 10)
60
+ const behind = Number.parseInt(behindResult.stdout || '0', 10)
61
+
62
+ if (Number.isFinite(behind) && behind > 0) {
63
+ if (remoteName && remoteBranch) {
64
+ logStep?.(`Fast-forwarding ${branch} with ${upstreamRef}...`)
65
+
66
+ try {
67
+ await runCommand('git', ['pull', '--ff-only', remoteName, remoteBranch], { cwd: rootDir, stdio: 'ignore' })
68
+ } catch (error) {
69
+ throw new Error(
70
+ `Unable to fast-forward ${branch} with ${upstreamRef}. Resolve conflicts manually, then rerun.\n${error.message}`
71
+ )
72
+ }
73
+
74
+ await ensureUpToDateWithUpstream({ branch, upstreamRef, rootDir, logStep, logWarning })
75
+ return
76
+ }
77
+
78
+ throw new Error(
79
+ `Branch ${branch} is behind ${upstreamRef} by ${behind} commit${behind === 1 ? '' : 's'}. Pull or rebase first.`
80
+ )
81
+ }
82
+
83
+ if (Number.isFinite(ahead) && ahead > 0) {
84
+ logWarning?.(`Branch ${branch} is ahead of ${upstreamRef} by ${ahead} commit${ahead === 1 ? '' : 's'}.`)
85
+ }
86
+ }
87
+
88
+ export async function pushQuiet(rootDir = process.cwd(), args = []) {
89
+ await runCommandCapture('git', ['push', ...args], { cwd: rootDir })
90
+ }
91
+
@@ -0,0 +1,6 @@
1
+ import crypto from 'node:crypto'
2
+
3
+ export function generateId() {
4
+ return crypto.randomBytes(8).toString('hex')
5
+ }
6
+
@@ -0,0 +1,76 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { ensureDirectory, getProjectConfigDir } from './paths.mjs'
4
+
5
+ let logFilePath = null
6
+
7
+ export async function getLogFilePath(rootDir) {
8
+ if (logFilePath) {
9
+ return logFilePath
10
+ }
11
+
12
+ const configDir = getProjectConfigDir(rootDir)
13
+ await ensureDirectory(configDir)
14
+
15
+ const now = new Date()
16
+ const dateStr = now.toISOString().replace(/:/g, '-').replace(/\..+/, '')
17
+ logFilePath = path.join(configDir, `${dateStr}.log`)
18
+
19
+ return logFilePath
20
+ }
21
+
22
+ export async function writeToLogFile(rootDir, message) {
23
+ const logPath = await getLogFilePath(rootDir)
24
+ const timestamp = new Date().toISOString()
25
+ await fs.appendFile(logPath, `${timestamp} - ${message}\n`)
26
+ }
27
+
28
+ export async function closeLogFile() {
29
+ logFilePath = null
30
+ }
31
+
32
+ export async function cleanupOldLogs(rootDir) {
33
+ const configDir = getProjectConfigDir(rootDir)
34
+
35
+ try {
36
+ const files = await fs.readdir(configDir)
37
+ const logFiles = files
38
+ .filter((file) => file.endsWith('.log'))
39
+ .map((file) => ({
40
+ name: file,
41
+ path: path.join(configDir, file)
42
+ }))
43
+
44
+ if (logFiles.length <= 3) {
45
+ return
46
+ }
47
+
48
+ const filesWithStats = await Promise.all(
49
+ logFiles.map(async (file) => {
50
+ const stats = await fs.stat(file.path)
51
+ return {
52
+ ...file,
53
+ mtime: stats.mtime
54
+ }
55
+ })
56
+ )
57
+
58
+ filesWithStats.sort((a, b) => b.mtime - a.mtime)
59
+
60
+ const filesToDelete = filesWithStats.slice(3)
61
+
62
+ for (const file of filesToDelete) {
63
+ try {
64
+ await fs.unlink(file.path)
65
+ } catch (_error) {
66
+ // Ignore errors when deleting old logs
67
+ }
68
+ }
69
+ } catch (error) {
70
+ // Ignore errors during log cleanup
71
+ if (error.code !== 'ENOENT') {
72
+ // Only log if it's not a "directory doesn't exist" error
73
+ }
74
+ }
75
+ }
76
+
@@ -0,0 +1,29 @@
1
+ import process from 'node:process'
2
+
3
+ export function writeStdoutLine(message = '') {
4
+ const text = message == null ? '' : String(message)
5
+ process.stdout.write(`${text}\n`)
6
+ }
7
+
8
+ export function writeStderrLine(message = '') {
9
+ const text = message == null ? '' : String(message)
10
+ process.stderr.write(`${text}\n`)
11
+ }
12
+
13
+ export function writeStderr(message = '') {
14
+ const text = message == null ? '' : String(message)
15
+ process.stderr.write(text)
16
+ if (text && !text.endsWith('\n')) {
17
+ process.stderr.write('\n')
18
+ }
19
+ }
20
+
21
+ export function createChalkLogger(chalk) {
22
+ return {
23
+ logProcessing: (message = '') => writeStdoutLine(chalk.yellow(message)),
24
+ logSuccess: (message = '') => writeStdoutLine(chalk.green(message)),
25
+ logWarning: (message = '') => writeStderrLine(chalk.yellow(message)),
26
+ logError: (message = '') => writeStderrLine(chalk.red(message))
27
+ }
28
+ }
29
+
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ export const PROJECT_CONFIG_DIR = '.zephyr'
5
+ export const PROJECT_CONFIG_FILE = 'config.json'
6
+ export const PROJECT_LOCK_FILE = 'deploy.lock'
7
+ export const PENDING_TASKS_FILE = 'pending-tasks.json'
8
+
9
+ export async function ensureDirectory(dirPath) {
10
+ await fs.mkdir(dirPath, { recursive: true })
11
+ }
12
+
13
+ export function getProjectConfigDir(rootDir) {
14
+ return path.join(rootDir, PROJECT_CONFIG_DIR)
15
+ }
16
+
17
+ export function getProjectConfigPath(rootDir) {
18
+ return path.join(rootDir, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILE)
19
+ }
20
+
21
+ export function getPendingTasksPath(rootDir) {
22
+ return path.join(getProjectConfigDir(rootDir), PENDING_TASKS_FILE)
23
+ }
24
+
25
+ export function getLockFilePath(rootDir) {
26
+ return path.join(getProjectConfigDir(rootDir), PROJECT_LOCK_FILE)
27
+ }
28
+
@@ -0,0 +1,23 @@
1
+ export function resolveRemotePath(projectPath, remoteHome) {
2
+ if (!projectPath) {
3
+ return projectPath
4
+ }
5
+
6
+ const sanitizedHome = remoteHome.replace(/\/+$/, '')
7
+
8
+ if (projectPath === '~') {
9
+ return sanitizedHome
10
+ }
11
+
12
+ if (projectPath.startsWith('~/')) {
13
+ const remainder = projectPath.slice(2)
14
+ return remainder ? `${sanitizedHome}/${remainder}` : sanitizedHome
15
+ }
16
+
17
+ if (projectPath.startsWith('/')) {
18
+ return projectPath
19
+ }
20
+
21
+ return `${sanitizedHome}/${projectPath}`
22
+ }
23
+
@@ -0,0 +1,96 @@
1
+ export function planLaravelDeploymentTasks({
2
+ branch,
3
+ isLaravel,
4
+ changedFiles,
5
+ horizonConfigured = false
6
+ }) {
7
+ const safeChangedFiles = Array.isArray(changedFiles) ? changedFiles : []
8
+
9
+ const shouldRunComposer =
10
+ isLaravel &&
11
+ safeChangedFiles.some(
12
+ (file) =>
13
+ file === 'composer.json' ||
14
+ file === 'composer.lock' ||
15
+ file.endsWith('/composer.json') ||
16
+ file.endsWith('/composer.lock')
17
+ )
18
+
19
+ const shouldRunMigrations =
20
+ isLaravel &&
21
+ safeChangedFiles.some((file) => file.startsWith('database/migrations/') && file.endsWith('.php'))
22
+
23
+ const hasPhpChanges = isLaravel && safeChangedFiles.some((file) => file.endsWith('.php'))
24
+
25
+ const shouldRunNpmInstall =
26
+ isLaravel &&
27
+ safeChangedFiles.some(
28
+ (file) =>
29
+ file === 'package.json' ||
30
+ file === 'package-lock.json' ||
31
+ file.endsWith('/package.json') ||
32
+ file.endsWith('/package-lock.json')
33
+ )
34
+
35
+ const hasFrontendChanges =
36
+ isLaravel &&
37
+ safeChangedFiles.some((file) =>
38
+ ['.vue', '.css', '.scss', '.js', '.ts', '.tsx', '.less'].some((ext) => file.endsWith(ext))
39
+ )
40
+
41
+ const shouldRunBuild = isLaravel && (hasFrontendChanges || shouldRunNpmInstall)
42
+ const shouldClearCaches = hasPhpChanges
43
+ const shouldRestartQueues = hasPhpChanges
44
+
45
+ const steps = [
46
+ {
47
+ label: `Pull latest changes for ${branch}`,
48
+ command: `git pull origin ${branch}`
49
+ }
50
+ ]
51
+
52
+ if (shouldRunComposer) {
53
+ steps.push({
54
+ label: 'Update Composer dependencies',
55
+ command: 'composer update --no-dev --no-interaction --prefer-dist'
56
+ })
57
+ }
58
+
59
+ if (shouldRunMigrations) {
60
+ steps.push({
61
+ label: 'Run database migrations',
62
+ command: 'php artisan migrate --force'
63
+ })
64
+ }
65
+
66
+ if (shouldRunNpmInstall) {
67
+ steps.push({
68
+ label: 'Install Node dependencies',
69
+ command: 'npm install'
70
+ })
71
+ }
72
+
73
+ if (shouldRunBuild) {
74
+ steps.push({
75
+ label: 'Compile frontend assets',
76
+ command: 'npm run build'
77
+ })
78
+ }
79
+
80
+ if (shouldClearCaches) {
81
+ steps.push({
82
+ label: 'Clear Laravel caches',
83
+ command: 'php artisan cache:clear && php artisan config:clear && php artisan view:clear'
84
+ })
85
+ }
86
+
87
+ if (shouldRestartQueues) {
88
+ steps.push({
89
+ label: horizonConfigured ? 'Restart Horizon workers' : 'Restart queue workers',
90
+ command: horizonConfigured ? 'php artisan horizon:terminate' : 'php artisan queue:restart'
91
+ })
92
+ }
93
+
94
+ return steps
95
+ }
96
+
@@ -97,7 +97,6 @@ async function reExecuteWithLatest(args) {
97
97
  return new Promise((resolve, reject) => {
98
98
  const child = spawn(command, npxArgs, {
99
99
  stdio: 'inherit',
100
- shell: IS_WINDOWS,
101
100
  env: {
102
101
  ...process.env,
103
102
  [ZEPHYR_SKIP_VERSION_CHECK_ENV]: '1'