@wyxos/zephyr 0.3.3 → 0.3.4

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@wyxos/zephyr",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "A streamlined deployment tool for web applications with intelligent Laravel project detection",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -3,6 +3,113 @@ import {planLaravelDeploymentTasks} from './plan-laravel-deployment-tasks.mjs'
3
3
 
4
4
  const PRERENDERED_MAINTENANCE_VIEW = 'errors::503'
5
5
  const PRERENDERED_MAINTENANCE_FILE = 'resources/views/errors/503.blade.php'
6
+ const LARAVEL_WRITABLE_PATHS = [
7
+ 'bootstrap/cache',
8
+ 'storage/framework/cache',
9
+ 'storage/framework/views',
10
+ 'storage/framework/sessions'
11
+ ]
12
+
13
+ function escapeForSingleQuotes(value) {
14
+ return value.replace(/'/g, "'\\''")
15
+ }
16
+
17
+ function isGroupWritable(mode) {
18
+ if (typeof mode !== 'string' || mode.length < 2) {
19
+ return false
20
+ }
21
+
22
+ const groupDigit = mode.at(-2)
23
+ const parsed = Number.parseInt(groupDigit, 8)
24
+ return Number.isInteger(parsed) && (parsed & 2) === 2
25
+ }
26
+
27
+ function shouldInspectLaravelWritablePaths(steps = []) {
28
+ return steps.some((step) => step.label === 'Clear Laravel caches')
29
+ }
30
+
31
+ async function inspectLaravelWritablePath(ssh, remoteCwd, relativePath) {
32
+ const escapedPath = escapeForSingleQuotes(relativePath)
33
+ const command = [
34
+ `if [ ! -e '${escapedPath}' ]; then`,
35
+ ' printf "__MISSING__";',
36
+ 'else',
37
+ ` WRITABLE="no"; [ -w '${escapedPath}' ] && WRITABLE="yes";`,
38
+ ` OWNER=$(stat -c '%U' '${escapedPath}' 2>/dev/null || printf '?');`,
39
+ ` GROUP=$(stat -c '%G' '${escapedPath}' 2>/dev/null || printf '?');`,
40
+ ` MODE=$(stat -c '%a' '${escapedPath}' 2>/dev/null || printf '?');`,
41
+ ' printf "%s|%s|%s|%s" "$WRITABLE" "$OWNER" "$GROUP" "$MODE";',
42
+ 'fi'
43
+ ].join(' ')
44
+
45
+ const result = await ssh.execCommand(command, {cwd: remoteCwd})
46
+ const output = result.stdout.trim()
47
+
48
+ if (output === '__MISSING__') {
49
+ return {
50
+ path: relativePath,
51
+ exists: false,
52
+ writable: false,
53
+ owner: null,
54
+ group: null,
55
+ mode: null
56
+ }
57
+ }
58
+
59
+ const [writableFlag, owner = '?', group = '?', mode = '?'] = output.split('|')
60
+
61
+ return {
62
+ path: relativePath,
63
+ exists: true,
64
+ writable: writableFlag === 'yes',
65
+ owner,
66
+ group,
67
+ mode
68
+ }
69
+ }
70
+
71
+ async function validateLaravelWritablePaths({
72
+ ssh,
73
+ remoteCwd,
74
+ sshUser,
75
+ steps,
76
+ logProcessing,
77
+ logWarning
78
+ } = {}) {
79
+ if (!shouldInspectLaravelWritablePaths(steps)) {
80
+ return
81
+ }
82
+
83
+ logProcessing?.(`Checking Laravel writable directories for deploy user ${sshUser || 'current SSH user'}...`)
84
+
85
+ const inspections = []
86
+ for (const relativePath of LARAVEL_WRITABLE_PATHS) {
87
+ inspections.push(await inspectLaravelWritablePath(ssh, remoteCwd, relativePath))
88
+ }
89
+
90
+ const blockedPaths = inspections.filter((inspection) => inspection.exists && !inspection.writable)
91
+ if (blockedPaths.length > 0) {
92
+ const details = blockedPaths
93
+ .map((inspection) => ` - ${inspection.path} (owner ${inspection.owner}:${inspection.group}, mode ${inspection.mode})`)
94
+ .join('\n')
95
+
96
+ throw new Error(
97
+ 'Laravel cache-related deployment tasks cannot run because the SSH deploy user cannot write to required directories:\n' +
98
+ `${details}\n` +
99
+ 'Fix permissions before releasing. Typical fix:\n' +
100
+ 'sudo chown -R $USER:www-data bootstrap/cache storage/framework/cache storage/framework/views storage/framework/sessions\n' +
101
+ 'sudo chmod -R ug+rwX bootstrap/cache storage/framework/cache storage/framework/views storage/framework/sessions'
102
+ )
103
+ }
104
+
105
+ const riskyPaths = inspections.filter((inspection) => inspection.exists && inspection.writable && !isGroupWritable(inspection.mode))
106
+ for (const inspection of riskyPaths) {
107
+ logWarning?.(
108
+ `${inspection.path} is writable by the deploy user (${inspection.owner}:${inspection.group}, mode ${inspection.mode}), ` +
109
+ 'but it is not group-writable. Web-created cache files may cause later permission drift.'
110
+ )
111
+ }
112
+ }
6
113
 
7
114
  async function detectRemoteLaravelProject(ssh, remoteCwd) {
8
115
  const laravelCheck = await ssh.execCommand(
@@ -290,6 +397,15 @@ export async function buildRemoteDeploymentPlan({
290
397
  maintenanceUpCommand: maintenanceModePlan.upCommand
291
398
  })
292
399
 
400
+ await validateLaravelWritablePaths({
401
+ ssh,
402
+ remoteCwd,
403
+ sshUser: config.sshUser,
404
+ steps,
405
+ logProcessing,
406
+ logWarning
407
+ })
408
+
293
409
  const usefulSteps = steps.length > 1
294
410
  const pendingSnapshot = !usefulSteps
295
411
  ? null
@@ -320,4 +436,4 @@ export async function buildRemoteDeploymentPlan({
320
436
  usefulSteps,
321
437
  pendingSnapshot
322
438
  }
323
- }
439
+ }