bod-cli 0.7.4 → 0.8.4
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/.github/workflows/publish.yml +0 -3
- package/package.json +2 -5
- package/src/cli.ts +2 -1
- package/src/commands/db.ts +64 -24
- package/dist/cli.js +0 -20578
package/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bod-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
|
-
"bod": "./
|
|
7
|
-
},
|
|
8
|
-
"scripts": {
|
|
9
|
-
"build": "bun build src/cli.ts --outdir dist --target node --format esm"
|
|
6
|
+
"bod": "./src/cli.ts"
|
|
10
7
|
},
|
|
11
8
|
"dependencies": {
|
|
12
9
|
"@inquirer/prompts": "^7.0.0",
|
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import serveCmd from './commands/serve'
|
|
|
16
16
|
import sshCmd from './commands/ssh'
|
|
17
17
|
import publishCmd from './commands/publish'
|
|
18
18
|
import dbCmd from './commands/db'
|
|
19
|
+
import pkg from '../package.json' with { type: 'json' }
|
|
19
20
|
|
|
20
21
|
// Parse --instance / --local / -l early so it's set before citty dispatches subcommands.
|
|
21
22
|
// Supports both `--instance=NAME` and `--instance NAME` forms.
|
|
@@ -53,7 +54,7 @@ const subCommands = {
|
|
|
53
54
|
const main = defineCommand({
|
|
54
55
|
meta: {
|
|
55
56
|
name: 'bod',
|
|
56
|
-
version:
|
|
57
|
+
version: pkg.version,
|
|
57
58
|
description: 'Unified CLI for the Bod product family',
|
|
58
59
|
},
|
|
59
60
|
args: {
|
package/src/commands/db.ts
CHANGED
|
@@ -79,42 +79,76 @@ function directTarget(port: number, token: string, label: string): DbTarget {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** True if cwd has a `bodify.yaml` (i.e. we're inside a Bodify app project). */
|
|
83
|
+
function isInAppFolder(): boolean {
|
|
84
|
+
return existsSync(join(process.cwd(), 'bodify.yaml'))
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
/**
|
|
83
88
|
* Resolve a target for DB commands.
|
|
84
89
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* Explicit, predictable rules — no silent auto-detection:
|
|
91
|
+
* • `-l` / `--local`: REQUIRE a running `bod serve` in cwd. Connects
|
|
92
|
+
* directly to its BodDB (port from `.bodify/serve.info.json`, or
|
|
93
|
+
* `.env BODDB_ADMIN_PASSWORD` + `bodify.yaml database.port`). Fails
|
|
94
|
+
* loudly if no local serve is discoverable — we never silently fall
|
|
95
|
+
* through to prod when `-l` was requested.
|
|
96
|
+
* • No `-l` (default): route through the Bodify agent proxy (remote prod
|
|
97
|
+
* by default, or the instance named by `--instance` / `BOD_INSTANCE`).
|
|
98
|
+
* Even when `bod serve` is running in the same directory, we stay on
|
|
99
|
+
* prod unless `-l` was passed — this is the opposite of the old
|
|
100
|
+
* behavior and prevents "which DB did I just write to?" confusion.
|
|
101
|
+
*
|
|
102
|
+
* Every resolved target carries a human label that the command layer prints
|
|
103
|
+
* as a banner before running, so the user can always see where writes land.
|
|
93
104
|
*/
|
|
94
|
-
async function resolveDbTarget(appArg: string | undefined): Promise<DbTarget> {
|
|
95
|
-
const
|
|
105
|
+
async function resolveDbTarget(appArg: string | undefined, wantLocal: boolean): Promise<DbTarget> {
|
|
106
|
+
const inApp = isInAppFolder()
|
|
107
|
+
const scopeLabel = inApp ? 'APP' : 'BODIFY-PLATFORM'
|
|
96
108
|
|
|
97
|
-
if (
|
|
98
|
-
//
|
|
109
|
+
if (wantLocal) {
|
|
110
|
+
// `-l` only makes sense inside an app folder — the Bodify platform DB
|
|
111
|
+
// isn't something you run locally via `bod serve`.
|
|
112
|
+
if (!inApp) {
|
|
113
|
+
console.error(chalk.red('--local (-l) only works inside a Bodify app folder (needs bodify.yaml).'))
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
99
116
|
const info = readServeInfo()
|
|
100
|
-
if (info) return directTarget(info.dbPort, info.dbAdminPassword,
|
|
101
|
-
// 2. .env BODDB_ADMIN_PASSWORD + bodify.yaml database.port — works without restarting serve.
|
|
117
|
+
if (info) return directTarget(info.dbPort, info.dbAdminPassword, `LOCAL ${scopeLabel} bod serve`)
|
|
102
118
|
const envPw = readDotEnv('BODDB_ADMIN_PASSWORD')
|
|
103
|
-
if (envPw) return directTarget(readDbPortFromYaml(), envPw,
|
|
119
|
+
if (envPw) return directTarget(readDbPortFromYaml(), envPw, `LOCAL ${scopeLabel} via .env`)
|
|
120
|
+
console.error(chalk.red('--local (-l) requires a running `bod serve` in this directory.'))
|
|
121
|
+
console.error(chalk.dim(' Expected `.bodify/serve.info.json` or BODDB_ADMIN_PASSWORD in .env — neither found.'))
|
|
122
|
+
process.exit(1)
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
//
|
|
125
|
+
// Remote: Bodify agent proxy. Inside an app folder → that app's DB;
|
|
126
|
+
// outside → the Bodify platform app's own DB.
|
|
107
127
|
const { url, apiKey } = getResolvedInstance(loadConfig())
|
|
108
128
|
const client = new BodClient(url, apiKey)
|
|
109
|
-
const
|
|
129
|
+
const effectiveAppArg = inApp ? appArg : (appArg ?? 'bodify')
|
|
130
|
+
const appId = await resolveAppId(client, resolveAppName(effectiveAppArg))
|
|
131
|
+
const instanceLabel = process.env._BOD_INSTANCE_OVERRIDE || process.env.BOD_INSTANCE || 'default'
|
|
110
132
|
return {
|
|
111
|
-
label: `
|
|
133
|
+
label: `REMOTE ${scopeLabel} ${instanceLabel} (${url}) → app ${appId}`,
|
|
112
134
|
request<T>(method: string, sub: string, body?: unknown): Promise<T> {
|
|
113
135
|
return client.request<T>(method, `/apps/${appId}${sub}`, body)
|
|
114
136
|
},
|
|
115
137
|
}
|
|
116
138
|
}
|
|
117
139
|
|
|
140
|
+
/** Print a clear banner showing the resolved DB target. Always on stderr. */
|
|
141
|
+
function printTargetBanner(target: DbTarget): void {
|
|
142
|
+
const isLocal = target.label.startsWith('LOCAL')
|
|
143
|
+
const color = isLocal ? chalk.cyan : chalk.yellow
|
|
144
|
+
console.error(color(`→ DB target: ${target.label}${isInAppFolder() ? '' : ' [no bodify.yaml in cwd]'}`))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Read --local / -l from argv before citty parses, so all subcommands see it. */
|
|
148
|
+
function readLocalFlag(): boolean {
|
|
149
|
+
return process.argv.slice(2).some(a => a === '--local' || a === '-l')
|
|
150
|
+
}
|
|
151
|
+
|
|
118
152
|
/** Read body from positional arg, --file, or stdin (in that order). */
|
|
119
153
|
async function readBody(positional: string | undefined, file: string | undefined): Promise<unknown> {
|
|
120
154
|
let raw: string | undefined
|
|
@@ -201,7 +235,8 @@ const getCmd = defineCommand({
|
|
|
201
235
|
},
|
|
202
236
|
async run({ args }) {
|
|
203
237
|
strictArgs(['path', 'app', 'a', 'deep', 'd', 'limit', 'n', 'offset', 'output', 'o'])
|
|
204
|
-
const target = await resolveDbTarget(args.app)
|
|
238
|
+
const target = await resolveDbTarget(args.app, readLocalFlag())
|
|
239
|
+
printTargetBanner(target)
|
|
205
240
|
const path = normalizePath(args.path)
|
|
206
241
|
const qs: string[] = []
|
|
207
242
|
if (!args.deep) {
|
|
@@ -229,7 +264,8 @@ const setCmd = defineCommand({
|
|
|
229
264
|
async run({ args }) {
|
|
230
265
|
strictArgs(['path', 'value', 'app', 'a', 'file', 'f'])
|
|
231
266
|
const body = await readBody(args.value, args.file)
|
|
232
|
-
const target = await resolveDbTarget(args.app)
|
|
267
|
+
const target = await resolveDbTarget(args.app, readLocalFlag())
|
|
268
|
+
printTargetBanner(target)
|
|
233
269
|
await target.request('PUT', `/db/${normalizePath(args.path)}`, body)
|
|
234
270
|
console.log(chalk.green(`✓ Set ${args.path}`))
|
|
235
271
|
},
|
|
@@ -246,7 +282,8 @@ const updateCmd = defineCommand({
|
|
|
246
282
|
async run({ args }) {
|
|
247
283
|
strictArgs(['path', 'value', 'app', 'a', 'file', 'f'])
|
|
248
284
|
const body = await readBody(args.value, args.file)
|
|
249
|
-
const target = await resolveDbTarget(args.app)
|
|
285
|
+
const target = await resolveDbTarget(args.app, readLocalFlag())
|
|
286
|
+
printTargetBanner(target)
|
|
250
287
|
await target.request('PATCH', `/db/${normalizePath(args.path)}`, body)
|
|
251
288
|
console.log(chalk.green(`✓ Updated ${args.path}`))
|
|
252
289
|
},
|
|
@@ -263,7 +300,8 @@ const pushCmd = defineCommand({
|
|
|
263
300
|
async run({ args }) {
|
|
264
301
|
strictArgs(['path', 'value', 'app', 'a', 'file', 'f'])
|
|
265
302
|
const body = await readBody(args.value, args.file)
|
|
266
|
-
const target = await resolveDbTarget(args.app)
|
|
303
|
+
const target = await resolveDbTarget(args.app, readLocalFlag())
|
|
304
|
+
printTargetBanner(target)
|
|
267
305
|
const res = await target.request<DbResponse>('POST', `/db/${normalizePath(args.path)}`, body)
|
|
268
306
|
console.log(chalk.green(`✓ Pushed → ${args.path}/${res.key}`))
|
|
269
307
|
},
|
|
@@ -282,7 +320,8 @@ const deleteCmd = defineCommand({
|
|
|
282
320
|
console.error(chalk.yellow(`Refusing to delete "${args.path}" without --confirm (-y).`))
|
|
283
321
|
process.exit(1)
|
|
284
322
|
}
|
|
285
|
-
const target = await resolveDbTarget(args.app)
|
|
323
|
+
const target = await resolveDbTarget(args.app, readLocalFlag())
|
|
324
|
+
printTargetBanner(target)
|
|
286
325
|
await target.request('DELETE', `/db/${normalizePath(args.path)}`)
|
|
287
326
|
console.log(chalk.green(`✓ Deleted ${args.path}`))
|
|
288
327
|
},
|
|
@@ -350,7 +389,8 @@ const queryCmd = defineCommand({
|
|
|
350
389
|
limit: args.limit ? Number(args.limit) : undefined,
|
|
351
390
|
offset: args.offset ? Number(args.offset) : undefined,
|
|
352
391
|
}
|
|
353
|
-
const target = await resolveDbTarget(args.app)
|
|
392
|
+
const target = await resolveDbTarget(args.app, readLocalFlag())
|
|
393
|
+
printTargetBanner(target)
|
|
354
394
|
const res = await target.request<DbResponse>('POST', `/query/${normalizePath(args.path)}`, body)
|
|
355
395
|
let rows = res.data as Array<Record<string, unknown>> | null
|
|
356
396
|
if (!args.deep && Array.isArray(rows)) {
|