@o/scripts 0.16.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 vxrn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@o/scripts",
3
+ "version": "0.16.19",
4
+ "type": "module",
5
+ "main": "./src/cmd.ts",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/cmd.ts",
10
+ "default": "./src/cmd.ts"
11
+ },
12
+ "./package.json": "./package.json",
13
+ "./helpers/*": {
14
+ "types": "./src/helpers/*.ts",
15
+ "default": "./src/helpers/*.ts"
16
+ },
17
+ "./*": {
18
+ "types": "./src/*.ts",
19
+ "default": "./src/*.ts"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "lint": "oxlint src",
24
+ "lint:fix": "oxfmt src && oxlint --fix --fix-suggestions src",
25
+ "typecheck": "tko run typecheck"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "dependencies": {
31
+ "@clack/prompts": "^0.8.2",
32
+ "@o/helpers": "0.16.19",
33
+ "@o/run": "0.16.19",
34
+ "picocolors": "^1.1.1",
35
+ "@aws-sdk/client-s3": "^3.709.0",
36
+ "fs-extra": "^11.2.0",
37
+ "p-map": "^7.0.3"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/natew/orez.git",
42
+ "directory": "packages/scripts"
43
+ },
44
+ "files": [
45
+ "src"
46
+ ],
47
+ "peerDependencies": {
48
+ "vxrn": ">=1.25.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "vxrn": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": null,
56
+ "optionalDependencies": null
57
+ }
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cmd } from './cmd'
4
+
5
+ await cmd`bootstrap project workspace and build initial packages`.run(
6
+ async ({ $, fs, path }) => {
7
+ // use our own file because we know its takeout-like packages if it exists
8
+ const hasPackages = fs.existsSync(`./packages/scripts/src/build-initial.ts`)
9
+
10
+ // only run once
11
+ if (!fs.existsSync(`./node_modules/.bin/tko`)) {
12
+ if (hasPackages) {
13
+ symlinkBins()
14
+ }
15
+ }
16
+
17
+ // check if critical packages are built
18
+ if (hasPackages) {
19
+ if (!fs.existsSync(`./packages/helpers/dist`)) {
20
+ // build helpers first as other packages depend on it
21
+ await $`cd packages/helpers && bun run build`
22
+ }
23
+
24
+ // then build all other packages in parallel
25
+ await $`bun ./packages/run/src/run.ts build --no-root`
26
+ }
27
+
28
+ // show welcome message if not onboarded
29
+ checkAndShowWelcome()
30
+
31
+ function checkAndShowWelcome(cwd: string = process.cwd()): void {
32
+ try {
33
+ const packagePath = path.join(cwd, 'package.json')
34
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'))
35
+
36
+ if (pkg.takeout?.onboarded === false) {
37
+ console.info()
38
+ console.info(`
39
+ ████████╗ █████╗ ██╗ ██╗███████╗ ██████╗ ██╗ ██╗████████╗
40
+ ╚══██╔══╝██╔══██╗██║ ██╔╝██╔════╝██╔═══██╗██║ ██║╚══██╔══╝
41
+ ██║ ███████║█████╔╝ █████╗ ██║ ██║██║ ██║ ██║
42
+ ██║ ██╔══██║██╔═██╗ ██╔══╝ ██║ ██║██║ ██║ ██║
43
+ ██║ ██║ ██║██║ ██╗███████╗╚██████╔╝╚██████╔╝ ██║
44
+ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝
45
+ 麵 碼 飯
46
+ `)
47
+ console.info()
48
+ console.info(' welcome to takeout! 🥡')
49
+ console.info()
50
+ console.info(' run \x1b[32mbun onboard\x1b[0m to get things set up')
51
+ console.info()
52
+ }
53
+ } catch {
54
+ // silently fail if package.json doesn't exist or is malformed
55
+ }
56
+ }
57
+
58
+ function symlinkBins() {
59
+ // workaround for https://github.com/oven-sh/bun/issues/19782
60
+ const packagesWithCLI = [
61
+ { name: 'cli', cliFile: 'cli.mjs', alias: 'tko' },
62
+ { name: 'postgres', cliFile: 'cli.cjs' },
63
+ ]
64
+
65
+ const binDir = path.join(process.cwd(), 'node_modules', '.bin')
66
+
67
+ if (!fs.existsSync(binDir)) {
68
+ fs.mkdirSync(binDir, { recursive: true })
69
+ }
70
+
71
+ for (const pkg of packagesWithCLI) {
72
+ const binPath = path.join(binDir, pkg.name)
73
+ const sourcePath = path.join(process.cwd(), 'packages', pkg.name, pkg.cliFile)
74
+ symlinkTo(sourcePath, binPath)
75
+
76
+ if (pkg.alias) {
77
+ const aliasPath = path.join(binDir, pkg.alias)
78
+ symlinkTo(sourcePath, aliasPath)
79
+ }
80
+ }
81
+
82
+ function symlinkTo(source: string, target: string): void {
83
+ if (fs.existsSync(target)) {
84
+ console.info(`✓ Symlink already exists: ${target}`)
85
+ return
86
+ }
87
+
88
+ fs.symlinkSync(source, target)
89
+ console.info(`→ Created symlink: ${source} ⇢ ${target}`)
90
+ }
91
+ }
92
+ }
93
+ )
package/src/clean.ts ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cmd } from './cmd'
4
+
5
+ await cmd`clean build artifacts and temporary files`
6
+ .args('--full boolean --modules boolean --native boolean')
7
+ .run(async ({ args, $ }) => {
8
+ const full = args.full
9
+ const modules = full || args.modules
10
+ const native = full || args.native
11
+
12
+ await $`rm -rf dist types .tamagui .vite node_modules/.cache`
13
+
14
+ if (modules) {
15
+ console.info('removing all node_modules...')
16
+ await $`find . -name 'node_modules' -type d -prune -exec rm -rf {} +`
17
+ }
18
+
19
+ if (native) {
20
+ console.info('removing ios and android folders...')
21
+ await $`rm -rf ios android`
22
+ }
23
+
24
+ console.info('cleanup complete!')
25
+
26
+ if (!full) {
27
+ console.info('use --modules, --native, or --full for deeper cleaning')
28
+ }
29
+ })
package/src/cmd.ts ADDED
@@ -0,0 +1,105 @@
1
+ type Prettify<T> = { [K in keyof T]: T[K] } & {}
2
+
3
+ type CmdContext = {
4
+ $: typeof import('bun').$
5
+ colors: typeof import('picocolors')
6
+ prompt: typeof import('@clack/prompts')
7
+ run: typeof import('./helpers/run').run
8
+ fs: typeof import('node:fs')
9
+ path: typeof import('node:path')
10
+ os: typeof import('node:os')
11
+ }
12
+
13
+ type Args<S extends string> = import('./helpers/args').Args<S>
14
+
15
+ type RunFn<S extends string> = (
16
+ fn: (ctx: Prettify<{ args: Args<S> } & CmdContext>) => Promise<void> | void
17
+ ) => Promise<void>
18
+
19
+ let _interceptCmd: ((info: { description: string; args?: string }) => void) | undefined
20
+
21
+ export function setInterceptCmd(
22
+ fn: ((info: { description: string; args?: string }) => void) | undefined
23
+ ) {
24
+ _interceptCmd = fn
25
+ }
26
+
27
+ function createCmd(description: string) {
28
+ let argsSpec: string | undefined
29
+ let envVars: Record<string, string> = {}
30
+
31
+ function makeRun<S extends string>(spec: S): RunFn<S> {
32
+ return async (fn) => {
33
+ if (_interceptCmd) {
34
+ _interceptCmd({ description, args: argsSpec })
35
+ return
36
+ }
37
+
38
+ const colors = (await import(`picocolors`)).default
39
+
40
+ if (process.argv.includes(`--help`) || process.argv.includes(`-h`)) {
41
+ console.info()
42
+ console.info(colors.bold(description))
43
+ if (argsSpec) {
44
+ console.info()
45
+ const flags = [...argsSpec.matchAll(/--?([a-z-]+)\s+(string|number|boolean)/gi)]
46
+ for (const [, flag, type] of flags) {
47
+ console.info(` --${flag} ${colors.dim(type!)}`)
48
+ }
49
+ }
50
+ console.info()
51
+ process.exit(0)
52
+ }
53
+
54
+ // apply env vars before running
55
+ for (const [key, value] of Object.entries(envVars)) {
56
+ process.env[key] = value
57
+ }
58
+
59
+ const [{ $ }, prompt, { run }, { args: parseArgs }, fs, path, os] =
60
+ await Promise.all([
61
+ import(`bun`),
62
+ import(`@clack/prompts`),
63
+ import(`./helpers/run`),
64
+ import(`./helpers/args`),
65
+ import(`node:fs`),
66
+ import(`node:path`),
67
+ import(`node:os`),
68
+ ])
69
+
70
+ const args = spec ? parseArgs(spec) : ({ rest: [] } as Args<S>)
71
+ await fn({ args, $, colors, prompt, run, fs, path, os } as Prettify<
72
+ { args: Args<S> } & CmdContext
73
+ >)
74
+ }
75
+ }
76
+
77
+ function makeChainable<S extends string>(spec: S) {
78
+ return {
79
+ env(vars: Record<string, string>) {
80
+ Object.assign(envVars, vars)
81
+ return this
82
+ },
83
+ run: makeRun(spec),
84
+ }
85
+ }
86
+
87
+ return {
88
+ env(vars: Record<string, string>) {
89
+ Object.assign(envVars, vars)
90
+ return this
91
+ },
92
+ args<const S extends string>(spec: S) {
93
+ argsSpec = spec
94
+ return makeChainable(spec)
95
+ },
96
+ run: makeRun(`` as ``),
97
+ }
98
+ }
99
+
100
+ export function cmd(description: string): ReturnType<typeof createCmd>
101
+ export function cmd(strings: TemplateStringsArray): ReturnType<typeof createCmd>
102
+ export function cmd(input: string | TemplateStringsArray) {
103
+ const description = typeof input === `string` ? input : input[0] || ``
104
+ return createCmd(description)
105
+ }
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import fs from 'node:fs'
4
+
5
+ import { cmd } from './cmd'
6
+
7
+ await cmd`set up cloudflare dev tunnel for local development`
8
+ .args('--port number')
9
+ .run(async ({ args, run, os, path }) => {
10
+ const { handleProcessExit } = await import('@o/run/helpers/handleProcessExit')
11
+
12
+ handleProcessExit()
13
+
14
+ const TUNNEL_CONFIG_DIR = path.join(os.homedir(), '.onechat-tunnel')
15
+ const TUNNEL_ID_FILE = path.join(TUNNEL_CONFIG_DIR, 'tunnel-id.txt')
16
+
17
+ async function ensureCloudflared(): Promise<boolean> {
18
+ try {
19
+ await run('cloudflared --version', { silent: true })
20
+ return true
21
+ } catch {
22
+ try {
23
+ await run('npm install -g cloudflared')
24
+ return true
25
+ } catch (error) {
26
+ console.error('Error installing cloudflared:', error)
27
+ return false
28
+ }
29
+ }
30
+ }
31
+
32
+ async function ensureAuthenticated(): Promise<boolean> {
33
+ const certPath = path.join(os.homedir(), '.cloudflared', 'cert.pem')
34
+ if (fs.existsSync(certPath)) {
35
+ return true
36
+ }
37
+
38
+ try {
39
+ await run('cloudflared tunnel login')
40
+ return true
41
+ } catch {
42
+ console.error('\n❌ Authentication failed')
43
+ return false
44
+ }
45
+ }
46
+
47
+ async function getOrCreateTunnel(): Promise<string | null> {
48
+ if (!fs.existsSync(TUNNEL_CONFIG_DIR)) {
49
+ fs.mkdirSync(TUNNEL_CONFIG_DIR, { recursive: true })
50
+ }
51
+
52
+ if (fs.existsSync(TUNNEL_ID_FILE)) {
53
+ const tunnelId = fs.readFileSync(TUNNEL_ID_FILE, 'utf-8').trim()
54
+ return tunnelId
55
+ }
56
+
57
+ const tunnelName = `onechat-dev-${process.env.USER || 'tunnel'}`
58
+
59
+ try {
60
+ const { stdout, stderr } = await run(`cloudflared tunnel create ${tunnelName}`, {
61
+ captureOutput: true,
62
+ })
63
+ const output = stdout + stderr
64
+
65
+ const match1 = output.match(/Created tunnel .+ with id ([a-f0-9-]+)/i)
66
+ const match2 = output.match(/Tunnel ([a-f0-9-]+) created/i)
67
+ const match3 = output.match(
68
+ /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i
69
+ )
70
+
71
+ const tunnelId = match1?.[1] || match2?.[1] || match3?.[1]
72
+
73
+ if (tunnelId) {
74
+ fs.writeFileSync(TUNNEL_ID_FILE, tunnelId)
75
+ return tunnelId
76
+ }
77
+
78
+ console.error('Failed to extract tunnel ID from output')
79
+ console.error('Output was:', output)
80
+ return null
81
+ } catch (error: any) {
82
+ if (error.message?.includes('already exists')) {
83
+ try {
84
+ const { stdout } = await run(
85
+ `cloudflared tunnel list --name ${tunnelName} --output json`,
86
+ { captureOutput: true }
87
+ )
88
+ const tunnels = JSON.parse(stdout)
89
+ if (tunnels.length > 0) {
90
+ const tunnelId = tunnels[0].id
91
+ fs.writeFileSync(TUNNEL_ID_FILE, tunnelId)
92
+ return tunnelId
93
+ }
94
+ } catch (e) {
95
+ console.error('Failed to parse tunnel list:', e)
96
+ }
97
+ } else {
98
+ console.error('Failed to create tunnel:', error)
99
+ }
100
+ return null
101
+ }
102
+ }
103
+
104
+ const port = args.port ?? 8081
105
+
106
+ // ensure cloudflared is installed
107
+ const isInstalled = await ensureCloudflared()
108
+ if (!isInstalled) {
109
+ console.error('Failed to install cloudflared')
110
+ process.exit(1)
111
+ }
112
+
113
+ // ensure authenticated
114
+ const isAuthenticated = await ensureAuthenticated()
115
+ if (!isAuthenticated) {
116
+ console.error('Failed to authenticate with Cloudflare')
117
+ process.exit(1)
118
+ }
119
+
120
+ // get or create tunnel
121
+ const tunnelId = await getOrCreateTunnel()
122
+ if (!tunnelId) {
123
+ console.error('Failed to get or create tunnel')
124
+ process.exit(1)
125
+ }
126
+
127
+ // save the expected url immediately so it's available right away
128
+ const expectedUrl = `https://${tunnelId}.cfargotunnel.com`
129
+ fs.writeFileSync(path.join(TUNNEL_CONFIG_DIR, 'tunnel-url.txt'), expectedUrl)
130
+ console.info(`\n🌐 Tunnel URL: ${expectedUrl}`)
131
+
132
+ // get the public url in the background
133
+ setTimeout(async () => {
134
+ try {
135
+ const { stdout } = await run(
136
+ `cloudflared tunnel info ${tunnelId} --output json`,
137
+ {
138
+ captureOutput: true,
139
+ silent: true,
140
+ }
141
+ )
142
+
143
+ try {
144
+ const info = JSON.parse(stdout)
145
+ const hostname = info.hostname || `${tunnelId}.cfargotunnel.com`
146
+ fs.writeFileSync(
147
+ path.join(TUNNEL_CONFIG_DIR, 'tunnel-url.txt'),
148
+ `https://${hostname}`
149
+ )
150
+ } catch (e) {
151
+ // use fallback url already saved
152
+ }
153
+ } catch (e) {
154
+ // use fallback url already saved
155
+ }
156
+ }, 3000)
157
+
158
+ // run tunnel - managed by handleProcessExit
159
+ await run(`cloudflared tunnel run --url http://localhost:${port} ${tunnelId}`, {
160
+ detached: false,
161
+ })
162
+ })
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cmd } from './cmd'
4
+
5
+ await cmd`ensure a port is available, exit with error if in use`
6
+ .args('--auto-kill string')
7
+ .run(async ({ args }) => {
8
+ const { execSync } = await import('node:child_process')
9
+
10
+ const port = args.rest[0]
11
+
12
+ if (!port) {
13
+ console.error('usage: bun tko ensure-port <port> [--auto-kill <prefix>]')
14
+ process.exit(1)
15
+ }
16
+
17
+ const portNum = Number.parseInt(port, 10)
18
+
19
+ if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {
20
+ console.error(`invalid port: ${port}`)
21
+ process.exit(1)
22
+ }
23
+
24
+ function getListeningProcess(p: number): { pid?: number; command?: string } {
25
+ try {
26
+ // use -sTCP:LISTEN to only find processes LISTENING on the port (servers)
27
+ const output = execSync(`lsof -i :${p} -sTCP:LISTEN -t`, {
28
+ encoding: 'utf-8',
29
+ stdio: ['pipe', 'pipe', 'ignore'],
30
+ }).trim()
31
+
32
+ if (!output) return {}
33
+
34
+ const pid = Number.parseInt(output.split('\n')[0] || '', 10)
35
+ if (Number.isNaN(pid)) return {}
36
+
37
+ const ps = execSync(`ps -p ${pid} -o comm=`, {
38
+ encoding: 'utf-8',
39
+ stdio: ['pipe', 'pipe', 'ignore'],
40
+ }).trim()
41
+
42
+ return { pid, command: ps }
43
+ } catch {
44
+ return {}
45
+ }
46
+ }
47
+
48
+ function killProcess(pid: number): boolean {
49
+ try {
50
+ execSync(`kill ${pid}`, { stdio: 'ignore' })
51
+ return true
52
+ } catch {
53
+ return false
54
+ }
55
+ }
56
+
57
+ const { pid, command } = getListeningProcess(portNum)
58
+
59
+ if (pid) {
60
+ if (args.autoKill && command?.startsWith(args.autoKill)) {
61
+ console.info(`killing ${command} (pid ${pid}) on port ${portNum}`)
62
+ if (killProcess(pid)) {
63
+ Bun.sleepSync(100)
64
+ const check = getListeningProcess(portNum)
65
+ if (!check.pid) {
66
+ process.exit(0)
67
+ }
68
+ console.error(`failed to free port ${portNum}`)
69
+ process.exit(1)
70
+ }
71
+ console.error(`failed to kill pid ${pid}`)
72
+ process.exit(1)
73
+ }
74
+
75
+ console.error(`port ${portNum} in use by ${command || 'unknown'} (pid ${pid})`)
76
+ console.error(`run: kill ${pid}`)
77
+ process.exit(1)
78
+ }
79
+ })
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cmd } from './cmd'
4
+
5
+ export function ensureEnv() {
6
+ // make it a module
7
+ }
8
+
9
+ await cmd`check if SST production tunnel is active`.run(async () => {
10
+ const { spawnSync } = await import('node:child_process')
11
+
12
+ const netstat = spawnSync('netstat', ['-rn'], { encoding: 'utf-8' }).stdout || ''
13
+ const isTunnelActive = netstat.includes(`10.0.8/22`)
14
+
15
+ if (!isTunnelActive) {
16
+ console.error(`No tunnel active, run bun sst:tunnel:production first`)
17
+ process.exit(1)
18
+ }
19
+ })
@@ -0,0 +1,31 @@
1
+ import { cmd } from './cmd'
2
+
3
+ const SQLITE_NODE_PATH =
4
+ 'node_modules/@rocicorp/zero-sqlite3/build/Release/better_sqlite3.node'
5
+
6
+ await cmd`ensure zero-sqlite3 native module is built`.run(async ({ $, fs }) => {
7
+ if (fs.existsSync(SQLITE_NODE_PATH)) {
8
+ return
9
+ }
10
+
11
+ // bun trustedDependencies doesn't reliably run zero-sqlite3's install script,
12
+ // so build/fetch the native module ourselves. run the package's OWN install
13
+ // chain (`npm run install` = `prebuild-install || <binding exists?> ||
14
+ // node-gyp rebuild`), which prefers the PREBUILT binary and only source-builds
15
+ // as a last resort. @rocicorp/zero-sqlite3 >=1.1.2 is prebuilt-ONLY — its npm
16
+ // tarball omits generated headers (e.g. unicode_case_data.h), so a direct
17
+ // `npm run build-release` (node-gyp) fails; prebuild-install is the supported
18
+ // path. success is "the binding now exists" (the install chain can exit
19
+ // non-zero from a failed prebuild-install fallback even when an earlier step
20
+ // already produced the binding).
21
+ const result = await $`cd node_modules/@rocicorp/zero-sqlite3 && npm run install`
22
+ .quiet()
23
+ .nothrow()
24
+
25
+ if (!fs.existsSync(SQLITE_NODE_PATH)) {
26
+ console.error(
27
+ 'zero-sqlite3 native module unavailable, will fall back to wasm:',
28
+ result.stderr.toString()
29
+ )
30
+ }
31
+ })
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import fs from 'node:fs'
4
+
5
+ import { cmd } from './cmd'
6
+
7
+ await cmd`pull environment variables from SST production`.run(async ({ path }) => {
8
+ const { getEnvironment } = await import('./sst-get-environment')
9
+
10
+ const rootDir = process.cwd()
11
+
12
+ const envFilePath = path.join(rootDir, '.env.production')
13
+ if (fs.existsSync(envFilePath)) {
14
+ console.error(
15
+ '❌ Error: .env.production already exists. Please remove or rename it first.'
16
+ )
17
+ process.exit(1)
18
+ }
19
+
20
+ const packageJson = require(path.join(rootDir, 'package.json'))
21
+ const envVarsNeeded = packageJson.env ? Object.keys(packageJson.env) : []
22
+ if (!envVarsNeeded.length) {
23
+ console.error(
24
+ `❌ Error: No environment variables specified in package.json 'env' array.`
25
+ )
26
+ process.exit(1)
27
+ }
28
+
29
+ try {
30
+ const envVars = getEnvironment('WebApp')
31
+
32
+ let envFileContent = ''
33
+ let foundCount = 0
34
+
35
+ for (const varName of envVarsNeeded) {
36
+ if (envVars[varName] !== undefined) {
37
+ envFileContent += `${varName}=${envVars[varName]}\n`
38
+ foundCount++
39
+ } else {
40
+ console.warn(`⚠️ Warning: Didn't find env: ${varName}`)
41
+ }
42
+ }
43
+
44
+ if (foundCount === 0) {
45
+ console.error(`❌ Error: None of the required environment variables were found.`)
46
+ process.exit(1)
47
+ }
48
+
49
+ fs.writeFileSync(envFilePath, envFileContent)
50
+
51
+ console.info(
52
+ `✅ Success! ${foundCount} environment variables written to .env.production`
53
+ )
54
+ } catch (error) {
55
+ console.error('❌ Error fetching environment variables:', error)
56
+ process.exit(1)
57
+ }
58
+ })