@take-out/scripts 0.0.28
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 +27 -0
- package/src/bootstrap.ts +182 -0
- package/src/check-circular-deps.ts +113 -0
- package/src/clean.ts +15 -0
- package/src/dev-tunnel-if-exist.ts +166 -0
- package/src/dev-tunnel.ts +178 -0
- package/src/ensure-tunnel.ts +13 -0
- package/src/env-pull.ts +54 -0
- package/src/env-update.ts +126 -0
- package/src/exec-with-env.ts +57 -0
- package/src/helpers/check-port.ts +22 -0
- package/src/helpers/ensure-s3-bucket.ts +88 -0
- package/src/helpers/env-load.ts +26 -0
- package/src/helpers/get-docker-host.ts +37 -0
- package/src/helpers/get-test-env.ts +25 -0
- package/src/helpers/handleProcessExit.ts +254 -0
- package/src/helpers/run.ts +310 -0
- package/src/helpers/wait-for-port.ts +33 -0
- package/src/helpers/zero-get-version.ts +8 -0
- package/src/node-version-check.ts +49 -0
- package/src/release.ts +352 -0
- package/src/run.ts +358 -0
- package/src/sst-get-environment.ts +31 -0
- package/src/typescript.ts +16 -0
- package/src/update-deps.ts +336 -0
- package/src/wait-for-dev.ts +40 -0
- package/tsconfig.json +9 -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,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@take-out/scripts",
|
|
3
|
+
"version": "0.0.28",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
"./package.json": "./package.json",
|
|
9
|
+
"./helpers/*": {
|
|
10
|
+
"types": "./src/helpers/*.ts",
|
|
11
|
+
"default": "./src/helpers/*.ts"
|
|
12
|
+
},
|
|
13
|
+
"./*": {
|
|
14
|
+
"types": "./src/*.ts",
|
|
15
|
+
"default": "./src/*.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@take-out/helpers": "0.0.28"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"vxrn": "*"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @description Bootstrap project dependencies and workspace packages
|
|
5
|
+
*
|
|
6
|
+
* This script runs automatically during `bun install` via the prepare lifecycle.
|
|
7
|
+
* It manages the .env file configuration with the following responsibilities:
|
|
8
|
+
*
|
|
9
|
+
* 1. Creates .env from .env.template if it doesn't exist
|
|
10
|
+
* 2. Maintains an auto-generated section in .env with package metadata
|
|
11
|
+
* 3. Currently syncs ZERO_VERSION from package.json dependencies
|
|
12
|
+
* 4. Preserves all user-defined environment variables
|
|
13
|
+
* 5. Creates backups before modifications for safety
|
|
14
|
+
*
|
|
15
|
+
* The auto-generated section is clearly marked and should not be edited manually.
|
|
16
|
+
* All operations are defensive and will not fail the install process.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
existsSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
copyFileSync,
|
|
24
|
+
renameSync,
|
|
25
|
+
unlinkSync,
|
|
26
|
+
} from 'node:fs'
|
|
27
|
+
import { join } from 'node:path'
|
|
28
|
+
import { getZeroVersion } from '@take-out/scripts/helpers/zero-get-version'
|
|
29
|
+
|
|
30
|
+
const ENV_PATH = join(process.cwd(), '.env')
|
|
31
|
+
const ENV_TEMPLATE_PATH = join(process.cwd(), '.env.template')
|
|
32
|
+
const ENV_BACKUP_PATH = join(process.cwd(), '.env.backup')
|
|
33
|
+
const ENV_TEMP_PATH = join(process.cwd(), '.env.tmp')
|
|
34
|
+
|
|
35
|
+
// auto-generated section markers
|
|
36
|
+
const BEGIN_MARKER = '# ---- BEGIN AUTO-GENERATED (DO NOT EDIT) ----'
|
|
37
|
+
const END_MARKER = '# ---- END AUTO-GENERATED ----'
|
|
38
|
+
|
|
39
|
+
function createEnvFromTemplate(): boolean {
|
|
40
|
+
if (!existsSync(ENV_TEMPLATE_PATH)) {
|
|
41
|
+
console.info('No .env.template found, skipping .env creation')
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
copyFileSync(ENV_TEMPLATE_PATH, ENV_PATH)
|
|
47
|
+
console.info('Created .env from .env.template')
|
|
48
|
+
return true
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Failed to create .env from .env.template:', error)
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getAutoGeneratedContent(): string {
|
|
56
|
+
const zeroVersion = getZeroVersion()
|
|
57
|
+
if (!zeroVersion) {
|
|
58
|
+
console.warn('Could not determine Zero version')
|
|
59
|
+
return ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// build the auto-generated content
|
|
63
|
+
const lines = [
|
|
64
|
+
BEGIN_MARKER,
|
|
65
|
+
`# Generated at: ${new Date().toISOString()}`,
|
|
66
|
+
`ZERO_VERSION=${zeroVersion}`,
|
|
67
|
+
END_MARKER,
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
return lines.join('\n')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function updateEnvFile(): void {
|
|
74
|
+
// ensure .env exists
|
|
75
|
+
if (!existsSync(ENV_PATH)) {
|
|
76
|
+
const created = createEnvFromTemplate()
|
|
77
|
+
if (!created && !existsSync(ENV_PATH)) {
|
|
78
|
+
// create empty .env if no template exists
|
|
79
|
+
writeFileSync(ENV_PATH, '')
|
|
80
|
+
console.info('Created empty .env file')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// create backup
|
|
86
|
+
if (existsSync(ENV_PATH)) {
|
|
87
|
+
copyFileSync(ENV_PATH, ENV_BACKUP_PATH)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// read current content
|
|
91
|
+
const currentContent = readFileSync(ENV_PATH, 'utf-8')
|
|
92
|
+
|
|
93
|
+
// find existing auto-generated section
|
|
94
|
+
const beginIndex = currentContent.indexOf(BEGIN_MARKER)
|
|
95
|
+
const endIndex = currentContent.indexOf(END_MARKER)
|
|
96
|
+
|
|
97
|
+
let newContent: string
|
|
98
|
+
|
|
99
|
+
if (beginIndex !== -1 && endIndex !== -1 && endIndex > beginIndex) {
|
|
100
|
+
// replace existing auto-generated section
|
|
101
|
+
const beforeSection = currentContent.substring(0, beginIndex).trimEnd()
|
|
102
|
+
const afterSection = currentContent
|
|
103
|
+
.substring(endIndex + END_MARKER.length)
|
|
104
|
+
.trimStart()
|
|
105
|
+
|
|
106
|
+
newContent = [beforeSection, getAutoGeneratedContent(), afterSection]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join('\n\n')
|
|
109
|
+
} else if (beginIndex !== -1 || endIndex !== -1) {
|
|
110
|
+
// malformed markers - preserve content and append new section
|
|
111
|
+
console.warn('Found malformed auto-generated section, appending new section')
|
|
112
|
+
newContent = currentContent.trimEnd() + '\n\n' + getAutoGeneratedContent()
|
|
113
|
+
} else {
|
|
114
|
+
// no existing section - append to end
|
|
115
|
+
const trimmedContent = currentContent.trimEnd()
|
|
116
|
+
newContent = trimmedContent
|
|
117
|
+
? trimmedContent + '\n\n' + getAutoGeneratedContent()
|
|
118
|
+
: getAutoGeneratedContent()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// write to temp file first (atomic operation)
|
|
122
|
+
writeFileSync(ENV_TEMP_PATH, newContent)
|
|
123
|
+
|
|
124
|
+
// validate temp file
|
|
125
|
+
const tempContent = readFileSync(ENV_TEMP_PATH, 'utf-8')
|
|
126
|
+
if (!tempContent.includes(BEGIN_MARKER) || !tempContent.includes(END_MARKER)) {
|
|
127
|
+
throw new Error('Generated content validation failed')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// atomic replace
|
|
131
|
+
renameSync(ENV_TEMP_PATH, ENV_PATH)
|
|
132
|
+
|
|
133
|
+
if (existsSync(ENV_BACKUP_PATH)) {
|
|
134
|
+
try {
|
|
135
|
+
unlinkSync(ENV_BACKUP_PATH)
|
|
136
|
+
} catch {
|
|
137
|
+
// ignore cleanup errors
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.info('Updated .env auto-generated section')
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('Failed to update .env file:', error)
|
|
144
|
+
|
|
145
|
+
// attempt to restore backup
|
|
146
|
+
if (existsSync(ENV_BACKUP_PATH)) {
|
|
147
|
+
try {
|
|
148
|
+
copyFileSync(ENV_BACKUP_PATH, ENV_PATH)
|
|
149
|
+
console.info('Restored .env from backup')
|
|
150
|
+
} catch (restoreError) {
|
|
151
|
+
console.error('Failed to restore backup:', restoreError)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// clean up temp file
|
|
156
|
+
if (existsSync(ENV_TEMP_PATH)) {
|
|
157
|
+
try {
|
|
158
|
+
unlinkSync(ENV_TEMP_PATH)
|
|
159
|
+
} catch {
|
|
160
|
+
// ignore cleanup errors
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// don't fail the install process
|
|
165
|
+
process.exit(0)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// main execution
|
|
170
|
+
// skip bootstrap in CI environments
|
|
171
|
+
if (process.env.CI === 'true') {
|
|
172
|
+
console.info('Skipping bootstrap in CI environment')
|
|
173
|
+
process.exit(0)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
updateEnvFile()
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// catch any unexpected errors and exit gracefully
|
|
180
|
+
console.error('Bootstrap script error:', error)
|
|
181
|
+
process.exit(0)
|
|
182
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from 'bun'
|
|
4
|
+
import { globSync } from 'glob'
|
|
5
|
+
import { basename } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const projectRoot = process.cwd()
|
|
8
|
+
|
|
9
|
+
// parse --ignore flag
|
|
10
|
+
const args = process.argv.slice(2)
|
|
11
|
+
const ignoreIndex = args.indexOf('--ignore')
|
|
12
|
+
const ignoredFiles: string[] = []
|
|
13
|
+
|
|
14
|
+
if (ignoreIndex !== -1 && args[ignoreIndex + 1]) {
|
|
15
|
+
// split comma-separated files
|
|
16
|
+
const ignoreArg = args[ignoreIndex + 1]
|
|
17
|
+
if (ignoreArg) {
|
|
18
|
+
const ignoreList = ignoreArg.split(',')
|
|
19
|
+
ignoredFiles.push(...ignoreList.map((f) => f.trim()))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// glob all tsx files in app directory
|
|
24
|
+
const appFiles = globSync('app/**/*.tsx', {
|
|
25
|
+
cwd: projectRoot,
|
|
26
|
+
absolute: true,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// glob all tsx files in src directory
|
|
30
|
+
const srcFiles = globSync('src/**/*.tsx', {
|
|
31
|
+
cwd: projectRoot,
|
|
32
|
+
absolute: true,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// combine all files
|
|
36
|
+
const allFiles = [...appFiles, ...srcFiles]
|
|
37
|
+
|
|
38
|
+
if (allFiles.length === 0) {
|
|
39
|
+
console.error('No tsx files found')
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// build the command with all -r flags
|
|
44
|
+
const commandArgs = [
|
|
45
|
+
'bunx',
|
|
46
|
+
'@glideapps/ts-helper',
|
|
47
|
+
'-c', // check circular dependencies
|
|
48
|
+
'-p',
|
|
49
|
+
projectRoot,
|
|
50
|
+
...allFiles.flatMap((file) => ['-r', file]),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
console.info(`Checking circular dependencies for ${allFiles.length} files...`)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// run the command and capture output
|
|
57
|
+
await $`${commandArgs}`.quiet()
|
|
58
|
+
console.info('✅ No circular dependencies found')
|
|
59
|
+
} catch (error: any) {
|
|
60
|
+
// parse the output to check for cycles
|
|
61
|
+
const output = error.stderr?.toString() || error.stdout?.toString() || ''
|
|
62
|
+
|
|
63
|
+
// parse cycle arrays from output
|
|
64
|
+
const cycleRegex = /\[([^\]]+)\]/g
|
|
65
|
+
const cycles: string[][] = []
|
|
66
|
+
let match
|
|
67
|
+
|
|
68
|
+
while ((match = cycleRegex.exec(output)) !== null) {
|
|
69
|
+
// parse the cycle array
|
|
70
|
+
const cycleStr = match[1]
|
|
71
|
+
if (cycleStr) {
|
|
72
|
+
const files = cycleStr.split(',').map((f) => f.trim().replace(/"/g, ''))
|
|
73
|
+
cycles.push(files)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// filter out cycles that contain ignored files
|
|
78
|
+
const remainingCycles = cycles.filter((cycle) => {
|
|
79
|
+
const containsIgnored = cycle.some((file) =>
|
|
80
|
+
ignoredFiles.some((ignored) => basename(file) === ignored || file.includes(ignored))
|
|
81
|
+
)
|
|
82
|
+
return !containsIgnored
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (remainingCycles.length > 0) {
|
|
86
|
+
// reconstruct output with filtered cycles
|
|
87
|
+
console.info(
|
|
88
|
+
output
|
|
89
|
+
.split('\n')
|
|
90
|
+
.filter((line: string) => !line.startsWith('['))
|
|
91
|
+
.join('\n')
|
|
92
|
+
)
|
|
93
|
+
console.info(`Found ${remainingCycles.length} dependency cycles`)
|
|
94
|
+
remainingCycles.forEach((cycle) => {
|
|
95
|
+
console.info(JSON.stringify(cycle))
|
|
96
|
+
})
|
|
97
|
+
console.error('❌ Circular dependencies detected')
|
|
98
|
+
process.exit(1)
|
|
99
|
+
} else if (cycles.length > 0) {
|
|
100
|
+
// all cycles were ignored
|
|
101
|
+
console.info(
|
|
102
|
+
`✅ Found ${cycles.length} circular dependencies but all contain ignored files`
|
|
103
|
+
)
|
|
104
|
+
if (ignoredFiles.length > 0) {
|
|
105
|
+
console.info(` Ignored files: ${ignoredFiles.join(', ')}`)
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// no cycles found, but command failed for another reason
|
|
109
|
+
console.error(output)
|
|
110
|
+
console.error('❌ Error running circular dependency check')
|
|
111
|
+
process.exit(1)
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/clean.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @description Clean build artifacts and temporary files
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'node:child_process'
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
execSync('rm -rf dist types .tamagui .vite node_modules/.cache', { stdio: 'inherit' })
|
|
11
|
+
console.info('Cleanup complete!')
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('Error cleaning up')
|
|
14
|
+
throw error
|
|
15
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
const TUNNEL_CONFIG_DIR = join(homedir(), '.onechat-tunnel')
|
|
9
|
+
const TUNNEL_ID_FILE = join(TUNNEL_CONFIG_DIR, 'tunnel-id.txt')
|
|
10
|
+
const TUNNEL_CONFIG_FILE = join(TUNNEL_CONFIG_DIR, 'config.yml')
|
|
11
|
+
|
|
12
|
+
// check internet connectivity first
|
|
13
|
+
async function checkInternetConnection(): Promise<boolean> {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
// try to ping cloudflare dns
|
|
16
|
+
const pingProcess = spawn('ping', ['-c', '1', '-W', '2', '1.1.1.1'], {
|
|
17
|
+
stdio: 'pipe',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
pingProcess.on('error', () => {
|
|
21
|
+
resolve(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
pingProcess.on('exit', (code) => {
|
|
25
|
+
resolve(code === 0)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// check connectivity before proceeding
|
|
31
|
+
const isOnline = await checkInternetConnection()
|
|
32
|
+
if (!isOnline) {
|
|
33
|
+
console.info('📵 Offline - skipping tunnel setup')
|
|
34
|
+
// Keep process alive for watch mode
|
|
35
|
+
await new Promise(() => {})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ensure config dir exists
|
|
39
|
+
if (!existsSync(TUNNEL_CONFIG_DIR)) {
|
|
40
|
+
mkdirSync(TUNNEL_CONFIG_DIR, { recursive: true })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// check if cloudflared is authenticated
|
|
44
|
+
const certPath = join(homedir(), '.cloudflared', 'cert.pem')
|
|
45
|
+
const hasCloudflaredAuth = existsSync(certPath)
|
|
46
|
+
|
|
47
|
+
if (!hasCloudflaredAuth) {
|
|
48
|
+
console.info('☁️ Tunnel not set up. Run "bun dev:tunnel" to enable.')
|
|
49
|
+
// Keep process alive for watch mode
|
|
50
|
+
await new Promise(() => {})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// check if we have a tunnel id
|
|
54
|
+
const hasTunnelId = existsSync(TUNNEL_ID_FILE)
|
|
55
|
+
if (!hasTunnelId) {
|
|
56
|
+
console.info('☁️ No tunnel created. Run "bun dev:tunnel" once to set up.')
|
|
57
|
+
// Keep process alive for watch mode
|
|
58
|
+
await new Promise(() => {})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// check if cloudflared is installed
|
|
62
|
+
const checkProcess = spawn('which', ['cloudflared'], {
|
|
63
|
+
shell: true,
|
|
64
|
+
stdio: 'pipe',
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
checkProcess.on('error', () => {
|
|
68
|
+
console.warn('⚠️ cloudflared not found - skipping tunnel setup')
|
|
69
|
+
// Keep process alive for watch mode
|
|
70
|
+
new Promise(() => {})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
checkProcess.on('exit', async (code) => {
|
|
74
|
+
if (code === 0) {
|
|
75
|
+
const tunnelId = readFileSync(TUNNEL_ID_FILE, 'utf-8').trim()
|
|
76
|
+
const tunnelName = 'onechat-dev-n8' // your tunnel name
|
|
77
|
+
console.info(`🚇 Starting tunnel ${tunnelId}...`)
|
|
78
|
+
|
|
79
|
+
// The stable URL format for your domain
|
|
80
|
+
const tunnelDomain = `${tunnelName}.start.chat`
|
|
81
|
+
|
|
82
|
+
// Create config file with ingress rules
|
|
83
|
+
const tunnelConfig = `
|
|
84
|
+
tunnel: ${tunnelId}
|
|
85
|
+
credentials-file: ${join(homedir(), '.cloudflared', `${tunnelId}.json`)}
|
|
86
|
+
|
|
87
|
+
ingress:
|
|
88
|
+
- hostname: ${tunnelDomain}
|
|
89
|
+
service: http://localhost:8081
|
|
90
|
+
- service: http_status:404
|
|
91
|
+
`
|
|
92
|
+
writeFileSync(TUNNEL_CONFIG_FILE, tunnelConfig)
|
|
93
|
+
|
|
94
|
+
// Set up DNS route if needed (this is idempotent, safe to run multiple times)
|
|
95
|
+
const routeProcess = spawn(
|
|
96
|
+
'cloudflared',
|
|
97
|
+
['tunnel', 'route', 'dns', tunnelId, tunnelDomain],
|
|
98
|
+
{
|
|
99
|
+
stdio: 'pipe',
|
|
100
|
+
shell: true,
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
await new Promise((resolve) => {
|
|
105
|
+
routeProcess.on('exit', resolve)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const tunnelUrl = `https://${tunnelDomain}`
|
|
109
|
+
const tunnelUrlFile = join(TUNNEL_CONFIG_DIR, 'tunnel-url.txt')
|
|
110
|
+
writeFileSync(tunnelUrlFile, tunnelUrl)
|
|
111
|
+
console.info(`🌐 Tunnel URL: ${tunnelUrl}`)
|
|
112
|
+
|
|
113
|
+
// Run tunnel with config file
|
|
114
|
+
const tunnelProcess = spawn(
|
|
115
|
+
'cloudflared',
|
|
116
|
+
['tunnel', '--config', TUNNEL_CONFIG_FILE, 'run'],
|
|
117
|
+
{
|
|
118
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
119
|
+
shell: true,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// Track if we've shown the connection message
|
|
124
|
+
let hasShownConnected = false
|
|
125
|
+
|
|
126
|
+
// Process stdout to filter logs
|
|
127
|
+
tunnelProcess.stdout?.on('data', (data) => {
|
|
128
|
+
const output = data.toString()
|
|
129
|
+
|
|
130
|
+
// Only show first successful connection
|
|
131
|
+
if (!hasShownConnected && output.includes('Registered tunnel connection')) {
|
|
132
|
+
console.info('✅ Tunnel connected')
|
|
133
|
+
hasShownConnected = true
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Process stderr (where cloudflared logs go)
|
|
138
|
+
tunnelProcess.stderr?.on('data', (data) => {
|
|
139
|
+
const output = data.toString()
|
|
140
|
+
|
|
141
|
+
// Only show first successful connection
|
|
142
|
+
if (!hasShownConnected && output.includes('Registered tunnel connection')) {
|
|
143
|
+
console.info('✅ Tunnel connected')
|
|
144
|
+
hasShownConnected = true
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// handle process termination
|
|
149
|
+
process.on('SIGINT', () => {
|
|
150
|
+
tunnelProcess.kill()
|
|
151
|
+
process.exit(0)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
tunnelProcess.on('exit', (code) => {
|
|
155
|
+
if (code !== 0) {
|
|
156
|
+
console.warn(`⚠️ Tunnel process exited with code ${code}`)
|
|
157
|
+
}
|
|
158
|
+
// Keep process alive for watch mode
|
|
159
|
+
new Promise(() => {})
|
|
160
|
+
})
|
|
161
|
+
} else {
|
|
162
|
+
console.warn('⚠️ cloudflared not found - skipping tunnel setup')
|
|
163
|
+
// Keep process alive for watch mode
|
|
164
|
+
await new Promise(() => {})
|
|
165
|
+
}
|
|
166
|
+
})
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { handleProcessExit } from './helpers/handleProcessExit'
|
|
7
|
+
import { run } from './helpers/run'
|
|
8
|
+
|
|
9
|
+
handleProcessExit()
|
|
10
|
+
|
|
11
|
+
const TUNNEL_CONFIG_DIR = join(homedir(), '.onechat-tunnel')
|
|
12
|
+
const TUNNEL_ID_FILE = join(TUNNEL_CONFIG_DIR, 'tunnel-id.txt')
|
|
13
|
+
|
|
14
|
+
async function ensureCloudflared(): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
// check if cloudflared is installed
|
|
17
|
+
await run('cloudflared --version', { silent: true })
|
|
18
|
+
return true
|
|
19
|
+
} catch {
|
|
20
|
+
// install cloudflared using npm
|
|
21
|
+
try {
|
|
22
|
+
await run('npm install -g cloudflared')
|
|
23
|
+
return true
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Error installing cloudflared:', error)
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function ensureAuthenticated(): Promise<boolean> {
|
|
32
|
+
// check if we have credentials
|
|
33
|
+
const certPath = join(homedir(), '.cloudflared', 'cert.pem')
|
|
34
|
+
if (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
|
+
// create config directory if it doesn't exist
|
|
49
|
+
if (!existsSync(TUNNEL_CONFIG_DIR)) {
|
|
50
|
+
mkdirSync(TUNNEL_CONFIG_DIR, { recursive: true })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// check if we have a saved tunnel ID
|
|
54
|
+
if (existsSync(TUNNEL_ID_FILE)) {
|
|
55
|
+
const tunnelId = readFileSync(TUNNEL_ID_FILE, 'utf-8').trim()
|
|
56
|
+
return tunnelId
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// create a new tunnel with a stable name
|
|
60
|
+
const tunnelName = `onechat-dev-${process.env.USER || 'tunnel'}`
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const { stdout, stderr } = await run(`cloudflared tunnel create ${tunnelName}`, {
|
|
64
|
+
captureOutput: true,
|
|
65
|
+
})
|
|
66
|
+
const output = stdout + stderr
|
|
67
|
+
|
|
68
|
+
// extract tunnel ID from output - try multiple patterns
|
|
69
|
+
const match1 = output.match(/Created tunnel .+ with id ([a-f0-9-]+)/i)
|
|
70
|
+
const match2 = output.match(/Tunnel ([a-f0-9-]+) created/i)
|
|
71
|
+
const match3 = output.match(
|
|
72
|
+
/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const tunnelId = match1?.[1] || match2?.[1] || match3?.[1]
|
|
76
|
+
|
|
77
|
+
if (tunnelId) {
|
|
78
|
+
writeFileSync(TUNNEL_ID_FILE, tunnelId)
|
|
79
|
+
return tunnelId
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.error('Failed to extract tunnel ID from output')
|
|
83
|
+
console.error('Output was:', output)
|
|
84
|
+
return null
|
|
85
|
+
} catch (error: any) {
|
|
86
|
+
// check if tunnel already exists
|
|
87
|
+
if (error.message?.includes('already exists')) {
|
|
88
|
+
try {
|
|
89
|
+
const { stdout } = await run(
|
|
90
|
+
`cloudflared tunnel list --name ${tunnelName} --output json`,
|
|
91
|
+
{ captureOutput: true }
|
|
92
|
+
)
|
|
93
|
+
const tunnels = JSON.parse(stdout)
|
|
94
|
+
if (tunnels.length > 0) {
|
|
95
|
+
const tunnelId = tunnels[0].id
|
|
96
|
+
writeFileSync(TUNNEL_ID_FILE, tunnelId)
|
|
97
|
+
return tunnelId
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error('Failed to parse tunnel list:', e)
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
console.error('Failed to create tunnel:', error)
|
|
104
|
+
}
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function runTunnel(port: number = 8081) {
|
|
110
|
+
// ensure cloudflared is installed
|
|
111
|
+
const isInstalled = await ensureCloudflared()
|
|
112
|
+
if (!isInstalled) {
|
|
113
|
+
console.error('Failed to install cloudflared')
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ensure authenticated
|
|
118
|
+
const isAuthenticated = await ensureAuthenticated()
|
|
119
|
+
if (!isAuthenticated) {
|
|
120
|
+
console.error('Failed to authenticate with Cloudflare')
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// get or create tunnel
|
|
125
|
+
const tunnelId = await getOrCreateTunnel()
|
|
126
|
+
if (!tunnelId) {
|
|
127
|
+
console.error('Failed to get or create tunnel')
|
|
128
|
+
process.exit(1)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// save the expected URL immediately so it's available right away
|
|
132
|
+
const expectedUrl = `https://${tunnelId}.cfargotunnel.com`
|
|
133
|
+
writeFileSync(join(TUNNEL_CONFIG_DIR, 'tunnel-url.txt'), expectedUrl)
|
|
134
|
+
console.info(`\n🌐 Tunnel URL: ${expectedUrl}`)
|
|
135
|
+
|
|
136
|
+
// get the public URL in the background
|
|
137
|
+
setTimeout(async () => {
|
|
138
|
+
try {
|
|
139
|
+
const { stdout } = await run(`cloudflared tunnel info ${tunnelId} --output json`, {
|
|
140
|
+
captureOutput: true,
|
|
141
|
+
silent: true,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const info = JSON.parse(stdout)
|
|
146
|
+
const hostname = info.hostname || `${tunnelId}.cfargotunnel.com`
|
|
147
|
+
writeFileSync(join(TUNNEL_CONFIG_DIR, 'tunnel-url.txt'), `https://${hostname}`)
|
|
148
|
+
} catch (e) {
|
|
149
|
+
// use fallback URL already saved
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// use fallback URL already saved
|
|
153
|
+
}
|
|
154
|
+
}, 3000)
|
|
155
|
+
|
|
156
|
+
// run tunnel in detached mode so it keeps running and gets managed by handleProcessExit
|
|
157
|
+
await run(`cloudflared tunnel run --url http://localhost:${port} ${tunnelId}`, {
|
|
158
|
+
detached: false,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Parse command line arguments
|
|
163
|
+
const args = process.argv.slice(2)
|
|
164
|
+
let port = 8081
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < args.length; i++) {
|
|
167
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
168
|
+
const portArg = args[i + 1]
|
|
169
|
+
if (portArg) {
|
|
170
|
+
const parsedPort = parseInt(portArg, 10)
|
|
171
|
+
if (!Number.isNaN(parsedPort)) {
|
|
172
|
+
port = parsedPort
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
runTunnel(port)
|