@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 +21 -0
- package/package.json +57 -0
- package/src/build-initial.ts +93 -0
- package/src/clean.ts +29 -0
- package/src/cmd.ts +105 -0
- package/src/dev-tunnel.ts +162 -0
- package/src/ensure-port.ts +79 -0
- package/src/ensure-tunnel.ts +19 -0
- package/src/ensure-zero-sqlite.ts +31 -0
- package/src/env-pull.ts +58 -0
- package/src/env-update.ts +342 -0
- package/src/exec-with-env.ts +61 -0
- package/src/helpers/args.ts +97 -0
- package/src/helpers/check-port.ts +22 -0
- package/src/helpers/deploy-health.ts +227 -0
- package/src/helpers/deploy-lock.ts +231 -0
- package/src/helpers/ensure-s3-bucket.ts +88 -0
- package/src/helpers/env-load.ts +43 -0
- package/src/helpers/get-docker-host.ts +37 -0
- package/src/helpers/get-test-env.ts +46 -0
- package/src/helpers/github-tail.ts +249 -0
- package/src/helpers/multipass.ts +174 -0
- package/src/helpers/parse-env-file.ts +71 -0
- package/src/helpers/process-compose-env.ts +57 -0
- package/src/helpers/resolve-dep-version.ts +11 -0
- package/src/helpers/run.ts +340 -0
- package/src/helpers/ssh.ts +50 -0
- package/src/helpers/uncloud-deploy.ts +162 -0
- package/src/helpers/uncloud.ts +197 -0
- package/src/helpers/wait-for-port.ts +33 -0
- package/src/helpers/zero-get-version.ts +8 -0
- package/src/release.ts +544 -0
- package/src/sst-get-environment.ts +38 -0
- package/src/typecheck.ts +21 -0
- package/src/up.ts +440 -0
- package/src/update-changelog.ts +92 -0
- package/src/wait-for-dev.ts +41 -0
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
|
+
})
|
package/src/env-pull.ts
ADDED
|
@@ -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
|
+
})
|