@take-out/scripts 0.2.1 → 0.2.2
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 +3 -3
- package/src/env-update.ts +238 -103
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@take-out/scripts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/cmd.ts",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^0.8.2",
|
|
32
32
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
33
|
-
"@take-out/helpers": "0.2.
|
|
34
|
-
"@take-out/run": "0.2.
|
|
33
|
+
"@take-out/helpers": "0.2.2",
|
|
34
|
+
"@take-out/run": "0.2.2",
|
|
35
35
|
"picocolors": "^1.1.1"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
package/src/env-update.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
4
5
|
|
|
5
6
|
import { cmd } from './cmd'
|
|
6
7
|
import { resolveDepVersion } from './helpers/resolve-dep-version'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
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
|
+
],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const IGNORE_DIRS = new Set([
|
|
21
|
+
'node_modules',
|
|
22
|
+
'.git',
|
|
23
|
+
'dist',
|
|
24
|
+
'build',
|
|
25
|
+
'.next',
|
|
26
|
+
'.cache',
|
|
27
|
+
'.docker',
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
await cmd`sync environment variables from package.json to matching files`
|
|
31
|
+
.args('--preset string')
|
|
32
|
+
.run(async ({ args }) => {
|
|
10
33
|
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
|
|
11
34
|
const envVars = packageJson.env as Record<string, boolean | string>
|
|
12
35
|
|
|
@@ -15,75 +38,192 @@ await cmd`sync environment variables from package.json to CI and server configs`
|
|
|
15
38
|
process.exit(1)
|
|
16
39
|
}
|
|
17
40
|
|
|
41
|
+
// resolve $dep: values
|
|
42
|
+
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
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// load .gitignore patterns
|
|
51
|
+
const gitignorePatterns: string[] = []
|
|
52
|
+
try {
|
|
53
|
+
const gi = fs.readFileSync('.gitignore', 'utf-8')
|
|
54
|
+
for (const line of gi.split('\n')) {
|
|
55
|
+
const trimmed = line.trim()
|
|
56
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
57
|
+
gitignorePatterns.push(trimmed.replace(/\/$/, ''))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch {}
|
|
61
|
+
|
|
62
|
+
function isIgnored(filePath: string): boolean {
|
|
63
|
+
const parts = filePath.split('/')
|
|
64
|
+
for (const part of parts) {
|
|
65
|
+
if (IGNORE_DIRS.has(part)) return true
|
|
66
|
+
}
|
|
67
|
+
for (const pattern of gitignorePatterns) {
|
|
68
|
+
if (filePath.startsWith(pattern) || parts.some((p) => p === pattern)) {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// simple glob: supports *.ext, dir/*.ext, dir/**/*.ext, .prefix*
|
|
76
|
+
function expandPattern(pattern: string): string[] {
|
|
77
|
+
const results: string[] = []
|
|
78
|
+
|
|
79
|
+
if (!pattern.includes('*')) {
|
|
80
|
+
if (fs.existsSync(pattern)) results.push(pattern)
|
|
81
|
+
return results
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isRecursive = pattern.includes('**')
|
|
85
|
+
const parts = pattern.split('**/')
|
|
86
|
+
const baseDir = parts.length > 1 ? parts[0]!.replace(/\/$/, '') || '.' : '.'
|
|
87
|
+
const filePattern =
|
|
88
|
+
parts.length > 1
|
|
89
|
+
? parts[1]!
|
|
90
|
+
: pattern.includes('/')
|
|
91
|
+
? pattern.split('/').pop()!
|
|
92
|
+
: pattern
|
|
93
|
+
|
|
94
|
+
// for non-recursive patterns with a dir prefix like "src/uncloud/*.yml"
|
|
95
|
+
const nonRecursiveDir =
|
|
96
|
+
!isRecursive && pattern.includes('/')
|
|
97
|
+
? pattern.slice(0, pattern.lastIndexOf('/'))
|
|
98
|
+
: null
|
|
99
|
+
|
|
100
|
+
const matchRe = new RegExp(
|
|
101
|
+
'^' + filePattern.replace(/\./g, '\\.').replace(/\*/g, '[^/]*') + '$'
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
function walk(dir: string) {
|
|
105
|
+
let entries: fs.Dirent[]
|
|
106
|
+
try {
|
|
107
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
108
|
+
} catch {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const full = dir === '.' ? entry.name : `${dir}/${entry.name}`
|
|
113
|
+
if (isRecursive && isIgnored(full)) continue
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
if (isRecursive) walk(full)
|
|
116
|
+
} else if (matchRe.test(entry.name)) {
|
|
117
|
+
results.push(full)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
walk(nonRecursiveDir || baseDir)
|
|
123
|
+
return results
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// resolve file list: --preset > CLI args > package.json config > preset:takeout
|
|
127
|
+
let patterns: string[]
|
|
128
|
+
if (args.preset) {
|
|
129
|
+
const preset = PRESETS[args.preset]
|
|
130
|
+
if (!preset) {
|
|
131
|
+
console.error(
|
|
132
|
+
`Unknown preset: ${args.preset}\nAvailable: ${Object.keys(PRESETS).join(', ')}`
|
|
133
|
+
)
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
|
136
|
+
patterns = preset
|
|
137
|
+
} else if (args.rest.length) {
|
|
138
|
+
patterns = args.rest
|
|
139
|
+
} else {
|
|
140
|
+
patterns = packageJson.envUpdateFiles || PRESETS.takeout!
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// expand all patterns
|
|
144
|
+
const files: string[] = []
|
|
145
|
+
for (const p of patterns) {
|
|
146
|
+
files.push(...expandPattern(p))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// markers
|
|
18
150
|
const markerStart = '🔒 start - this is generated by "bun env:update"'
|
|
19
151
|
const markerEnd = '🔒 end - this is generated by "bun env:update"'
|
|
152
|
+
const yamlStart = `# ${markerStart}`
|
|
153
|
+
const yamlEnd = `# ${markerEnd}`
|
|
154
|
+
const jsStart = `// ${markerStart}`
|
|
155
|
+
const jsEnd = `// ${markerEnd}`
|
|
20
156
|
|
|
21
|
-
|
|
22
|
-
const yamlEndMarker = `# ${markerEnd}`
|
|
157
|
+
type Strategy = 'yaml-markers' | 'js-markers' | 'dotenv-inline' | 'ts-inline'
|
|
23
158
|
|
|
24
|
-
|
|
25
|
-
|
|
159
|
+
function detectStrategy(filePath: string, content: string): Strategy | null {
|
|
160
|
+
if (content.includes(yamlStart) && content.includes(yamlEnd)) {
|
|
161
|
+
return 'yaml-markers'
|
|
162
|
+
}
|
|
163
|
+
if (content.includes(jsStart) && content.includes(jsEnd)) {
|
|
164
|
+
return 'js-markers'
|
|
165
|
+
}
|
|
166
|
+
if (/^\.env/.test(path.basename(filePath))) {
|
|
167
|
+
return 'dotenv-inline'
|
|
168
|
+
}
|
|
169
|
+
if (filePath.endsWith('.ts') && Object.keys(depResolved).length > 0) {
|
|
170
|
+
return 'ts-inline'
|
|
171
|
+
}
|
|
172
|
+
return null
|
|
173
|
+
}
|
|
26
174
|
|
|
27
|
-
function
|
|
175
|
+
function escapeRegExp(s: string): string {
|
|
176
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function replaceMarkerSection(
|
|
28
180
|
content: string,
|
|
29
|
-
|
|
30
|
-
|
|
181
|
+
startMarker: string,
|
|
182
|
+
endMarker: string,
|
|
183
|
+
newSection: string
|
|
31
184
|
): string {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
185
|
+
const re = new RegExp(
|
|
186
|
+
`^([ \\t]*)${escapeRegExp(startMarker)}(.|\n)*?${escapeRegExp(endMarker)}`,
|
|
187
|
+
'gm'
|
|
35
188
|
)
|
|
189
|
+
return content.replace(re, (_match, indent) => {
|
|
190
|
+
return `${indent}${startMarker}\n${newSection}\n${indent}${endMarker}`
|
|
191
|
+
})
|
|
36
192
|
}
|
|
37
193
|
|
|
38
|
-
function
|
|
39
|
-
const
|
|
40
|
-
|
|
194
|
+
function applyYamlMarkers(filePath: string, content: string): string {
|
|
195
|
+
const markerMatch = content.match(
|
|
196
|
+
new RegExp(`^(\\s*)${escapeRegExp(yamlStart)}`, 'm')
|
|
197
|
+
)
|
|
198
|
+
const indent = markerMatch?.[1] || ' '
|
|
41
199
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
200
|
+
const beforeMarker = content.slice(0, content.indexOf(yamlStart))
|
|
201
|
+
const isMap = /x-[\w-]+:.*&[\w-]+/s.test(beforeMarker)
|
|
202
|
+
const isGithubActions = filePath.includes('.github/workflows')
|
|
45
203
|
|
|
46
|
-
const
|
|
47
|
-
const envSection = Object.entries(envVars)
|
|
204
|
+
const envLines = Object.entries(envVars)
|
|
48
205
|
.map(([key, value]) => {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
206
|
+
const dep = resolveDepVersion(value, packageJson.dependencies)
|
|
207
|
+
const defaultVal = dep || (typeof value === 'string' ? value : '')
|
|
208
|
+
|
|
209
|
+
if (isGithubActions) {
|
|
210
|
+
return dep
|
|
211
|
+
? `${indent}${key}: \${{ secrets.${key} || '${dep}' }}`
|
|
212
|
+
: `${indent}${key}: \${{ secrets.${key} }}`
|
|
52
213
|
}
|
|
53
|
-
|
|
214
|
+
|
|
215
|
+
return isMap
|
|
216
|
+
? `${indent}${key}: \${${key}:-${defaultVal}}`
|
|
217
|
+
: `${indent}- ${key}=\${${key}:-${defaultVal}}`
|
|
54
218
|
})
|
|
55
219
|
.join('\n')
|
|
56
220
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
fs.writeFileSync(deployYmlPath, newDeployYml, 'utf-8')
|
|
60
|
-
console.info('✅ Updated Github workflow')
|
|
221
|
+
return replaceMarkerSection(content, yamlStart, yamlEnd, envLines)
|
|
61
222
|
}
|
|
62
223
|
|
|
63
|
-
function
|
|
64
|
-
const candidates = ['src/constants/env-server.ts', 'src/server/env-server.ts']
|
|
65
|
-
let envServerPath = ''
|
|
66
|
-
let envServer = ''
|
|
67
|
-
|
|
68
|
-
for (const p of candidates) {
|
|
69
|
-
try {
|
|
70
|
-
envServer = fs.readFileSync(p, 'utf-8')
|
|
71
|
-
envServerPath = p
|
|
72
|
-
break
|
|
73
|
-
} catch {}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!envServerPath) {
|
|
77
|
-
throw new Error(`env-server.ts not found at ${candidates.join(' or ')}`)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!envServer.includes(jsStartMarker) || !envServer.includes(jsEndMarker)) {
|
|
81
|
-
throw new Error(`Markers not found in ${envServerPath}`)
|
|
82
|
-
}
|
|
83
|
-
|
|
224
|
+
function applyJsMarkers(_filePath: string, content: string): string {
|
|
84
225
|
const envExports = Object.entries(envVars)
|
|
85
226
|
.map(([key, value]) => {
|
|
86
|
-
// $dep: values have no compile-time default — resolved into .env at runtime
|
|
87
227
|
if (typeof value === 'string' && value.startsWith('$dep:')) {
|
|
88
228
|
return `export const ${key} = ensureEnv('${key}')`
|
|
89
229
|
}
|
|
@@ -91,69 +231,64 @@ await cmd`sync environment variables from package.json to CI and server configs`
|
|
|
91
231
|
})
|
|
92
232
|
.join('\n')
|
|
93
233
|
|
|
94
|
-
|
|
95
|
-
new RegExp(`${jsStartMarker}(.|\n)*?${jsEndMarker}`, 'm'),
|
|
96
|
-
`${jsStartMarker}\n${envExports}\n${jsEndMarker}`
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
fs.writeFileSync(envServerPath, newEnvServer, 'utf-8')
|
|
100
|
-
console.info('✅ Updated server env')
|
|
234
|
+
return replaceMarkerSection(content, jsStart, jsEnd, envExports)
|
|
101
235
|
}
|
|
102
236
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return
|
|
237
|
+
function applyDotenvInline(_filePath: string, content: string): string {
|
|
238
|
+
let result = content
|
|
239
|
+
for (const [key, value] of Object.entries(depResolved)) {
|
|
240
|
+
const re = new RegExp(`^(${key})=(.+)$`, 'm')
|
|
241
|
+
const match = result.match(re)
|
|
242
|
+
if (match && match[2] !== value) {
|
|
243
|
+
result = result.replace(re, `$1=${value}`)
|
|
244
|
+
}
|
|
112
245
|
}
|
|
246
|
+
return result
|
|
247
|
+
}
|
|
113
248
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
249
|
+
function applyTsInline(_filePath: string, content: string): string {
|
|
250
|
+
let result = content
|
|
251
|
+
for (const [key, value] of Object.entries(depResolved)) {
|
|
252
|
+
const re = new RegExp(`(${key}:\\s*)(['"])([^'"]+)\\2`, 'g')
|
|
253
|
+
result = result.replace(re, (_match, prefix, quote, oldValue) => {
|
|
254
|
+
if (oldValue !== value) return `${prefix}${quote}${value}${quote}`
|
|
255
|
+
return _match
|
|
256
|
+
})
|
|
120
257
|
}
|
|
258
|
+
return result
|
|
259
|
+
}
|
|
121
260
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
261
|
+
const strategies: Record<Strategy, (filePath: string, content: string) => string> = {
|
|
262
|
+
'yaml-markers': applyYamlMarkers,
|
|
263
|
+
'js-markers': applyJsMarkers,
|
|
264
|
+
'dotenv-inline': applyDotenvInline,
|
|
265
|
+
'ts-inline': applyTsInline,
|
|
266
|
+
}
|
|
127
267
|
|
|
128
|
-
|
|
129
|
-
// otherwise use list syntax (- KEY=val)
|
|
130
|
-
const beforeMarker = dockerCompose.slice(0, dockerCompose.indexOf(yamlStartMarker))
|
|
131
|
-
const isMap = /x-[\w-]+:.*&[\w-]+/s.test(beforeMarker)
|
|
268
|
+
let updated = 0
|
|
132
269
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
})
|
|
141
|
-
.join('\n')
|
|
270
|
+
for (const filePath of files) {
|
|
271
|
+
let content = ''
|
|
272
|
+
try {
|
|
273
|
+
content = fs.readFileSync(filePath, 'utf-8')
|
|
274
|
+
} catch {
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
142
277
|
|
|
143
|
-
const
|
|
278
|
+
const strategy = detectStrategy(filePath, content)
|
|
279
|
+
if (!strategy) continue
|
|
144
280
|
|
|
145
|
-
|
|
146
|
-
|
|
281
|
+
const newContent = strategies[strategy](filePath, content)
|
|
282
|
+
if (newContent !== content) {
|
|
283
|
+
fs.writeFileSync(filePath, newContent, 'utf-8')
|
|
284
|
+
console.info(`✅ ${filePath} (${strategy})`)
|
|
285
|
+
updated++
|
|
286
|
+
}
|
|
147
287
|
}
|
|
148
288
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.info('✅ All files updated successfully')
|
|
154
|
-
} catch (error: any) {
|
|
155
|
-
console.error('❌ Update failed:', error.message)
|
|
156
|
-
process.exit(1)
|
|
289
|
+
if (updated > 0) {
|
|
290
|
+
console.info(`\n✅ Updated ${updated} file${updated > 1 ? 's' : ''}`)
|
|
291
|
+
} else {
|
|
292
|
+
console.info('All files up to date')
|
|
157
293
|
}
|
|
158
|
-
}
|
|
159
|
-
)
|
|
294
|
+
})
|