@dboio/cli 0.9.8 → 0.11.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.
Files changed (38) hide show
  1. package/README.md +172 -70
  2. package/bin/dbo.js +2 -0
  3. package/bin/postinstall.js +9 -1
  4. package/package.json +3 -3
  5. package/plugins/claude/dbo/commands/dbo.md +3 -3
  6. package/plugins/claude/dbo/skills/cli/SKILL.md +3 -3
  7. package/src/commands/add.js +50 -0
  8. package/src/commands/clone.js +720 -552
  9. package/src/commands/content.js +7 -3
  10. package/src/commands/deploy.js +22 -7
  11. package/src/commands/diff.js +41 -3
  12. package/src/commands/init.js +42 -79
  13. package/src/commands/input.js +5 -0
  14. package/src/commands/login.js +2 -2
  15. package/src/commands/mv.js +3 -0
  16. package/src/commands/output.js +8 -10
  17. package/src/commands/pull.js +268 -87
  18. package/src/commands/push.js +814 -94
  19. package/src/commands/rm.js +4 -1
  20. package/src/commands/status.js +12 -1
  21. package/src/commands/sync.js +71 -0
  22. package/src/lib/client.js +10 -0
  23. package/src/lib/config.js +80 -8
  24. package/src/lib/delta.js +178 -25
  25. package/src/lib/diff.js +150 -20
  26. package/src/lib/folder-icon.js +120 -0
  27. package/src/lib/ignore.js +2 -3
  28. package/src/lib/input-parser.js +37 -10
  29. package/src/lib/metadata-templates.js +21 -4
  30. package/src/lib/migrations.js +75 -0
  31. package/src/lib/save-to-disk.js +1 -1
  32. package/src/lib/scaffold.js +58 -3
  33. package/src/lib/structure.js +158 -21
  34. package/src/lib/toe-stepping.js +381 -0
  35. package/src/migrations/001-transaction-key-preset-scope.js +35 -0
  36. package/src/migrations/002-move-entity-dirs-to-lib.js +190 -0
  37. package/src/migrations/003-move-deploy-config.js +50 -0
  38. package/src/migrations/004-rename-output-files.js +101 -0
@@ -9,6 +9,7 @@ import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/m
9
9
  import { checkStoredTicket, applyStoredTicketToSubmission, clearGlobalTicket, getGlobalTicket, getRecordTicket } from '../lib/ticketing.js';
10
10
  import { resolveTransactionKey } from '../lib/transaction-key.js';
11
11
  import { log } from '../lib/logger.js';
12
+ import { runPendingMigrations } from '../lib/migrations.js';
12
13
 
