@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.
@@ -0,0 +1,197 @@
1
+ import fs from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import { run } from './run'
6
+
7
+ export interface UncloudConfig {
8
+ context?: string
9
+ }
10
+
11
+ function ucCmd(config?: UncloudConfig): string {
12
+ return config?.context ? `uc -c ${config.context}` : 'uc'
13
+ }
14
+
15
+ export async function checkUncloudCLI(): Promise<void> {
16
+ try {
17
+ // uc has no --version flag; `uc version` is the cobra subcommand
18
+ const { stdout } = await run('uc version', { silent: true, captureOutput: true })
19
+ console.info(`āœ… uncloud cli installed (${stdout.trim().split('\n')[0]})`)
20
+ } catch (error) {
21
+ throw new Error(
22
+ `uncloud cli check failed (install: curl -fsS https://get.uncloud.run/install.sh | sh): ${error}`
23
+ )
24
+ }
25
+ }
26
+
27
+ function ensureUncloudContext(host: string, sshKey: string, contextName: string): void {
28
+ const configDir = join(homedir(), '.config', 'uncloud')
29
+ const configPath = join(configDir, 'config.yaml')
30
+
31
+ if (!fs.existsSync(configDir)) {
32
+ fs.mkdirSync(configDir, { recursive: true })
33
+ }
34
+
35
+ // check if config already has context pointing to correct host
36
+ if (fs.existsSync(configPath)) {
37
+ const existing = fs.readFileSync(configPath, 'utf-8')
38
+ const hostname = host.split('@')[1]
39
+ if (hostname && existing.includes(`${contextName}:`) && existing.includes(hostname)) {
40
+ console.info(`āœ… uncloud config already has ${contextName} context for ${host}`)
41
+ return
42
+ }
43
+ }
44
+
45
+ // only create config if it doesn't exist - don't overwrite existing config
46
+ // which may have other contexts
47
+ if (!fs.existsSync(configPath)) {
48
+ const config = `current_context: ${contextName}
49
+ contexts:
50
+ ${contextName}:
51
+ connections:
52
+ - ssh: ${host}
53
+ ssh_key_file: ${sshKey}
54
+ `
55
+ fs.writeFileSync(configPath, config)
56
+ console.info(`āœ… created uncloud config at ${configPath}`)
57
+ } else {
58
+ console.info(`āœ… using existing uncloud config (context: ${contextName})`)
59
+ }
60
+ }
61
+
62
+ export async function initUncloud(
63
+ host: string,
64
+ sshKey: string,
65
+ options: { noDNS?: boolean; noCaddy?: boolean; context?: string } = {}
66
+ ): Promise<void> {
67
+ console.info('\nšŸš€ initializing uncloud...\n')
68
+
69
+ const uc = ucCmd(options)
70
+ const contextName = options.context || 'default'
71
+
72
+ // check if we already have local context that works
73
+ try {
74
+ await run(`${uc} ls`, { silent: true })
75
+ console.info('āœ… local cluster context exists and connected')
76
+ return
77
+ } catch {
78
+ // no local context - check if server has uncloud running
79
+ }
80
+
81
+ const sshCmd = `ssh -i ${sshKey} -o StrictHostKeyChecking=no ${host}`
82
+
83
+ // check if server already has uncloud daemon running
84
+ let serverHasUncloud = false
85
+ try {
86
+ await run(`${sshCmd} "systemctl is-active uncloud.service"`, { silent: true })
87
+ serverHasUncloud = true
88
+ console.info('āœ… server has uncloud daemon running')
89
+ } catch {
90
+ try {
91
+ await run(`${sshCmd} "test -f /usr/local/bin/uncloudd"`, { silent: true })
92
+ serverHasUncloud = true
93
+ console.info('āœ… server has uncloud installed')
94
+ } catch {
95
+ // server doesn't have uncloud - needs fresh init
96
+ }
97
+ }
98
+
99
+ if (serverHasUncloud) {
100
+ ensureUncloudContext(host, sshKey, contextName)
101
+ console.info(' (skipping init to avoid cluster reset)')
102
+ return
103
+ }
104
+
105
+ // fresh server - need to initialize
106
+ console.info('šŸ“¦ uncloud not installed on server - initializing...')
107
+
108
+ const flags: string[] = []
109
+ if (options.noDNS) flags.push('--no-dns')
110
+ if (options.noCaddy) flags.push('--no-caddy')
111
+ const flagStr = flags.join(' ')
112
+
113
+ await run(`echo "y" | uc machine init ${host} -i ${sshKey} ${flagStr}`)
114
+
115
+ console.info('āœ… uncloud initialized')
116
+ }
117
+
118
+ export async function pushImage(
119
+ imageName: string,
120
+ config?: UncloudConfig
121
+ ): Promise<void> {
122
+ console.info('\nšŸ“¤ pushing image to cluster...\n')
123
+
124
+ await run(`${ucCmd(config)} image push ${imageName}`)
125
+ console.info('āœ… image pushed')
126
+ }
127
+
128
+ export async function deployStack(
129
+ composeFile: string,
130
+ options?: { profile?: string; service?: string; recreate?: boolean } & UncloudConfig
131
+ ): Promise<void> {
132
+ const service = options?.service
133
+ console.info(`\nšŸ“¦ deploying ${service ?? 'stack'}...\n`)
134
+
135
+ const profileFlag = options?.profile ? `--profile ${options.profile}` : ''
136
+ // --recreate forces fresh containers even when config is unchanged (picks up the new
137
+ // :latest image). pass recreate:false to let uncloud restart a service ONLY when its
138
+ // image/config actually changed — used for zero, whose restarts drop every websocket
139
+ // connection and cause a client reconnect storm.
140
+ const recreateFlag = options?.recreate === false ? '' : '--recreate'
141
+ const serviceArg = service ? ` ${service}` : ''
142
+ await run(
143
+ `${ucCmd(options)} deploy -f ${composeFile} ${profileFlag} ${recreateFlag} --yes${serviceArg}`
144
+ )
145
+
146
+ console.info('\nāœ… deployment complete!')
147
+ }
148
+
149
+ export async function showStatus(config?: UncloudConfig): Promise<void> {
150
+ console.info('\nšŸ“Š deployment status:\n')
151
+
152
+ try {
153
+ await run(`${ucCmd(config)} ls`)
154
+ } catch {
155
+ console.error('could not fetch status')
156
+ }
157
+ }
158
+
159
+ export async function showContainers(config?: UncloudConfig): Promise<void> {
160
+ console.info('\nšŸ“¦ container status:\n')
161
+
162
+ try {
163
+ await run(`${ucCmd(config)} ps`)
164
+ } catch {
165
+ console.error('could not fetch container status')
166
+ }
167
+ }
168
+
169
+ export async function startService(
170
+ service?: string,
171
+ config?: UncloudConfig
172
+ ): Promise<void> {
173
+ const target = service || 'all services'
174
+ console.info(`\nā–¶ļø starting ${target}...\n`)
175
+
176
+ try {
177
+ await run(`${ucCmd(config)} start${service ? ` ${service}` : ''}`)
178
+ console.info(`āœ… ${target} started`)
179
+ } catch {
180
+ throw new Error(`failed to start ${target}`)
181
+ }
182
+ }
183
+
184
+ export async function stopService(
185
+ service?: string,
186
+ config?: UncloudConfig
187
+ ): Promise<void> {
188
+ const target = service || 'all services'
189
+ console.info(`\nā¹ļø stopping ${target}...\n`)
190
+
191
+ try {
192
+ await run(`${ucCmd(config)} stop${service ? ` ${service}` : ''}`)
193
+ console.info(`āœ… ${target} stopped`)
194
+ } catch {
195
+ throw new Error(`failed to stop ${target}`)
196
+ }
197
+ }
@@ -0,0 +1,33 @@
1
+ import { checkPort } from './check-port'
2
+
3
+ interface WaitForPortOptions {
4
+ host?: string
5
+ intervalMs?: number
6
+ timeoutMs?: number
7
+ retries?: number
8
+ }
9
+
10
+ export async function waitForPort(
11
+ port: number,
12
+ options: WaitForPortOptions = {}
13
+ ): Promise<void> {
14
+ const {
15
+ host = '127.0.0.1',
16
+ intervalMs = 1000,
17
+ timeoutMs = 30000,
18
+ retries = Math.floor(timeoutMs / intervalMs),
19
+ } = options
20
+
21
+ for (let i = 0; i < retries; i++) {
22
+ const isOpen = await checkPort(port, host)
23
+ if (isOpen) {
24
+ return
25
+ }
26
+
27
+ if (i < retries - 1) {
28
+ await new Promise((resolve) => setTimeout(resolve, intervalMs))
29
+ }
30
+ }
31
+
32
+ throw new Error(`Port ${port} on ${host} did not become available after ${timeoutMs}ms`)
33
+ }
@@ -0,0 +1,8 @@
1
+ import fs from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export function getZeroVersion() {
5
+ const packageJsonPath = join(process.cwd(), 'package.json')
6
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
7
+ return packageJson.dependencies?.['@rocicorp/zero']?.replace(/^[\^~]/, '')
8
+ }