@take-out/scripts 0.3.1 → 0.4.1-1775377981053

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": "@take-out/scripts",
3
- "version": "0.3.1",
3
+ "version": "0.4.1-1775377981053",
4
4
  "type": "module",
5
5
  "main": "./src/cmd.ts",
6
6
  "sideEffects": false,
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^0.8.2",
32
- "@take-out/helpers": "0.3.1",
33
- "@take-out/run": "0.3.1",
32
+ "@take-out/helpers": "0.4.1-1775377981053",
33
+ "@take-out/run": "0.4.1-1775377981053",
34
34
  "picocolors": "^1.1.1"
35
35
  }
36
36
  }
package/src/env-update.ts CHANGED
@@ -4,17 +4,9 @@ import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
 
6
6
  import { cmd } from './cmd'
7
- import { resolveDepVersion } from './helpers/resolve-dep-version'
8
7
 
9
8
  const PRESETS: Record<string, string[]> = {
10
- takeout: [
11
- '.github/workflows/*.yml',
12
- 'src/uncloud/*.yml',
13
- '.env*',
14
- 'src/env.ts',
15
- 'src/constants/env-server.ts',
16
- 'src/server/env-server.ts',
17
- ],
9
+ takeout: ['.github/workflows/*.yml', 'src/uncloud/*.yml', '.env*'],
18
10
  }
19
11
 
