bod-cli 0.7.4 → 0.8.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 CHANGED
@@ -20151,26 +20151,47 @@ function directTarget(port, token, label) {
20151
20151
  }
20152
20152
  };
20153
20153
  }
20154
- async function resolveDbTarget(appArg) {
20155
- const explicitInstance = !!(process.env._BOD_INSTANCE_OVERRIDE || process.env.BOD_INSTANCE);
20156
- if (!explicitInstance) {
20154
+ function isInAppFolder() {
20155
+ return existsSync6(join6(process.cwd(), "bodify.yaml"));
20156
+ }
20157
+ async function resolveDbTarget(appArg, wantLocal) {
20158
+ const inApp = isInAppFolder();
20159
+ const scopeLabel = inApp ? "APP" : "BODIFY-PLATFORM";
20160
+ if (wantLocal) {
20161
+ if (!inApp) {
20162
+ console.error(source_default.red("--local (-l) only works inside a Bodify app folder (needs bodify.yaml)."));
20163
+ process.exit(1);
20164
+ }
20157
20165
  const info = readServeInfo();
20158
20166
  if (info)
20159
- return directTarget(info.dbPort, info.dbAdminPassword, "local serve");
20167
+ return directTarget(info.dbPort, info.dbAdminPassword, `LOCAL ${scopeLabel} bod serve`);
20160
20168
  const envPw = readDotEnv("BODDB_ADMIN_PASSWORD");
20161
20169
  if (envPw)
20162
- return directTarget(readDbPortFromYaml(), envPw, "direct via .env");
20170
+ return directTarget(readDbPortFromYaml(), envPw, `LOCAL ${scopeLabel} via .env`);
20171
+ console.error(source_default.red("--local (-l) requires a running `bod serve` in this directory."));
20172
+ console.error(source_default.dim(" Expected `.bodify/serve.info.json` or BODDB_ADMIN_PASSWORD in .env — neither found."));
20173
+ process.exit(1);
20163
20174
  }
20164
20175
  const { url, apiKey } = getResolvedInstance(loadConfig());
20165
20176
  const client = new BodClient(url, apiKey);
20166
- const appId = await resolveAppId(client, resolveAppName(appArg));
20177
+ const effectiveAppArg = inApp ? appArg : appArg ?? "bodify";
20178
+ const appId = await resolveAppId(client, resolveAppName(effectiveAppArg));
20179
+ const instanceLabel = process.env._BOD_INSTANCE_OVERRIDE || process.env.BOD_INSTANCE || "default";
20167
20180
  return {
20168
- label: `agent ${url} → app ${appId}`,
20181
+ label: `REMOTE ${scopeLabel} ${instanceLabel} (${url}) → app ${appId}`,
20169
20182
  request(method, sub, body) {
20170
20183
  return client.request(method, `/apps/${appId}${sub}`, body);
20171
20184
  }
20172
20185
  };
20173
20186
  }
20187
+ function printTargetBanner(target) {
20188
+ const isLocal = target.label.startsWith("LOCAL");
20189
+ const color = isLocal ? source_default.cyan : source_default.yellow;
20190
+ console.error(color(`→ DB target: ${target.label}${isInAppFolder() ? "" : " [no bodify.yaml in cwd]"}`));
20191
+ }
20192
+ function readLocalFlag() {
20193
+ return process.argv.slice(2).some((a2) => a2 === "--local" || a2 === "-l");
20194
+ }
20174
20195
  async function readBody(positional, file) {
20175
20196
  let raw;
20176
20197
  if (positional)
@@ -20251,7 +20272,8 @@ var getCmd = defineCommand2({
20251
20272
  },
20252
20273
  async run({ args }) {
20253
20274
  strictArgs(["path", "app", "a", "deep", "d", "limit", "n", "offset", "output", "o"]);
20254
- const target = await resolveDbTarget(args.app);
20275
+ const target = await resolveDbTarget(args.app, readLocalFlag());
20276
+ printTargetBanner(target);
20255
20277
  const path = normalizePath(args.path);
20256
20278
  const qs = [];
20257
20279
  if (!args.deep) {
@@ -20280,7 +20302,8 @@ var setCmd2 = defineCommand2({
20280
20302
  async run({ args }) {
20281
20303
  strictArgs(["path", "value", "app", "a", "file", "f"]);
20282
20304
  const body = await readBody(args.value, args.file);
20283
- const target = await resolveDbTarget(args.app);
20305
+ const target = await resolveDbTarget(args.app, readLocalFlag());
20306
+ printTargetBanner(target);
20284
20307
  await target.request("PUT", `/db/${normalizePath(args.path)}`, body);
20285
20308
  console.log(source_default.green(`✓ Set ${args.path}`));
20286
20309
  }
@@ -20296,7 +20319,8 @@ var updateCmd = defineCommand2({
20296
20319
  async run({ args }) {
20297
20320
  strictArgs(["path", "value", "app", "a", "file", "f"]);
20298
20321
  const body = await readBody(args.value, args.file);
20299
- const target = await resolveDbTarget(args.app);
20322
+ const target = await resolveDbTarget(args.app, readLocalFlag());
20323
+ printTargetBanner(target);
20300
20324
  await target.request("PATCH", `/db/${normalizePath(args.path)}`, body);
20301
20325
  console.log(source_default.green(`✓ Updated ${args.path}`));
20302
20326
  }
@@ -20312,7 +20336,8 @@ var pushCmd = defineCommand2({
20312
20336
  async run({ args }) {
20313
20337
  strictArgs(["path", "value", "app", "a", "file", "f"]);
20314
20338
  const body = await readBody(args.value, args.file);
20315
- const target = await resolveDbTarget(args.app);
20339
+ const target = await resolveDbTarget(args.app, readLocalFlag());
20340
+ printTargetBanner(target);
20316
20341
  const res = await target.request("POST", `/db/${normalizePath(args.path)}`, body);
20317
20342
  console.log(source_default.green(`✓ Pushed → ${args.path}/${res.key}`));
20318
20343
  }
@@ -20330,7 +20355,8 @@ var deleteCmd = defineCommand2({
20330
20355
  console.error(source_default.yellow(`Refusing to delete "${args.path}" without --confirm (-y).`));
20331
20356
  process.exit(1);
20332
20357
  }
20333
- const target = await resolveDbTarget(args.app);
20358
+ const target = await resolveDbTarget(args.app, readLocalFlag());
20359
+ printTargetBanner(target);
20334
20360
  await target.request("DELETE", `/db/${normalizePath(args.path)}`);
20335
20361
  console.log(source_default.green(`✓ Deleted ${args.path}`));
20336
20362
  }
@@ -20397,7 +20423,8 @@ var queryCmd = defineCommand2({
20397
20423
  limit: args.limit ? Number(args.limit) : undefined,
20398
20424
  offset: args.offset ? Number(args.offset) : undefined
20399
20425
  };
20400
- const target = await resolveDbTarget(args.app);
20426
+ const target = await resolveDbTarget(args.app, readLocalFlag());
20427
+ printTargetBanner(target);
20401
20428
  const res = await target.request("POST", `/query/${normalizePath(args.path)}`, body);
20402
20429
  let rows = res.data;
20403
20430
  if (!args.deep && Array.isArray(rows)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bod-cli",
3
- "version": "0.7.4",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "bod": "./dist/cli.js"
@@ -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)) {