bod-cli 0.3.1 → 0.5.1
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/dist/cli.js +7212 -33
- package/package.json +2 -1
- package/src/cli.ts +8 -2
- package/src/commands/deploy.ts +99 -23
- package/src/commands/env.ts +6 -1
- package/src/commands/publish.ts +87 -0
- package/src/commands/rollback.ts +2 -2
- package/src/config.ts +5 -6
- package/src/utils/output.ts +1 -0
- package/src/utils/resolve.ts +80 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bod-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bod": "./dist/cli.js"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"chalk": "^5.3.0",
|
|
14
14
|
"citty": "^0.1.6",
|
|
15
15
|
"cli-table3": "^0.6.5",
|
|
16
|
+
"yaml": "^2.8.3",
|
|
16
17
|
"zod": "^3.23.0"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
package/src/cli.ts
CHANGED
|
@@ -14,10 +14,13 @@ import removeCmd from './commands/remove'
|
|
|
14
14
|
import openCmd from './commands/open'
|
|
15
15
|
import serveCmd from './commands/serve'
|
|
16
16
|
import sshCmd from './commands/ssh'
|
|
17
|
+
import publishCmd from './commands/publish'
|
|
17
18
|
|
|
18
|
-
// Parse --instance early so it's set before citty dispatches subcommands
|
|
19
|
+
// Parse --instance / --local / -l early so it's set before citty dispatches subcommands
|
|
19
20
|
const instanceFlag = process.argv.find(a => a.startsWith('--instance='))?.split('=').slice(1).join('=')
|
|
21
|
+
const isLocal = process.argv.includes('--local') || process.argv.includes('-l')
|
|
20
22
|
if (instanceFlag) setInstanceOverride(instanceFlag)
|
|
23
|
+
else if (isLocal) setInstanceOverride('local')
|
|
21
24
|
|
|
22
25
|
const subCommands = {
|
|
23
26
|
login: loginCmd,
|
|
@@ -32,6 +35,7 @@ const subCommands = {
|
|
|
32
35
|
open: openCmd,
|
|
33
36
|
serve: serveCmd,
|
|
34
37
|
ssh: sshCmd,
|
|
38
|
+
publish: publishCmd,
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const main = defineCommand({
|
|
@@ -72,6 +76,7 @@ const main = defineCommand({
|
|
|
72
76
|
{ value: 'init', name: 'init — Initialize a project' },
|
|
73
77
|
{ value: 'add', name: 'add — Add a package' },
|
|
74
78
|
{ value: 'remove', name: 'remove — Remove a package' },
|
|
79
|
+
{ value: 'publish', name: 'publish — Publish package to registry' },
|
|
75
80
|
{ value: 'login', name: 'login — Add/switch instance' },
|
|
76
81
|
{ value: 'exit', name: 'exit — Exit' },
|
|
77
82
|
]
|
|
@@ -79,7 +84,7 @@ const main = defineCommand({
|
|
|
79
84
|
while (true) {
|
|
80
85
|
let command: string
|
|
81
86
|
try {
|
|
82
|
-
command = await select({ message: 'What would you like to do?', choices })
|
|
87
|
+
command = await select({ message: 'What would you like to do?', choices, theme: { prefix: { idle: '>', done: '>' }, icon: { cursor: '>' }, style: { help: () => '', keysHelpTip: () => '' } } })
|
|
83
88
|
} catch (e) {
|
|
84
89
|
if ((e as Error).name === 'ExitPromptError') return
|
|
85
90
|
throw e
|
|
@@ -135,6 +140,7 @@ async function getInteractiveArgs(command: string): Promise<string[] | null> {
|
|
|
135
140
|
if (!pkg) return null
|
|
136
141
|
return [pkg]
|
|
137
142
|
}
|
|
143
|
+
case 'publish': return []
|
|
138
144
|
default: return []
|
|
139
145
|
}
|
|
140
146
|
} catch (e) {
|
package/src/commands/deploy.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { defineCommand } from 'citty'
|
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import { loadConfig, getResolvedInstance } from '../config'
|
|
4
4
|
import { BodClient } from '../client'
|
|
5
|
-
import { resolveAppId, resolveAppName } from '../utils/resolve'
|
|
5
|
+
import { resolveAppId, resolveAppName, readInstanceFromYaml, readDeployModeFromYaml, readExcludesFromYaml, detectSiblingDeps } from '../utils/resolve'
|
|
6
6
|
|
|
7
7
|
async function detectBranch(explicit?: string): Promise<string> {
|
|
8
8
|
if (explicit) return explicit
|
|
@@ -11,36 +11,65 @@ async function detectBranch(explicit?: string): Promise<string> {
|
|
|
11
11
|
return branch || 'main'
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function formatSize(bytes: number): string {
|
|
15
|
+
if (bytes < 1024) return `${bytes} B`
|
|
16
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
17
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
async function uploadDeploy(client: BodClient, appId: string, branch: string) {
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const defaultExcludes = ['node_modules', '.git', 'dist']
|
|
22
|
+
const userExcludes = readExcludesFromYaml()
|
|
23
|
+
const allExcludes = [...new Set([...defaultExcludes, ...userExcludes])]
|
|
24
|
+
const excludeFlags = allExcludes.map(e => `--exclude=${e}`)
|
|
25
|
+
|
|
26
|
+
// Auto-detect file:.. dependencies and include them in the tarball
|
|
27
|
+
const siblingPaths = detectSiblingDeps()
|
|
28
|
+
if (siblingPaths.length) console.log(chalk.dim(`Including siblings: ${siblingPaths.join(', ')}`))
|
|
29
|
+
|
|
30
|
+
console.log(chalk.dim(`Packing source (excluding: ${allExcludes.join(', ')})...`))
|
|
31
|
+
const tmpFile = `${Bun.env.TMPDIR || '/tmp'}/bod-deploy-${Date.now()}.tar.gz`
|
|
32
|
+
const tar = Bun.spawnSync(['tar', 'cz', ...excludeFlags, '-f', tmpFile, '.', ...siblingPaths], { stderr: 'pipe' })
|
|
33
|
+
if (tar.exitCode !== 0) {
|
|
34
|
+
const errText = tar.stderr.toString()
|
|
35
|
+
throw new Error(`tar failed: ${errText}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const file = Bun.file(tmpFile)
|
|
39
|
+
const size = file.size
|
|
40
|
+
console.log(chalk.dim(`Uploading ${formatSize(size)}...`))
|
|
41
|
+
|
|
42
|
+
const start = Date.now()
|
|
20
43
|
await client.upload<any>(
|
|
21
44
|
`/apps/${appId}/deploy/upload`,
|
|
22
|
-
|
|
45
|
+
file.stream(),
|
|
23
46
|
{ 'x-branch': branch },
|
|
24
47
|
)
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
48
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1)
|
|
49
|
+
console.log(chalk.dim(`Uploaded ${formatSize(size)} in ${elapsed}s`))
|
|
50
|
+
|
|
51
|
+
// Cleanup temp file
|
|
52
|
+
try { (await import('fs')).unlinkSync(tmpFile) } catch {}
|
|
30
53
|
}
|
|
31
54
|
|
|
32
55
|
async function gitDeploy(client: BodClient, appId: string, branch: string) {
|
|
33
56
|
await client.post<any>(`/apps/${appId}/deploy`, { branch })
|
|
34
57
|
}
|
|
35
58
|
|
|
36
|
-
|
|
59
|
+
function branchToSubdomain(branch: string, domain: string, productionBranch: string): string {
|
|
60
|
+
if (branch === productionBranch) return domain
|
|
61
|
+
const sanitized = branch.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
62
|
+
return `${sanitized}--${domain}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function pollDeploy(client: BodClient, appId: string, since: number, instanceCaps?: { baseDomain?: string | null }, localConfig?: { localDomain?: string; caddyPort?: number } | null) {
|
|
37
66
|
console.log(chalk.dim('Waiting for deployment...'))
|
|
38
67
|
let lastStatus = ''
|
|
68
|
+
let lastLogTs = since
|
|
39
69
|
for (let i = 0; i < 120; i++) {
|
|
40
70
|
await Bun.sleep(2000)
|
|
41
71
|
const detail = await client.get<any>(`/apps/${appId}`)
|
|
42
72
|
const deps = detail.deployments ?? []
|
|
43
|
-
// Find the deployment created after we triggered (don't fall back to older ones)
|
|
44
73
|
const latest = deps.find((d: any) => (d.createdAt ?? 0) >= since)
|
|
45
74
|
if (!latest) continue
|
|
46
75
|
const status = latest.status
|
|
@@ -48,13 +77,49 @@ async function pollDeploy(client: BodClient, appId: string, since: number) {
|
|
|
48
77
|
console.log(chalk.dim(` Status: ${status}`))
|
|
49
78
|
lastStatus = status
|
|
50
79
|
}
|
|
80
|
+
|
|
81
|
+
// Tail logs during build/deploy
|
|
82
|
+
try {
|
|
83
|
+
const signals = await client.get<any[]>(`/apps/${appId}/signals?limit=50&from=${lastLogTs + 1}`)
|
|
84
|
+
if (Array.isArray(signals) && signals.length) {
|
|
85
|
+
// Signals come desc — reverse for chronological display
|
|
86
|
+
for (const s of [...signals].reverse()) {
|
|
87
|
+
const time = new Date(s.ts).toISOString().slice(11, 23)
|
|
88
|
+
const msg = s.message ?? ''
|
|
89
|
+
if (s.level === 'error' || s.source === 'stderr') {
|
|
90
|
+
console.log(chalk.dim(time) + ' ' + chalk.red(msg))
|
|
91
|
+
} else {
|
|
92
|
+
console.log(chalk.dim(time) + ' ' + chalk.dim(msg))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
lastLogTs = Math.max(...signals.map((s: any) => s.ts))
|
|
96
|
+
}
|
|
97
|
+
} catch { /* signal fetch is best-effort */ }
|
|
98
|
+
|
|
51
99
|
if (status === 'live') {
|
|
52
100
|
console.log(chalk.green(`✓ Deployment live!`))
|
|
53
|
-
if (detail.domain
|
|
101
|
+
if (detail.domain && latest.branch) {
|
|
102
|
+
const host = branchToSubdomain(latest.branch, detail.domain, detail.productionBranch ?? 'main')
|
|
103
|
+
const isPreview = host !== detail.domain
|
|
104
|
+
// For local instances, rewrite domain to localDomain variant
|
|
105
|
+
const baseDomain = instanceCaps?.baseDomain
|
|
106
|
+
const ld = localConfig?.localDomain
|
|
107
|
+
const port = localConfig?.caddyPort
|
|
108
|
+
let displayUrl: string
|
|
109
|
+
if (ld && baseDomain && host.endsWith(`.${baseDomain}`)) {
|
|
110
|
+
const localHost = host.slice(0, -(baseDomain.length + 1)) + `.${ld}`
|
|
111
|
+
displayUrl = `http://${localHost}${port ? `:${port}` : ''}`
|
|
112
|
+
} else {
|
|
113
|
+
displayUrl = `https://${host}`
|
|
114
|
+
}
|
|
115
|
+
console.log(` → ${displayUrl}`)
|
|
116
|
+
if (isPreview) console.log(chalk.yellow(` ⚠ Preview deployment (branch: ${latest.branch})`))
|
|
117
|
+
}
|
|
54
118
|
return
|
|
55
119
|
}
|
|
56
120
|
if (status === 'failed') {
|
|
57
121
|
console.error(chalk.red(`✗ Deployment failed`))
|
|
122
|
+
if (latest.error) console.error(chalk.red(` ${latest.error}`))
|
|
58
123
|
process.exit(1)
|
|
59
124
|
}
|
|
60
125
|
}
|
|
@@ -66,10 +131,18 @@ export default defineCommand({
|
|
|
66
131
|
args: {
|
|
67
132
|
app: { type: 'positional', description: 'App name (or reads from bodify.yaml)', required: false },
|
|
68
133
|
branch: { type: 'string', description: 'Branch to deploy (default: current git branch)' },
|
|
134
|
+
upload: { type: 'boolean', description: 'Upload local source instead of git clone', default: false },
|
|
69
135
|
},
|
|
70
136
|
async run({ args }) {
|
|
71
|
-
const { url, apiKey } = getResolvedInstance(loadConfig())
|
|
137
|
+
const { url, apiKey, name: instanceName, instance } = getResolvedInstance(loadConfig(), readInstanceFromYaml())
|
|
138
|
+
console.log(chalk.dim(`Using instance "${instanceName}"`))
|
|
72
139
|
const client = new BodClient(url, apiKey)
|
|
140
|
+
// Fetch local config for URL generation
|
|
141
|
+
const isLocal = url.startsWith('http://localhost')
|
|
142
|
+
let localConfig: { localDomain?: string; caddyPort?: number } | null = null
|
|
143
|
+
if (isLocal) {
|
|
144
|
+
try { localConfig = await client.get<any>('/config/public') } catch {}
|
|
145
|
+
}
|
|
73
146
|
|
|
74
147
|
const appName = resolveAppName(args.app)
|
|
75
148
|
|
|
@@ -77,8 +150,10 @@ export default defineCommand({
|
|
|
77
150
|
const branch = await detectBranch(args.branch)
|
|
78
151
|
const detail = await client.get<any>(`/apps/${appId}`)
|
|
79
152
|
|
|
153
|
+
const forceUpload = args.upload || readDeployModeFromYaml() === 'upload'
|
|
154
|
+
|
|
80
155
|
let hasRepo = !!detail.repo
|
|
81
|
-
if (!hasRepo) {
|
|
156
|
+
if (!hasRepo && !forceUpload) {
|
|
82
157
|
const proc = Bun.spawnSync(['git', 'remote', 'get-url', 'origin'])
|
|
83
158
|
const repo = proc.exitCode === 0 ? proc.stdout.toString().trim() : ''
|
|
84
159
|
if (repo) {
|
|
@@ -88,16 +163,17 @@ export default defineCommand({
|
|
|
88
163
|
}
|
|
89
164
|
}
|
|
90
165
|
|
|
91
|
-
|
|
166
|
+
const useUpload = forceUpload || !hasRepo
|
|
167
|
+
console.log(chalk.dim(`Deploying ${appName} (branch: ${branch})${useUpload ? ' via upload' : ''}...`))
|
|
92
168
|
|
|
93
|
-
const since = Date.now()
|
|
94
|
-
if (
|
|
95
|
-
await gitDeploy(client, appId, branch)
|
|
96
|
-
} else {
|
|
169
|
+
const since = Date.now() - 5000 // buffer for clock skew
|
|
170
|
+
if (useUpload) {
|
|
97
171
|
await uploadDeploy(client, appId, branch)
|
|
172
|
+
} else {
|
|
173
|
+
await gitDeploy(client, appId, branch)
|
|
98
174
|
}
|
|
99
175
|
|
|
100
176
|
console.log(chalk.green(`✓ Deployment queued`))
|
|
101
|
-
await pollDeploy(client, appId, since)
|
|
177
|
+
await pollDeploy(client, appId, since, instance.capabilities, localConfig)
|
|
102
178
|
},
|
|
103
179
|
})
|
package/src/commands/env.ts
CHANGED
|
@@ -32,8 +32,13 @@ function parseDotEnv(content: string): Record<string, string> {
|
|
|
32
32
|
if (eqIdx === -1) continue
|
|
33
33
|
const key = trimmed.slice(0, eqIdx).trim()
|
|
34
34
|
let value = trimmed.slice(eqIdx + 1).trim()
|
|
35
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
|
|
35
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
36
36
|
value = value.slice(1, -1)
|
|
37
|
+
} else {
|
|
38
|
+
// Strip inline comments (unquoted values only)
|
|
39
|
+
const commentIdx = value.indexOf(' #')
|
|
40
|
+
if (commentIdx !== -1) value = value.slice(0, commentIdx).trimEnd()
|
|
41
|
+
}
|
|
37
42
|
env[key] = value
|
|
38
43
|
}
|
|
39
44
|
return env
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { defineCommand } from 'citty'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { readFileSync } from 'fs'
|
|
4
|
+
import { loadConfig, getResolvedInstance } from '../config'
|
|
5
|
+
import { BodClient } from '../client'
|
|
6
|
+
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
meta: { name: 'publish', description: 'Publish a package to Bodify registry' },
|
|
9
|
+
args: {
|
|
10
|
+
tag: { type: 'string', description: 'Dist-tag (default: latest)', default: 'latest' },
|
|
11
|
+
},
|
|
12
|
+
async run({ args }) {
|
|
13
|
+
// Read package.json
|
|
14
|
+
let pkg: { name: string; version: string; description?: string }
|
|
15
|
+
try {
|
|
16
|
+
pkg = JSON.parse(readFileSync('package.json', 'utf-8'))
|
|
17
|
+
} catch {
|
|
18
|
+
console.error(chalk.red('No package.json found in current directory'))
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
if (!pkg.name || !pkg.version) {
|
|
22
|
+
console.error(chalk.red('package.json must have name and version'))
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { url, apiKey, name: instanceName, instance } = getResolvedInstance(loadConfig())
|
|
27
|
+
if (!instance.capabilities.registryEnabled) {
|
|
28
|
+
console.error(chalk.red(`Registry not enabled on instance "${instanceName}"`))
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(chalk.dim(`Publishing ${pkg.name}@${pkg.version} to ${instanceName}...`))
|
|
33
|
+
|
|
34
|
+
// Pack tarball (npm pack works with any package manager)
|
|
35
|
+
const pack = Bun.spawnSync(['npm', 'pack', '--pack-destination', '.'], { stderr: 'pipe', stdout: 'pipe' })
|
|
36
|
+
if (pack.exitCode !== 0) {
|
|
37
|
+
console.error(chalk.red(`npm pack failed: ${pack.stderr.toString()}`))
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
const tarballPath = pack.stdout.toString().trim().split('\n').pop()!
|
|
41
|
+
const tarballData = readFileSync(tarballPath)
|
|
42
|
+
const tarballBase64 = tarballData.toString('base64')
|
|
43
|
+
|
|
44
|
+
// Build npm-compatible publish body
|
|
45
|
+
const distTag = args.tag || 'latest'
|
|
46
|
+
const safeName = pkg.name.replace('/', '%2f')
|
|
47
|
+
const tarballFilename = `${pkg.name.replace('@', '').replace('/', '-')}-${pkg.version}.tgz`
|
|
48
|
+
|
|
49
|
+
const body = {
|
|
50
|
+
name: pkg.name,
|
|
51
|
+
'dist-tags': { [distTag]: pkg.version },
|
|
52
|
+
versions: {
|
|
53
|
+
[pkg.version]: {
|
|
54
|
+
name: pkg.name,
|
|
55
|
+
version: pkg.version,
|
|
56
|
+
description: pkg.description,
|
|
57
|
+
dist: {
|
|
58
|
+
tarball: `${url}/api/registry/${safeName}/-/${tarballFilename}`,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
_attachments: {
|
|
63
|
+
[tarballFilename]: {
|
|
64
|
+
content_type: 'application/octet-stream',
|
|
65
|
+
data: tarballBase64,
|
|
66
|
+
length: tarballData.length,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const client = new BodClient(url, apiKey)
|
|
72
|
+
try {
|
|
73
|
+
await client.put(`/registry/${encodeURIComponent(pkg.name)}`, body)
|
|
74
|
+
console.log(chalk.green(`✓ Published ${pkg.name}@${pkg.version}`))
|
|
75
|
+
} catch (e: any) {
|
|
76
|
+
if (e.message?.includes('409')) {
|
|
77
|
+
console.error(chalk.red(`${pkg.name}@${pkg.version} already exists`))
|
|
78
|
+
} else {
|
|
79
|
+
console.error(chalk.red(`Publish failed: ${e.message}`))
|
|
80
|
+
}
|
|
81
|
+
process.exit(1)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Cleanup tarball
|
|
85
|
+
try { (await import('fs')).unlinkSync(tarballPath) } catch {}
|
|
86
|
+
},
|
|
87
|
+
})
|
package/src/commands/rollback.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { defineCommand } from 'citty'
|
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import { loadConfig, getResolvedInstance } from '../config'
|
|
4
4
|
import { BodClient } from '../client'
|
|
5
|
-
import { resolveAppId, resolveAppName } from '../utils/resolve'
|
|
5
|
+
import { resolveAppId, resolveAppName, readInstanceFromYaml } from '../utils/resolve'
|
|
6
6
|
|
|
7
7
|
export default defineCommand({
|
|
8
8
|
meta: { name: 'rollback', description: 'Rollback to previous deployment' },
|
|
@@ -11,7 +11,7 @@ export default defineCommand({
|
|
|
11
11
|
branch: { type: 'string', description: 'Branch to rollback (default: production branch)' },
|
|
12
12
|
},
|
|
13
13
|
async run({ args }) {
|
|
14
|
-
const { url, apiKey } = getResolvedInstance(loadConfig())
|
|
14
|
+
const { url, apiKey } = getResolvedInstance(loadConfig(), readInstanceFromYaml())
|
|
15
15
|
const client = new BodClient(url, apiKey)
|
|
16
16
|
|
|
17
17
|
const appName = resolveAppName(args.app)
|
package/src/config.ts
CHANGED
|
@@ -63,15 +63,14 @@ export function getDefaultInstance(config: Config): { name: string; instance: In
|
|
|
63
63
|
return { name, instance: config.instances[name] }
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** Override instance from --instance flag or BOD_INSTANCE env var
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
/** Override instance from --instance flag or BOD_INSTANCE env var.
|
|
67
|
+
* Uses process.env to avoid bundler module-duplication issues. */
|
|
69
68
|
export function setInstanceOverride(name: string | undefined) {
|
|
70
|
-
|
|
69
|
+
if (name) process.env._BOD_INSTANCE_OVERRIDE = name
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
export function getResolvedInstance(config: Config): { name: string; url: string; apiKey: string; instance: Instance } {
|
|
74
|
-
const override =
|
|
72
|
+
export function getResolvedInstance(config: Config, _yamlInstance?: string): { name: string; url: string; apiKey: string; instance: Instance } {
|
|
73
|
+
const override = process.env._BOD_INSTANCE_OVERRIDE ?? process.env.BOD_INSTANCE
|
|
75
74
|
if (override) {
|
|
76
75
|
const inst = config.instances[override]
|
|
77
76
|
if (!inst) throw new Error(`Instance "${override}" not found in config. Available: ${Object.keys(config.instances).join(', ')}`)
|
package/src/utils/output.ts
CHANGED
|
@@ -7,6 +7,7 @@ export function printTable(rows: Record<string, unknown>[], columns?: string[])
|
|
|
7
7
|
const table = new Table({
|
|
8
8
|
head: keys.map(k => chalk.bold(k)),
|
|
9
9
|
style: { head: [], border: [] },
|
|
10
|
+
chars: { top: '-', 'top-mid': '+', 'top-left': '+', 'top-right': '+', bottom: '-', 'bottom-mid': '+', 'bottom-left': '+', 'bottom-right': '+', left: '|', 'left-mid': '+', mid: '-', 'mid-mid': '+', right: '|', 'right-mid': '+', middle: '|' },
|
|
10
11
|
})
|
|
11
12
|
for (const row of rows) {
|
|
12
13
|
table.push(keys.map(k => String(row[k] ?? '')))
|
package/src/utils/resolve.ts
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import { readFileSync } from 'fs'
|
|
3
|
+
import { parse } from 'yaml'
|
|
3
4
|
import { BodClient } from '../client'
|
|
4
5
|
|
|
6
|
+
interface BodifyYaml {
|
|
7
|
+
name?: string
|
|
8
|
+
instance?: string
|
|
9
|
+
deploy?: 'upload' | 'git'
|
|
10
|
+
exclude?: string[]
|
|
11
|
+
[key: string]: unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let _parsedYaml: BodifyYaml | null | undefined
|
|
15
|
+
|
|
16
|
+
function readYaml(): BodifyYaml | null {
|
|
17
|
+
if (_parsedYaml !== undefined) return _parsedYaml
|
|
18
|
+
try {
|
|
19
|
+
_parsedYaml = parse(readFileSync('bodify.yaml', 'utf-8')) as BodifyYaml
|
|
20
|
+
} catch {
|
|
21
|
+
_parsedYaml = null
|
|
22
|
+
}
|
|
23
|
+
return _parsedYaml
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
export async function resolveAppId(client: BodClient, nameOrId: string): Promise<string> {
|
|
6
27
|
const apps = await client.get<any[]>('/apps')
|
|
7
28
|
const match = apps.find(a => a.name === nameOrId || a.id === nameOrId)
|
|
@@ -13,10 +34,66 @@ export async function resolveAppId(client: BodClient, nameOrId: string): Promise
|
|
|
13
34
|
}
|
|
14
35
|
|
|
15
36
|
export function readAppNameFromYaml(): string | undefined {
|
|
37
|
+
return readYaml()?.name
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readInstanceFromYaml(): string | undefined {
|
|
41
|
+
return readYaml()?.instance
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function readDeployModeFromYaml(): 'upload' | 'git' | undefined {
|
|
45
|
+
const mode = readYaml()?.deploy
|
|
46
|
+
return mode === 'upload' ? 'upload' : mode === 'git' ? 'git' : undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readExcludesFromYaml(): string[] {
|
|
50
|
+
return readYaml()?.exclude ?? []
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Scan all package.json files for file:.. dependencies and return unique relative paths */
|
|
54
|
+
export function detectSiblingDeps(): string[] {
|
|
55
|
+
const { existsSync, readFileSync, readdirSync } = require('fs')
|
|
56
|
+
const paths = new Set<string>()
|
|
57
|
+
|
|
58
|
+
const scan = (pkgPath: string) => {
|
|
59
|
+
if (!existsSync(pkgPath)) return
|
|
60
|
+
try {
|
|
61
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
62
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
63
|
+
if (!pkg[field]) continue
|
|
64
|
+
for (const ver of Object.values(pkg[field]) as string[]) {
|
|
65
|
+
if (ver.startsWith('file:') && ver.includes('..')) {
|
|
66
|
+
// Normalize: resolve relative to project root
|
|
67
|
+
const rel = ver.slice(5) // strip 'file:'
|
|
68
|
+
// From package.json dir, resolve the relative path, then make it relative to cwd
|
|
69
|
+
const { resolve, relative } = require('path')
|
|
70
|
+
const { dirname } = require('path')
|
|
71
|
+
const abs = resolve(dirname(pkgPath), rel)
|
|
72
|
+
const fromCwd = relative(process.cwd(), abs)
|
|
73
|
+
if (fromCwd.startsWith('..') && existsSync(abs)) paths.add(fromCwd)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Root package.json
|
|
81
|
+
scan('package.json')
|
|
82
|
+
|
|
83
|
+
// Workspace packages
|
|
16
84
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
85
|
+
const root = JSON.parse(readFileSync('package.json', 'utf8'))
|
|
86
|
+
for (const pattern of root.workspaces ?? []) {
|
|
87
|
+
const base = pattern.replace(/\/?\*$/, '')
|
|
88
|
+
if (existsSync(base)) {
|
|
89
|
+
for (const dir of readdirSync(base)) {
|
|
90
|
+
scan(`${base}/${dir}/package.json`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
return [...paths]
|
|
20
97
|
}
|
|
21
98
|
|
|
22
99
|
/** Resolve app name from explicit arg or bodify.yaml fallback. Logs clearly when falling back. */
|