20
12
  const IGNORE_DIRS = new Set([
@@ -27,26 +19,45 @@ const IGNORE_DIRS = new Set([
27
19
  '.docker',
28
20
  ])
29
21
 
30
- await cmd`sync environment variables from package.json to matching files`
22
+ await cmd`sync environment variables from src/env.ts to matching files`
31
23
  .args('--preset string')
32
24
  .run(async ({ args }) => {
33
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
34
- const envVars = packageJson.env as Record<string, boolean | string>
25
+ // import env var definitions from src/env.ts (the single source of truth)
26
+ const envModule = await import(path.resolve('src/env.ts'))
27
+ const envVars = envModule.envVars as Record<string, string | symbol>
28
+ const envDeps = (envModule.envDeps || {}) as Record<string, string>
35
29
 
36
- if (!envVars || Array.isArray(envVars) || typeof envVars !== 'object') {
37
- console.error('No environment variables found in package.json')
30
+ if (!envVars || typeof envVars !== 'object') {
31
+ console.error('no envVars export found in src/env.ts')
38
32
  process.exit(1)
39
33
  }
40
34
 
41
- // resolve $dep: values
35
+ // the expected symbol from @take-out/env
36
+ const expectedSymbol = Symbol.for('take-out/env/expected')
37
+
38
+ // resolve dep versions from package.json dependencies
39
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
42
40
  const depResolved: Record<string, string> = {}
43
- for (const [key, value] of Object.entries(envVars)) {
44
- const dep = resolveDepVersion(value, packageJson.dependencies)
45
- if (dep) {
46
- depResolved[key] = dep
41
+ for (const [key, depName] of Object.entries(envDeps)) {
42
+ const version = packageJson.dependencies?.[depName]?.replace(/^[\^~]/, '')
43
+ if (version) {
44
+ depResolved[key] = version
45
+ // add dep-resolved vars to envVars with resolved value
46
+ envVars[key] = version
47
47
  }
48
48
  }
49
49
 
50
+ // normalize envVars: expected symbol → true (required), string → string
51
+ // for compatibility with strategy functions
52
+ type EnvEntry = { key: string; required: boolean; defaultValue: string }
53
+ const entries: EnvEntry[] = Object.entries(envVars)
54
+ .map(([key, value]) => ({
55
+ key,
56
+ required: value === expectedSymbol,
57
+ defaultValue: typeof value === 'string' ? value : '',
58
+ }))
59
+ .sort((a, b) => a.key.localeCompare(b.key))
60
+
50
61
  // load .gitignore patterns
51
62
  const gitignorePatterns: string[] = []
52
63
  try {
@@ -102,13 +113,13 @@ await cmd`sync environment variables from package.json to matching files`
102
113
  )
103
114
 
104
115
  function walk(dir: string) {
105
- let entries: fs.Dirent[]
116
+ let dirEntries: fs.Dirent[]
106
117
  try {
107
- entries = fs.readdirSync(dir, { withFileTypes: true })
118
+ dirEntries = fs.readdirSync(dir, { withFileTypes: true })
108
119
  } catch {
109
120
  return
110
121
  }
111
- for (const entry of entries) {
122
+ for (const entry of dirEntries) {
112
123
  const full = dir === '.' ? entry.name : `${dir}/${entry.name}`
113
124
  if (isRecursive && isIgnored(full)) continue
114
125
  if (entry.isDirectory()) {
@@ -156,37 +167,23 @@ await cmd`sync environment variables from package.json to matching files`
156
167
  const markerEnd = '🔒 end - this is generated by "bun env:update"'
157
168
  const yamlStart = `# ${markerStart}`
158
169
  const yamlEnd = `# ${markerEnd}`
159
- const jsStart = `// ${markerStart}`
160
- const jsEnd = `// ${markerEnd}`
161
-
162
170
  // .env auto-generated section markers
163
171
  const dotenvSectionStart = '# ---- BEGIN AUTO-GENERATED (DO NOT EDIT) ----'
164
172
  const dotenvSectionEnd = '# ---- END AUTO-GENERATED ----'
165
173
 
166
- type Strategy =
167
- | 'yaml-markers'
168
- | 'js-markers'
169
- | 'dotenv-section'
170
- | 'dotenv-inline'
171
- | 'ts-inline'
174
+ type Strategy = 'yaml-markers' | 'dotenv-section' | 'dotenv-inline'
172
175
 
173
176
  function detectStrategy(filePath: string, content: string): Strategy | null {
174
177
  if (content.includes(yamlStart) && content.includes(yamlEnd)) {
175
178
  return 'yaml-markers'
176
179
  }
177
- if (content.includes(jsStart) && content.includes(jsEnd)) {
178
- return 'js-markers'
179
- }
180
180
  const basename = path.basename(filePath)
181
181
  if (basename === '.env') {
182
182
  return 'dotenv-section'
183
183
  }
184
- if (/^\.env/.test(basename)) {
184
+ if (/^\.env/.test(basename) && Object.keys(depResolved).length > 0) {
185
185
  return 'dotenv-inline'
186
186
  }
187
- if (filePath.endsWith('.ts') && Object.keys(depResolved).length > 0) {
188
- return 'ts-inline'
189
- }
190
187
  return null
191
188
  }
192
189
 
@@ -219,10 +216,10 @@ await cmd`sync environment variables from package.json to matching files`
219
216
  const isMap = /x-[\w-]+:.*&[\w-]+/s.test(beforeMarker)
220
217
  const isGithubActions = filePath.includes('.github/workflows')
221
218
 
222
- const envLines = Object.entries(envVars)
223
- .map(([key, value]) => {
224
- const dep = resolveDepVersion(value, packageJson.dependencies)
225
- const defaultVal = dep || (typeof value === 'string' ? value : '')
219
+ const envLines = entries
220
+ .map(({ key, defaultValue }) => {
221
+ const dep = depResolved[key]
222
+ const val = dep || defaultValue
226
223
 
227
224
  if (isGithubActions) {
228
225
  return dep
@@ -231,40 +228,23 @@ await cmd`sync environment variables from package.json to matching files`
231
228
  }
232
229
 
233
230
  return isMap
234
- ? `${indent}${key}: \${${key}:-${defaultVal}}`
235
- : `${indent}- ${key}=\${${key}:-${defaultVal}}`
231
+ ? `${indent}${key}: \${${key}:-${val}}`
232
+ : `${indent}- ${key}=\${${key}:-${val}}`
236
233
  })
237
234
  .join('\n')
238
235
 
239
236
  return replaceMarkerSection(content, yamlStart, yamlEnd, envLines)
240
237
  }
241
238
 
242
- function applyJsMarkers(_filePath: string, content: string): string {
243
- const envExports = Object.entries(envVars)
244
- .map(([key, value]) => {
245
- if (typeof value === 'string' && value.startsWith('$dep:')) {
246
- return `export const ${key} = ensureEnv('${key}')`
247
- }
248
- return `export const ${key} = ensureEnv('${key}'${typeof value === 'string' ? `, ${JSON.stringify(value)}` : ''})`
249
- })
250
- .join('\n')
251
-
252
- return replaceMarkerSection(content, jsStart, jsEnd, envExports)
253
- }
254
-
255
239
  function applyDotenvSection(_filePath: string, content: string): string {
256
240
  const lines: string[] = [dotenvSectionStart]
257
241
 
258
- for (const [key, value] of Object.entries(envVars)) {
259
- const dep = resolveDepVersion(value, packageJson.dependencies)
242
+ for (const { key, required, defaultValue } of entries) {
243
+ const dep = depResolved[key]
260
244
  if (dep) {
261
245
  lines.push(`${key}=${dep}`)
262
- } else if (typeof value === 'string' && value.startsWith('$dep:')) {
263
- console.warn(
264
- `could not resolve dependency version for ${value.slice('$dep:'.length)}`,
265
- )
266
- } else if (typeof value === 'string' && value !== '') {
267
- lines.push(`${key}=${value}`)
246
+ } else if (!required && defaultValue !== '') {
247
+ lines.push(`${key}=${defaultValue}`)
268
248
  }
269
249
  }
270
250
 
@@ -297,24 +277,10 @@ await cmd`sync environment variables from package.json to matching files`
297
277
  return result
298
278
  }
299
279
 
300
- function applyTsInline(_filePath: string, content: string): string {
301
- let result = content
302
- for (const [key, value] of Object.entries(depResolved)) {
303
- const re = new RegExp(`(${key}:\\s*)(['"])([^'"]+)\\2`, 'g')
304
- result = result.replace(re, (_match, prefix, quote, oldValue) => {
305
- if (oldValue !== value) return `${prefix}${quote}${value}${quote}`
306
- return _match
307
- })
308
- }
309
- return result
310
- }
311
-
312
280
  const strategies: Record<Strategy, (filePath: string, content: string) => string> = {
313
281
  'yaml-markers': applyYamlMarkers,
314
- 'js-markers': applyJsMarkers,
315
282
  'dotenv-section': applyDotenvSection,
316
283
  'dotenv-inline': applyDotenvInline,
317
- 'ts-inline': applyTsInline,
318
284
  }
319
285
 
320
286
  let updated = 0
@@ -56,3 +56,53 @@ export async function releaseDeployLock(
56
56
  // best-effort, lock will auto-expire
57
57
  }
58
58
  }
59
+
60
+ /**
61
+ * run `fn` with the deploy lock held, releasing on ANY exit path —
62
+ * normal return, thrown error, or process signal (SIGTERM from CI cancel,
63
+ * SIGINT from ctrl-c, SIGHUP from terminal close).
64
+ *
65
+ * prefer this over raw acquire/release: a bare try/finally does NOT catch
66
+ * signals, so a CI-cancelled deploy would leak the lock for 15 minutes and
67
+ * block the next run. this helper installs signal handlers that release
68
+ * before exit, then removes them once `fn` completes.
69
+ */
70
+ export async function withDeployLock<T>(
71
+ ssh: string,
72
+ fn: () => Promise<T>,
73
+ opts?: DeployLockOptions,
74
+ ): Promise<T> {
75
+ await acquireDeployLock(ssh, opts)
76
+
77
+ let released = false
78
+ const release = async () => {
79
+ if (released) return
80
+ released = true
81
+ await releaseDeployLock(ssh, opts)
82
+ }
83
+
84
+ // signal handlers — fire-and-forget release with a safety timeout so the
85
+ // process always exits even if the ssh release hangs
86
+ const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP']
87
+ const handlers = new Map<NodeJS.Signals, () => void>()
88
+ for (const sig of signals) {
89
+ const handler = () => {
90
+ const done = release().catch(() => {})
91
+ const timeout = new Promise<void>((r) => setTimeout(r, 5_000))
92
+ Promise.race([done, timeout]).finally(() => {
93
+ process.exit(sig === 'SIGINT' ? 130 : 143)
94
+ })
95
+ }
96
+ handlers.set(sig, handler)
97
+ process.once(sig, handler)
98
+ }
99
+
100
+ try {
101
+ return await fn()
102
+ } finally {
103
+ for (const [sig, handler] of handlers) {
104
+ process.off(sig, handler)
105
+ }
106
+ await release()
107
+ }
108
+ }
@@ -1,18 +1,17 @@
1
- import fs from 'node:fs'
2
1
  import { join } from 'node:path'
3
2
 
4
3
  export async function loadEnv(
5
4
  environment: 'development' | 'production' | 'dev-prod',
6
- options?: { optional?: string[]; envPath?: string },
5
+ options?: { optional?: string[] },
7
6
  ) {
8
7
  const { loadEnv: vxrnLoadEnv } = await import('vxrn/loadEnv')
9
8
 
10
9
  // loads env into process.env
11
10
  await vxrnLoadEnv(environment)
12
11
 
13
- const envPath = options?.envPath || resolveEnvServerPath()
14
-
15
- const Environment = await import(envPath)
12
+ // import src/env.ts to get the env config applied (side effect: populates process.env)
13
+ const envModule = await import(join(process.cwd(), 'src/env.ts'))
14
+ const Environment = envModule.server || {}
16
15
 
17
16
  // validate
18
17
  for (const key in Environment) {
@@ -26,15 +25,3 @@ export async function loadEnv(
26
25
 
27
26
  return Environment
28
27
  }
29
-
30
- function resolveEnvServerPath(): string {
31
- const candidates = ['src/constants/env-server.ts', 'src/server/env-server.ts']
32
- for (const candidate of candidates) {
33
- const full = join(process.cwd(), candidate)
34
- if (fs.existsSync(full)) {
35
- return full
36
- }
37
- }
38
- // fall back to first candidate for error message
39
- return join(process.cwd(), 'src/constants/env-server.ts')
40
- }
@@ -1,4 +1,3 @@
1
- import fs from 'node:fs'
2
1
  import { join } from 'node:path'
3
2
 
4
3
  import { loadEnv } from './env-load'
@@ -7,12 +6,10 @@ import { getDockerHost } from './get-docker-host'
7
6
  export async function getTestEnv() {
8
7
  const dockerHost = getDockerHost()
9
8
  const devEnv = await loadEnv('development')
10
- const envPath =
11
- ['src/constants/env-server', 'src/server/env-server']
12
- .map((p) => join(process.cwd(), p))
13
- .find((p) => fs.existsSync(p + '.ts')) ||
14
- join(process.cwd(), 'src/constants/env-server')
15
- const serverEnvFallback = await import(envPath)
9
+
10
+ // import src/env.ts for server env fallback values
11
+ const envModule = await import(join(process.cwd(), 'src/env.ts'))
12
+ const serverEnvFallback = envModule.server || {}
16
13
 
17
14
  // ports come from VITE_PORT_* in .env.development (loaded by loadEnv)
18
15
  const dbPort = process.env.VITE_PORT_POSTGRES || '5433'