@wyxos/zephyr 0.2.19 → 0.2.21

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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "A streamlined deployment tool for web applications with intelligent Laravel project detection",
5
5
  "type": "module",
6
- "main": "./src/main.mjs",
6
+ "main": "./src/index.mjs",
7
7
  "exports": {
8
- ".": "./src/main.mjs",
8
+ ".": "./src/index.mjs",
9
9
  "./ssh": "./src/ssh/index.mjs"
10
10
  },
11
11
  "bin": {
@@ -31,7 +31,7 @@ function isLocalPathOutsideRepo(depPath, rootDir) {
31
31
  // Check if resolved path is outside the repository root
32
32
  // Use path.relative to check if the path goes outside
33
33
  const relative = path.relative(normalizedRoot, normalizedResolved)
34
-
34
+
35
35
  // If relative path starts with .., it's outside the repo
36
36
  // Also check if the resolved path doesn't start with the root + separator (for absolute paths)
37
37
  return relative.startsWith('..') || !normalizedResolved.startsWith(normalizedRoot + path.sep)
@@ -256,31 +256,8 @@ async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
256
256
  return false
257
257
  }
258
258
 
259
- const statusBefore = await getGitStatus(rootDir)
260
-
261
- // Avoid accidentally committing unrelated staged changes
262
- if (hasStagedChanges(statusBefore)) {
263
- if (logFn) {
264
- logFn('Staged changes detected. Skipping auto-commit of dependency updates.')
265
- }
266
- return false
267
- }
268
-
269
259
  const fileList = updatedFiles.map((f) => path.basename(f)).join(', ')
270
260
 
271
- const { shouldCommit } = await promptFn([
272
- {
273
- type: 'confirm',
274
- name: 'shouldCommit',
275
- message: `Commit dependency updates now? (${fileList})`,
276
- default: true
277
- }
278
- ])
279
-
280
- if (!shouldCommit) {
281
- return false
282
- }
283
-
284
261
  // Stage the updated files
285
262
  for (const file of updatedFiles) {
286
263
  try {
@@ -290,11 +267,6 @@ async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
290
267
  }
291
268
  }
292
269
 
293
- const newStatus = await getGitStatus(rootDir)
294
- if (!hasStagedChanges(newStatus)) {
295
- return false
296
- }
297
-
298
270
  // Build commit message
299
271
  const commitMessage = `chore: update local file dependencies to online versions (${fileList})`
300
272
 
@@ -302,7 +274,7 @@ async function commitDependencyUpdates(rootDir, updatedFiles, promptFn, logFn) {
302
274
  logFn('Committing dependency updates...')
303
275
  }
304
276
 
305
- await runCommand('git', ['commit', '-m', commitMessage], { cwd: rootDir })
277
+ await runCommand('git', ['commit', '-m', commitMessage, '--', ...updatedFiles], { cwd: rootDir })
306
278
 
307
279
  if (logFn) {
308
280
  logFn('Dependency updates committed.')
@@ -371,6 +343,9 @@ async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
371
343
  throw new Error('Release cancelled: local file dependencies must be updated before release.')
372
344
  }
373
345
 
346
+ // If we cannot commit the update, do not proceed (otherwise release-node will fail later with a dirty tree).
347
+ // We allow users to opt-in to committing together with existing staged changes via prompt.
348
+
374
349
  // Track which files were updated
375
350
  const updatedFiles = new Set()
376
351
 
@@ -437,7 +412,12 @@ async function validateLocalDependencies(rootDir, promptFn, logFn = null) {
437
412
 
438
413
  // Commit the changes if any files were updated
439
414
  if (updatedFiles.size > 0) {
440
- await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn)
415
+ const committed = await commitDependencyUpdates(rootDir, Array.from(updatedFiles), promptFn, logFn)
416
+ if (!committed) {
417
+ throw new Error(
418
+ 'Release cancelled: dependency updates were applied but were not committed. Commit/stash your changes and rerun.'
419
+ )
420
+ }
441
421
  }
442
422
  }
443
423
 