13
14
  function collect(value, previous) {
14
15
  return previous.concat([value]);
@@ -18,7 +19,7 @@ export const contentCommand = new Command('content')
18
19
  .description('Get, deploy, or pull content from DBO.io')
19
20
  .argument('[uid]', 'Content UID')
20
21
  .option('-o, --output <path>', 'Save content to local file')
21
- .option('--format <type>', 'Output format')
22
+ .option('--template <value>', 'Output template (e.g., json_raw, html, txt, or a content UID)')
22
23
  .option('--no-minify', 'Disable minification')
23
24
  .option('--json', 'Output raw JSON')
24
25
  .option('-v, --verbose', 'Show HTTP request details')
@@ -37,8 +38,10 @@ const deployCmd = new Command('deploy')
37
38
  .option('--json', 'Output raw JSON')
38
39
  .option('-v, --verbose', 'Show HTTP request details')
39
40
  .option('--domain <host>', 'Override domain')
41
+ .option('--no-migrate', 'Skip pending migrations for this invocation')
40
42
  .action(async (uid, filepath, options) => {
41
43
  try {
44
+ await runPendingMigrations(options);
42
45
  const client = new DboClient({ domain: options.domain, verbose: options.verbose });
43
46
 
44
47
  // ModifyKey guard
@@ -130,6 +133,7 @@ const deployCmd = new Command('deploy')
130
133
  result = await submit();
131
134
  }
132
135
 
136
+ if (result.successful) await client.voidCache();
133
137
  formatResponse(result, { json: options.json });
134
138
  if (!result.successful) process.exit(1);
135
139
  } catch (err) {
@@ -150,7 +154,7 @@ const pullCmd = new Command('pull')
150
154
  .action(async (uid, options) => {
151
155
  try {
152
156
  const client = new DboClient({ domain: options.domain, verbose: options.verbose });
153
- const params = { '_format': 'json_raw' };
157
+ const params = { '_template': 'json_raw' };
154
158
  if (options.maxrows) params['_maxrows'] = options.maxrows;
155
159
 
156
160
  for (const f of options.filter) {
@@ -216,7 +220,7 @@ contentCommand.action(async (uid, options) => {
216
220
  }
217
221
  const client = new DboClient({ domain: options.domain, verbose: options.verbose });
218
222
  const params = {};
219
- if (options.format) params['_format'] = options.format;
223
+ if (options.template) params['_template'] = options.template;
220
224
  if (options.noMinify) params['_no_minify'] = 'true';
221
225
 
222
226
  const result = await client.get(`/api/content/${uid}`, params);
@@ -7,12 +7,14 @@ import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/m
7
7
  import { checkStoredTicket, applyStoredTicketToSubmission, clearGlobalTicket, getGlobalTicket, getRecordTicket } from '../lib/ticketing.js';
8
8
  import { resolveTransactionKey } from '../lib/transaction-key.js';
9
9
  import { log } from '../lib/logger.js';
10
+ import { runPendingMigrations } from '../lib/migrations.js';
10
11
 
11
- const MANIFEST_FILE = 'dbo.deploy.json';
12
+ const MANIFEST_FILE = '.dbo/deploy_config.json';
13
+ const LEGACY_MANIFEST_FILE = 'dbo.deploy.json';
12
14
 
13
15
  export const deployCommand = new Command('deploy')
14
16
  .description('Deploy files to DBO.io using a manifest or direct arguments')
15
- .argument('[name]', 'Deployment name from dbo.deploy.json (e.g., css:colors)')
17
+ .argument('[name]', 'Deployment name from .dbo/deploy_config.json (e.g., css:colors)')
16
18
  .option('--all', 'Deploy all entries in the manifest')
17
19
  .option('-C, --confirm <value>', 'Commit: true (default) or false', 'true')
18
20
  .option('--ticket <id>', 'Override ticket ID')
@@ -21,20 +23,32 @@ export const deployCommand = new Command('deploy')
21
23
  .option('--json', 'Output raw JSON')
22
24
  .option('-v, --verbose', 'Show HTTP request details')
23
25
  .option('--domain <host>', 'Override domain')
26
+ .option('--no-migrate', 'Skip pending migrations for this invocation')
24
27
  .action(async (name, options) => {
25
28
  try {
29
+ await runPendingMigrations(options);
26
30
  const client = new DboClient({ domain: options.domain, verbose: options.verbose });
27
31
 
28
- // Load manifest
32
+ // Load manifest — try .dbo/deploy_config.json first, fall back to legacy dbo.deploy.json
29
33
  let manifest;
34
+ let manifestSource;
30
35
  try {
31
36
  const raw = await readFile(MANIFEST_FILE, 'utf8');
32
37
  manifest = JSON.parse(raw);
38
+ manifestSource = MANIFEST_FILE;
33
39
  } catch {
34
- if (!name) {
35
- log.error(`No ${MANIFEST_FILE} found and no deployment name specified.`);
36
- log.dim(' Create a dbo.deploy.json or use: dbo content deploy <uid> <filepath>');
37
- process.exit(1);
40
+ // Fall back to legacy location
41
+ try {
42
+ const raw = await readFile(LEGACY_MANIFEST_FILE, 'utf8');
43
+ manifest = JSON.parse(raw);
44
+ manifestSource = LEGACY_MANIFEST_FILE;
45
+ log.dim(` Using legacy ${LEGACY_MANIFEST_FILE} — run \`dbo\` to migrate it to ${MANIFEST_FILE}`);
46
+ } catch {
47
+ if (!name) {
48
+ log.error(`No ${MANIFEST_FILE} found and no deployment name specified.`);
49
+ log.dim(` Create ${MANIFEST_FILE} or use: dbo content deploy <uid> <filepath>`);
50
+ process.exit(1);
51
+ }
38
52
  }
39
53
  }
40
54
 
@@ -175,6 +189,7 @@ export const deployCommand = new Command('deploy')
175
189
  }
176
190
 
177
191
  if (result.successful) {
192
+ await client.voidCache();
178
193
  log.success(`${entryName} deployed`);
179
194
  } else {
180
195
  log.error(`${entryName} failed`);
@@ -1,9 +1,9 @@
1
1
  import { Command } from 'commander';
2
- import { stat } from 'fs/promises';
2
+ import { readFile, stat } from 'fs/promises';
3
3
  import { join, dirname, basename, extname } from 'path';
4
4
  import chalk from 'chalk';
5
5
  import { DboClient } from '../lib/client.js';
6
- import { loadConfig } from '../lib/config.js';
6
+ import { loadConfig, loadAppConfig, loadAppJsonBaseline } from '../lib/config.js';
7
7
  import { formatError } from '../lib/formatter.js';
8
8
  import { log } from '../lib/logger.js';
9
9
  import {
@@ -12,6 +12,9 @@ import {
12
12
  formatDiff,
13
13
  applyServerChanges,
14
14
  } from '../lib/diff.js';
15
+ import { fetchServerRecordsBatch } from '../lib/toe-stepping.js';
16
+ import { findBaselineEntry } from '../lib/delta.js';
17
+ import { runPendingMigrations } from '../lib/migrations.js';
15
18
 
16
19
  export const diffCommand = new Command('diff')
17
20
  .description('Compare local files with server versions and selectively merge changes')
@@ -20,8 +23,10 @@ export const diffCommand = new Command('diff')
20
23
  .option('-y, --yes', 'Accept all server changes without prompting')
21
24
  .option('-v, --verbose', 'Show HTTP request details')
22
25
  .option('--domain <host>', 'Override domain')
26
+ .option('--no-migrate', 'Skip pending migrations for this invocation')
23
27
  .action(async (targetPath, options) => {
24
28
  try {
29
+ await runPendingMigrations(options);
25
30
  const client = new DboClient({ domain: options.domain, verbose: options.verbose });
26
31
  const config = await loadConfig();
27
32
 
@@ -36,6 +41,39 @@ export const diffCommand = new Command('diff')
36
41
 
37
42
  log.info(`Comparing ${metaFiles.length} record(s) against server...`);
38
43
 
44
+ // Batch-fetch server records via /api/app/object/ with UpdatedAfter
45
+ const ora = (await import('ora')).default;
46
+ let serverRecordsMap = new Map();
47
+ const appConfig = await loadAppConfig();
48
+ if (appConfig?.AppShortName) {
49
+ // Find oldest baseline _LastUpdated across all files to diff
50
+ const baseline = await loadAppJsonBaseline();
51
+ let oldestDate = null;
52
+ for (const metaPath of metaFiles) {
53
+ try {
54
+ const meta = JSON.parse(await readFile(metaPath, 'utf8'));
55
+ const uid = meta.UID || meta._id;
56
+ const entity = meta._entity;
57
+ if (!uid || !entity || !baseline) continue;
58
+ const entry = findBaselineEntry(baseline, entity, uid);
59
+ if (!entry?._LastUpdated) continue;
60
+ const d = new Date(entry._LastUpdated);
61
+ if (!isNaN(d) && (!oldestDate || d < oldestDate)) oldestDate = d;
62
+ } catch { /* skip unreadable */ }
63
+ }
64
+
65
+ if (oldestDate) {
66
+ const updatedAfter = oldestDate.toISOString();
67
+ const spinner = ora('Fetching server state for comparison...').start();
68
+ serverRecordsMap = await fetchServerRecordsBatch(client, appConfig.AppShortName, updatedAfter);
69
+ if (serverRecordsMap.size > 0) {
70
+ spinner.succeed(`Fetched ${serverRecordsMap.size} record(s) from server`);
71
+ } else {
72
+ spinner.warn('No server records returned');
73
+ }
74
+ }
75
+ }
76
+
39
77
  let updated = 0;
40
78
  let skipped = 0;
41
79
  let unchanged = 0;
@@ -45,7 +83,7 @@ export const diffCommand = new Command('diff')
45
83
  for (const metaPath of metaFiles) {
46
84
  const metaBase = basename(metaPath, '.metadata.json');
47
85
 
48
- const result = await compareRecord(metaPath, client, config);
86
+ const result = await compareRecord(metaPath, config, serverRecordsMap);
49
87
 
50
88
  if (result.error) {
51
89
  log.warn(`${metaBase}: ${result.error}`);
@@ -1,13 +1,14 @@
1
1
  import { Command } from 'commander';
2
- import { access, readdir } from 'fs/promises';
2
+ import { mkdir } from 'fs/promises';
3
3
  import { join } from 'path';
4
4
  import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset, saveTicketSuggestionOutput } from '../lib/config.js';
5
5
  import { installOrUpdateClaudeCommands } from './install.js';
6
6
  import { scaffoldProjectDirs, logScaffoldResult } from '../lib/scaffold.js';
7
- import { createDboignore, loadIgnore } from '../lib/ignore.js';
7
+ import { createDboignore } from '../lib/ignore.js';
8
8
  import { log } from '../lib/logger.js';
9
9
  import { checkDomainChange, writeAppJsonDomain } from '../lib/domain-guard.js';
10
10
  import { performLogin } from './login.js';
11
+ import { runPendingMigrations } from '../lib/migrations.js';
11
12
 
12
13
  export const initCommand = new Command('init')
13
14
  .description('Initialize DBO CLI configuration for the current directory')
@@ -23,10 +24,12 @@ export const initCommand = new Command('init')
23
24
  .option('--scaffold', 'Create standard project directories (app_version, automation, bins, …)')
24
25
  .option('--dboignore', 'Create or reset .dboignore to defaults (use with --force to overwrite)')
25
26
  .option('--media-placement <placement>', 'Set media placement when cloning: fullpath or binpath (default: bin)')
27
+ .option('--no-migrate', 'Skip pending migrations for this invocation')
26
28
  .action(async (options) => {
27
29
  // Merge --yes into nonInteractive
28
30
  if (options.yes) options.nonInteractive = true;
29
31
  try {
32
+ await runPendingMigrations(options);
30
33
  // --dboignore: standalone operation, works regardless of init state
31
34
  if (options.dboignore) {
32
35
  const created = await createDboignore(process.cwd(), { force: options.force });
@@ -99,67 +102,46 @@ export const initCommand = new Command('init')
99
102
  }
100
103
 
101
104
  // Ensure sensitive files are gitignored
102
- await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', 'trash/']);
105
+ await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', '.dbo/errors.log', 'trash/', 'Icon\\r']);
103
106
 
104
107
  const createdIgnore = await createDboignore();
105
108
  if (createdIgnore) log.dim(' Created .dboignore');
106
109
 
110
+ // Ensure .claude/ directory structure exists
111
+ const claudeSubDirs = ['1_prompts', '2_specs', '3_plans', 'commands'];
112
+ for (const sub of claudeSubDirs) {
113
+ await mkdir(join(process.cwd(), '.claude', sub), { recursive: true });
114
+ }
115
+ log.dim(' Created .claude/ directory structure');
116
+
107
117
  log.success(`Initialized .dbo/ for ${domain}`);
108
- log.dim(' Run "dbo login" to authenticate.');
109
118
 
110
- // Prompt for TransactionKeyPreset
111
- if (!options.nonInteractive) {
112
- const inquirer = (await import('inquirer')).default;
113
- const { preset } = await inquirer.prompt([{
114
- type: 'list',
115
- name: 'preset',
116
- message: 'Which row key should the CLI use when building input expressions?',
117
- choices: [
118
- { name: 'RowUID (recommended — stable across domains)', value: 'RowUID' },
119
- { name: 'RowID (numeric IDs)', value: 'RowID' },
120
- ],
121
- }]);
122
- await saveTransactionKeyPreset(preset);
123
- log.dim(` TransactionKeyPreset: ${preset}`);
124
- } else {
125
- await saveTransactionKeyPreset('RowUID');
119
+ // Authenticate early so the session is ready for subsequent operations
120
+ if (!options.nonInteractive && username) {
121
+ await performLogin(domain, username);
126
122
  }
127
123
 
128
- // Prompt for TicketSuggestionOutput
124
+ // TransactionKeyPreset always RowUID (stable across domains)
125
+ await saveTransactionKeyPreset('RowUID');
126
+ log.dim(' TransactionKeyPreset: RowUID');
127
+
128
+ // TicketSuggestionOutput — auto-set default
129
129
  const DEFAULT_TICKET_SUGGESTION_OUTPUT = 'ojaie9t3o0kfvliahnuuda';
130
- if (!options.nonInteractive) {
131
- const inquirer = (await import('inquirer')).default;
132
- const { ticketSuggestionOutput } = await inquirer.prompt([{
133
- type: 'input',
134
- name: 'ticketSuggestionOutput',
135
- message: 'Output UID for ticket suggestions (leave blank to skip):',
136
- default: DEFAULT_TICKET_SUGGESTION_OUTPUT,
137
- }]);
138
- const tsVal = ticketSuggestionOutput.trim();
139
- if (tsVal) {
140
- await saveTicketSuggestionOutput(tsVal);
141
- log.dim(` TicketSuggestionOutput: ${tsVal}`);
142
- }
143
- } else {
144
- await saveTicketSuggestionOutput(DEFAULT_TICKET_SUGGESTION_OUTPUT);
145
- }
130
+ await saveTicketSuggestionOutput(DEFAULT_TICKET_SUGGESTION_OUTPUT);
131
+ log.dim(` TicketSuggestionOutput: ${DEFAULT_TICKET_SUGGESTION_OUTPUT}`);
146
132
 
147
133
  // ─── Scaffold ─────────────────────────────────────────────────────────────
148
- let shouldScaffold = options.scaffold;
149
-
150
- if (!shouldScaffold && !options.nonInteractive) {
151
- const entries = await readdir(process.cwd(), { withFileTypes: true });
152
- const ig = await loadIgnore();
153
- const isEmpty = entries.every(e => ig.ignores(e.isDirectory() ? e.name + '/' : e.name));
154
-
155
- const inquirer = (await import('inquirer')).default;
156
- const { doScaffold } = await inquirer.prompt([{
157
- type: 'confirm',
158
- name: 'doScaffold',
159
- message: 'Would you like to scaffold the standard project directory structure?',
160
- default: isEmpty,
161
- }]);
162
- shouldScaffold = doScaffold;
134
+ // Auto-scaffold if standard directories don't exist yet
135
+ let shouldScaffold = true;
136
+
137
+ if (!options.scaffold && !options.nonInteractive) {
138
+ // Check if already scaffolded (bins/ exists = already done)
139
+ try {
140
+ const s = await (await import('fs/promises')).stat(join(process.cwd(), 'lib/bins'));
141
+ if (s.isDirectory()) shouldScaffold = false; // already scaffolded
142
+ } catch {
143
+ // bins/ doesn't exist — scaffold needed
144
+ }
163
145
  }
164
146
 
165
147
  if (shouldScaffold) {
@@ -167,7 +149,7 @@ export const initCommand = new Command('init')
167
149
  logScaffoldResult(result);
168
150
  }
169
151
 
170
- // Clone if requested — requires authentication first
152
+ // Clone if requested
171
153
  if (options.clone || options.app) {
172
154
  let appShortName = options.app;
173
155
  if (!appShortName) {
@@ -180,38 +162,19 @@ export const initCommand = new Command('init')
180
162
  appShortName = appName;
181
163
  }
182
164
 
183
- // Authenticate before fetching app data from the server
184
- if (!options.nonInteractive) {
185
- log.info('Login required to fetch app data from the server.');
186
- await performLogin(domain, username);
187
- }
188
-
189
165
  const { performClone } = await import('./clone.js');
190
166
  await performClone(null, { app: appShortName, domain, mediaPlacement: options.mediaPlacement });
191
167
  }
192
168
 
193
- // Offer Claude Code integration (skip in non-interactive mode)
169
+ // Offer Claude Code plugin installation (skip in non-interactive mode)
194
170
  if (!options.nonInteractive) {
195
- const claudeDir = join(process.cwd(), '.claude');
196
171
  const inquirer = (await import('inquirer')).default;
197
- let hasClaudeDir = false;
198
- try { await access(claudeDir); hasClaudeDir = true; } catch {}
199
-
200
- if (hasClaudeDir) {
201
- const { installCmds } = await inquirer.prompt([{
202
- type: 'confirm', name: 'installCmds',
203
- message: 'Install DBO commands for Claude Code? (adds /dbo slash command)',
204
- default: true,
205
- }]);
206
- if (installCmds) await installOrUpdateClaudeCommands(options);
207
- } else {
208
- const { setupClaude } = await inquirer.prompt([{
209
- type: 'confirm', name: 'setupClaude',
210
- message: 'Set up Claude Code integration? (creates .claude/commands/)',
211
- default: false,
212
- }]);
213
- if (setupClaude) await installOrUpdateClaudeCommands(options);
214
- }
172
+ const { installCmds } = await inquirer.prompt([{
173
+ type: 'confirm', name: 'installCmds',
174
+ message: 'Install DBO commands for Claude Code? (adds /dbo slash command)',
175
+ default: true,
176
+ }]);
177
+ if (installCmds) await installOrUpdateClaudeCommands(options);
215
178
  }
216
179
  } catch (err) {
217
180
  log.error(err.message);
@@ -6,6 +6,7 @@ import { loadAppConfig } from '../lib/config.js';
6
6
  import { checkStoredTicket, clearGlobalTicket } from '../lib/ticketing.js';
7
7
  import { checkModifyKey, isModifyKeyError, handleModifyKeyError } from '../lib/modify-key.js';
8
8
  import { log } from '../lib/logger.js';
9
+ import { runPendingMigrations } from '../lib/migrations.js';
9
10
 
10
11
  function collect(value, previous) {
11
12
  return previous.concat([value]);
@@ -25,8 +26,10 @@ export const inputCommand = new Command('input')
25
26
  .option('--jq <expr>', 'Filter JSON response with jq-like expression (implies --json)')
26
27
  .option('-v, --verbose', 'Show HTTP request details')
27
28
  .option('--domain <host>', 'Override domain')
29
+ .option('--no-migrate', 'Skip pending migrations for this invocation')
28
30
  .action(async (options) => {
29
31
  try {
32
+ await runPendingMigrations(options);
30
33
  const client = new DboClient({ domain: options.domain, verbose: options.verbose });
31
34
 
32
35
  const extraParams = {};
@@ -124,6 +127,7 @@ export const inputCommand = new Command('input')
124
127
  result = await client.postMultipart('/api/input/submit', fields, files);
125
128
  }
126
129
 
130
+ if (result.successful) await client.voidCache();
127
131
  formatResponse(result, { json: options.json, jq: options.jq, verbose: options.verbose });
128
132
  if (!result.successful) process.exit(1);
129
133
  } else {
@@ -153,6 +157,7 @@ export const inputCommand = new Command('input')
153
157
  result = await client.postUrlEncoded('/api/input/submit', body);
154
158
  }
155
159
 
160
+ if (result.successful) await client.voidCache();
156
161
  formatResponse(result, { json: options.json, jq: options.jq, verbose: options.verbose });
157
162
  if (!result.successful) process.exit(1);
158
163
  }
@@ -42,7 +42,7 @@ export async function performLogin(domain, knownUsername) {
42
42
 
43
43
  // Fetch and store user info (non-critical)
44
44
  try {
45
- const userResult = await client.get('/api/output/31799e38f0854956a47f10', { '_format': 'json_raw' });
45
+ const userResult = await client.get('/api/output/31799e38f0854956a47f10', { '_template': 'json_raw' });
46
46
  const userData = userResult.payload || userResult.data;
47
47
  const rows = Array.isArray(userData) ? userData : (userData?.Rows || userData?.rows || []);
48
48
  if (rows.length > 0) {
@@ -121,7 +121,7 @@ export const loginCommand = new Command('login')
121
121
 
122
122
  // Fetch current user info to store ID for future submissions
123
123
  try {
124
- const userResult = await client.get('/api/output/31799e38f0854956a47f10', { '_format': 'json_raw' });
124
+ const userResult = await client.get('/api/output/31799e38f0854956a47f10', { '_template': 'json_raw' });
125
125
  const userData = userResult.payload || userResult.data;
126
126
  const rows = Array.isArray(userData) ? userData : (userData?.Rows || userData?.rows || []);
127
127
  if (rows.length > 0) {
@@ -13,6 +13,7 @@ import {
13
13
  BINS_DIR
14
14
  } from '../lib/structure.js';
15
15
  import { findMetadataFiles } from '../lib/diff.js';
16
+ import { runPendingMigrations } from '../lib/migrations.js';
16
17
 
17
18
  export const mvCommand = new Command('mv')
18
19
  .description('Move files or bins to a new location and update metadata')
@@ -22,8 +23,10 @@ export const mvCommand = new Command('mv')
22
23
  .option('-v, --verbose', 'Show detailed operations')
23
24
  .option('--dry-run', 'Preview changes without executing')
24
25
  .option('-C, --confirm <value>', 'Commit changes (true|false)', 'true')
26
+ .option('--no-migrate', 'Skip pending migrations for this invocation')
25
27
  .action(async (source, destination, options) => {
26
28
  try {
29
+ await runPendingMigrations(options);
27
30
  // Only items inside Bins/ are moveable
28
31
  const relSource = isAbsolute(source) ? relative(process.cwd(), source) : source;
29
32
  const normalized = relSource.replace(/\/+$/, '');
@@ -15,14 +15,13 @@ export const outputCommand = new Command('output')
15
15
  .option('-e, --entity <uid>', 'Query by entity UID')
16
16
  .option('--row <id>', 'Specific row ID')
17
17
  .option('--filter <expr>', 'Filter expression (repeatable)', collect, [])
18
- .option('--format <type>', 'Output format: json, html, csv, xml, txt, pdf')
18
+ .option('--template <value>', 'Output template: json_raw (default), json_indented, json, html, csv, xml, txt, pdf, or a content UID')
19
19
  .option('--sort <expr>', 'Sort expression (repeatable)', collect, [])
20
20
  .option('--search <expr>', 'Full-text search')
21
21
  .option('--page <n>', 'Page number')
22
22
  .option('--rows-per-page <n>', 'Rows per page')
23
23
  .option('--maxrows <n>', 'Maximum rows')
24
24
  .option('--rows <range>', 'Row range (e.g., 1-10)')
25
- .option('--template <value>', 'Custom template')
26
25
  .option('--debug', 'Include debug info')
27
26
  .option('--debug-sql', 'Include SQL debug info')
28
27
  .option('--debug-verbose', 'Verbose debug output')
@@ -30,7 +29,7 @@ export const outputCommand = new Command('output')
30
29
  .option('--limit <n>', 'Maximum rows to return (_limit)')
31
30
  .option('--rowcount <bool>', 'Include row count: true (default) or false for performance')
32
31
  .option('--display <expr>', 'Show/hide template tags (repeatable), e.g., "sidebar=hide"', collect, [])
33
- .option('--format-values', 'Enable value formatting in json_raw output')
32
+ .option('--format-values', 'Enable value formatting with json_raw template')
34
33
  .option('--empty-response-code <code>', 'HTTP status code when output returns no results')
35
34
  .option('--fallback-content <expr>', 'Fallback content UID for error codes, e.g., "404=contentUID"')
36
35
  .option('--escape-html <bool>', 'Control HTML escaping: true or false')
@@ -75,13 +74,12 @@ export const outputCommand = new Command('output')
75
74
  }
76
75
 
77
76
  // Build query params — default to json_raw for normalized data
78
- const params = { '_format': options.format || 'json_raw' };
77
+ const params = { '_template': options.template || 'json_raw' };
79
78
  if (options.search) params['_search'] = options.search;
80
79
  if (options.page) params['_page'] = options.page;
81
80
  if (options.rowsPerPage) params['_RowsPerPage'] = options.rowsPerPage;
82
81
  if (options.maxrows) params['_maxrows'] = options.maxrows;
83
82
  if (options.rows) params['_rows'] = options.rows;
84
- if (options.template) params['_template'] = options.template;
85
83
  if (options.limit) params['_limit'] = options.limit;
86
84
  if (options.rowcount !== undefined) params['_rowcount'] = options.rowcount;
87
85
  if (options.debug) params['_debug'] = 'true';
@@ -142,9 +140,9 @@ export const outputCommand = new Command('output')
142
140
  params['_sort'] = options.sort;
143
141
  }
144
142
 
145
- // For save mode or jq mode, force json_raw if user didn't set a custom format
146
- if ((options.save || options.saveFilename || options.jq) && !options.format) {
147
- params['_format'] = 'json_raw';
143
+ // For save mode or jq mode, force json_raw if user didn't set a custom template
144
+ if ((options.save || options.saveFilename || options.jq) && !options.template) {
145
+ params['_template'] = 'json_raw';
148
146
  }
149
147
 
150
148
  const result = await client.get(path, params);
@@ -171,8 +169,8 @@ export const outputCommand = new Command('output')
171
169
  return;
172
170
  }
173
171
 
174
- // Default to colorized JSON when using json_raw (no explicit --format, --json, or --jq)
175
- const jq = options.jq || (!options.format && !options.json ? '.' : undefined);
172
+ // Default to colorized JSON when using json_raw (no explicit --template, --json, or --jq)
173
+ const jq = options.jq || (!options.template && !options.json ? '.' : undefined);
176
174
  formatResponse(result, { json: options.json, jq, columns: options.columns });
177
175
  if (!result.successful && !result.ok) process.exit(1);
178
176
  } catch (err) {