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.
@@ -27,9 +27,6 @@ jobs:
27
27
  - name: Install dependencies
28
28
  run: bun install --frozen-lockfile
29
29
 
30
- - name: Build
31
- run: bun run build
32
-
33
30
  - name: Configure Git
34
31
  run: |
35
32
  git config user.name "GitHub Actions Bot"
package/package.json CHANGED
@@ -1,12 +1,9 @@
1
1
  {
2
2
  "name": "bod-cli",
3
- "version": "0.7.4",
3
+ "version": "0.8.4",
4
4
  "type": "module",
5
5
  "bin": {
6
- "bod": "./dist/cli.js"
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: '0.1.0',
57
+ version: pkg.version,
57
58
  description: 'Unified CLI for the Bod product family',
58
59
  },
59
60
  args: {
@@ -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
- * Precedence:
86
- * 1. Explicit `--instance <name>` / `BOD_INSTANCE` → always the Bodify agent
87
- * proxy for that instance. Local shortcuts are skipped so the flag isn't
88
- * silently ignored while `bod serve` runs in the same dir.
89
- * 2. Otherwise, prefer direct-to-BodDB if we can discover the port + admin
90
- * password locally (`.bodify/serve.info.json` from `bod serve`, or
91
- * `.env BODDB_ADMIN_PASSWORD` + `bodify.yaml database.port`).
92
- * 3. Fall back to the Bodify agent proxy for the default instance.
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 explicitInstance = !!(process.env._BOD_INSTANCE_OVERRIDE || process.env.BOD_INSTANCE)
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 (!explicitInstance) {
98
- // 1. Live `bod serve` info file — authoritative (correct port even if scanned).
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, 'local serve')
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, 'direct via .env')
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
- // 3. Bodify agent proxy (deployed apps — or explicitly-requested instance).
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 appId = await resolveAppId(client, resolveAppName(appArg))
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: `agent ${url} → app ${appId}`,
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)) {