package/src/index.mjs ADDED
@@ -0,0 +1,91 @@
1
+ import chalk from 'chalk'
2
+ import inquirer from 'inquirer'
3
+ import { NodeSSH } from 'node-ssh'
4
+
5
+ import { createChalkLogger } from './utils/output.mjs'
6
+ import { runCommand as runCommandBase, runCommandCapture as runCommandCaptureBase } from './utils/command.mjs'
7
+ import { createLocalCommandRunners } from './runtime/local-command.mjs'
8
+ import { createRunPrompt } from './runtime/prompt.mjs'
9
+ import { createSshClientFactory } from './runtime/ssh-client.mjs'
10
+ import { generateId } from './utils/id.mjs'
11
+
12
+ import { loadServers as loadServersImpl, saveServers } from './config/servers.mjs'
13
+ import { loadProjectConfig as loadProjectConfigImpl, saveProjectConfig } from './config/project.mjs'
14
+ import * as configFlow from './utils/config-flow.mjs'
15
+ import * as sshKeys from './ssh/keys.mjs'
16
+ import { writeToLogFile } from './utils/log-file.mjs'
17
+
18
+ export { main, runRemoteTasks } from './main.mjs'
19
+ export { connectToServer, executeRemoteCommand, readRemoteFile, downloadRemoteFile, deleteRemoteFile } from './ssh/index.mjs'
20
+
21
+ const { logProcessing, logSuccess, logWarning, logError } = createChalkLogger(chalk)
22
+ const runPrompt = createRunPrompt({ inquirer })
23
+ const { runCommand, runCommandCapture } = createLocalCommandRunners({
24
+ runCommandBase,
25
+ runCommandCaptureBase
26
+ })
27
+
28
+ // Keep this aligned with main's test injection behavior
29
+ const createSshClient = createSshClientFactory({ NodeSSH })
30
+
31
+ export { logProcessing, logSuccess, logWarning, logError, runCommand, runCommandCapture, writeToLogFile, createSshClient }
32
+
33
+ export async function loadServers() {
34
+ return await loadServersImpl({ logSuccess, logWarning })
35
+ }
36
+
37
+ export async function loadProjectConfig(rootDir, servers) {
38
+ return await loadProjectConfigImpl(rootDir, servers, { logSuccess, logWarning })
39
+ }
40
+
41
+ export function defaultProjectPath(currentDir) {
42
+ return configFlow.defaultProjectPath(currentDir)
43
+ }
44
+
45
+ export async function listGitBranches(currentDir) {
46
+ return await configFlow.listGitBranches(currentDir, { runCommandCapture, logWarning })
47
+ }
48
+
49
+ export async function promptSshDetails(currentDir, existing = {}) {
50
+ return await sshKeys.promptSshDetails(currentDir, existing, { runPrompt })
51
+ }
52
+
53
+ export async function promptServerDetails(existingServers = []) {
54
+ return await configFlow.promptServerDetails(existingServers, { runPrompt, generateId })
55
+ }
56
+
57
+ export async function selectServer(servers) {
58
+ return await configFlow.selectServer(servers, {
59
+ runPrompt,
60
+ logProcessing,
61
+ logSuccess,
62
+ saveServers,
63
+ promptServerDetails
64
+ })
65
+ }
66
+
67
+ export async function promptAppDetails(currentDir, existing = {}) {
68
+ return await configFlow.promptAppDetails(currentDir, existing, {
69
+ runPrompt,
70
+ listGitBranches,
71
+ defaultProjectPath,
72
+ promptSshDetails
73
+ })
74
+ }
75
+
76
+ export async function selectApp(projectConfig, server, currentDir) {
77
+ return await configFlow.selectApp(projectConfig, server, currentDir, {
78
+ runPrompt,
79
+ logWarning,
80
+ logProcessing,
81
+ logSuccess,
82
+ saveProjectConfig,
83
+ generateId,
84
+ promptAppDetails
85
+ })
86
+ }
87
+
88
+ export async function selectPreset(projectConfig, servers) {
89
+ return await configFlow.selectPreset(projectConfig, servers, { runPrompt })
90
+ }
91
+
@@ -126,13 +126,25 @@ async function runTests(skipTests, pkg, rootDir = process.cwd()) {
126
126
  logStep('Running test suite...')
127
127
 
128
128
  try {
129
+ const testRunScript = pkg?.scripts?.['test:run'] ?? ''
130
+ const testScript = pkg?.scripts?.test ?? ''
131
+ const usesNodeTest = (script) => /\bnode\b.*\s--test\b/.test(script)
132
+
129
133
  // Prefer test:run if available, otherwise use test with --run and --reporter flags
130
134
  if (hasScript(pkg, 'test:run')) {
131
- // Pass reporter flag to test:run script
132
- await runCommand('npm', ['run', 'test:run', '--', '--reporter=dot'], { cwd: rootDir })
135
+ if (usesNodeTest(testRunScript)) {
136
+ await runCommand('npm', ['run', 'test:run'], { cwd: rootDir })
137
+ } else {
138
+ // Pass reporter flag to test:run script
139
+ await runCommand('npm', ['run', 'test:run', '--', '--reporter=dot'], { cwd: rootDir })
140
+ }
133
141
  } else {
134
- // For test script, pass --run and --reporter flags (works with vitest)
135
- await runCommand('npm', ['test', '--', '--run', '--reporter=dot'], { cwd: rootDir })
142
+ if (usesNodeTest(testScript)) {
143
+ await runCommand('npm', ['test'], { cwd: rootDir })
144
+ } else {
145
+ // For test script, pass --run and --reporter flags (works with vitest)
146
+ await runCommand('npm', ['test', '--', '--run', '--reporter=dot'], { cwd: rootDir })
147
+ }
136
148
  }
137
149
 
138
150
  logSuccess('Tests passed.')
@@ -217,7 +229,13 @@ async function runLibBuild(skipBuild, pkg, rootDir = process.cwd()) {
217
229
  return hasLibChanges
218
230
  }
219
231
 
220
- async function ensureNpmAuth(rootDir = process.cwd()) {
232
+ async function ensureNpmAuth(pkg, rootDir = process.cwd()) {
233
+ const isPrivate = pkg?.publishConfig?.access === 'restricted'
234
+ if (isPrivate) {
235
+ logStep('Skipping npm authentication check (package is private/restricted).')
236
+ return
237
+ }
238
+
221
239
  logStep('Confirming npm authentication...')
222
240
  try {
223
241
  const result = await runCommand('npm', ['whoami'], { capture: true, cwd: rootDir })
@@ -452,7 +470,7 @@ export async function releaseNode() {
452
470
  await runLint(skipLint, pkg, rootDir)
453
471
  await runTests(skipTests, pkg, rootDir)
454
472
  await runLibBuild(skipBuild, pkg, rootDir)
455
- await ensureNpmAuth(rootDir)
473
+ await ensureNpmAuth(pkg, rootDir)
456
474
 
457
475
  const updatedPkg = await bumpVersion(releaseType, rootDir)
458
476
  await runBuild(skipBuild, updatedPkg, rootDir)
package/src/ssh/ssh.mjs CHANGED
@@ -13,6 +13,11 @@ const { logProcessing, logSuccess, logWarning, logError } = createChalkLogger(ch
13
13
 
14
14
  const createSshClient = createSshClientFactory({ NodeSSH })
15
15
 
16
+ function normalizeRemotePath(value) {
17
+ if (value == null) return value
18
+ return String(value).replace(/\\/g, '/')
19
+ }
20
+
16
21
  export async function connectToServer(config) {
17
22
  const ssh = createSshClient()
18
23
  const sshUser = config.sshUser || os.userInfo().username
@@ -64,10 +69,11 @@ export async function executeRemoteCommand(ssh, label, command, options = {}) {
64
69
  }
65
70
 
66
71
  export async function readRemoteFile(ssh, filePath, remoteCwd) {
67
- const escapedPath = filePath.replace(/'/g, "'\\''")
72
+ const normalizedPath = normalizeRemotePath(filePath)
73
+ const escapedPath = normalizedPath.replace(/'/g, "'\\''")
68
74
  const command = `cat '${escapedPath}'`
69
75
 
70
- const result = await ssh.execCommand(command, { cwd: remoteCwd })
76
+ const result = await ssh.execCommand(command, { cwd: normalizeRemotePath(remoteCwd) })
71
77
 
72
78
  if (result.code !== 0) {
73
79
  throw new Error(`Failed to read remote file ${filePath}: ${result.stderr}`)
@@ -77,9 +83,11 @@ export async function readRemoteFile(ssh, filePath, remoteCwd) {
77
83
  }
78
84
 
79
85
  export async function downloadRemoteFile(ssh, remotePath, localPath, remoteCwd) {
80
- const absoluteRemotePath = remotePath.startsWith('/')
81
- ? remotePath
82
- : `${remoteCwd}/${remotePath}`
86
+ const normalizedRemotePath = normalizeRemotePath(remotePath)
87
+ const normalizedCwd = normalizeRemotePath(remoteCwd)
88
+ const absoluteRemotePath = normalizedRemotePath.startsWith('/')
89
+ ? normalizedRemotePath
90
+ : `${normalizedCwd}/${normalizedRemotePath}`
83
91
 
84
92
  logProcessing(`Downloading ${absoluteRemotePath} to ${localPath}...`)
85
93
 
@@ -104,16 +112,18 @@ export async function downloadRemoteFile(ssh, remotePath, localPath, remoteCwd)
104
112
  }
105
113
 
106
114
  export async function deleteRemoteFile(ssh, remotePath, remoteCwd) {
107
- const absoluteRemotePath = remotePath.startsWith('/')
108
- ? remotePath
109
- : `${remoteCwd}/${remotePath}`
115
+ const normalizedRemotePath = normalizeRemotePath(remotePath)
116
+ const normalizedCwd = normalizeRemotePath(remoteCwd)
117
+ const absoluteRemotePath = normalizedRemotePath.startsWith('/')
118
+ ? normalizedRemotePath
119
+ : `${normalizedCwd}/${normalizedRemotePath}`
110
120
 
111
121
  const escapedPath = absoluteRemotePath.replace(/'/g, "'\\''")
112
122
  const command = `rm -f '${escapedPath}'`
113
123
 
114
124
  logProcessing(`Deleting remote file: ${absoluteRemotePath}...`)
115
125
 
116
- const result = await ssh.execCommand(command, { cwd: remoteCwd })
126
+ const result = await ssh.execCommand(command, { cwd: normalizedCwd })
117
127
 
118
128
  if (result.code !== 0 && result.code !== 1) {
119
129
  logWarning(`Failed to delete remote file ${absoluteRemotePath}: ${result.stderr}`)