@dboio/cli 0.11.4 → 0.15.0

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 (57) hide show
  1. package/README.md +183 -3
  2. package/bin/dbo.js +6 -0
  3. package/package.json +1 -1
  4. package/plugins/claude/dbo/.claude-plugin/plugin.json +1 -1
  5. package/plugins/claude/dbo/commands/dbo.md +66 -243
  6. package/plugins/claude/dbo/docs/_audit_required/API/all.md +40 -0
  7. package/plugins/claude/dbo/docs/_audit_required/API/app.md +38 -0
  8. package/plugins/claude/dbo/docs/_audit_required/API/athenticate.md +26 -0
  9. package/plugins/claude/dbo/docs/_audit_required/API/cache.md +29 -0
  10. package/plugins/claude/dbo/docs/_audit_required/API/content.md +14 -0
  11. package/plugins/claude/dbo/docs/_audit_required/API/data_source.md +28 -0
  12. package/plugins/claude/dbo/docs/_audit_required/API/email.md +18 -0
  13. package/plugins/claude/dbo/docs/_audit_required/API/input.md +25 -0
  14. package/plugins/claude/dbo/docs/_audit_required/API/instance.md +28 -0
  15. package/plugins/claude/dbo/docs/_audit_required/API/log.md +8 -0
  16. package/plugins/claude/dbo/docs/_audit_required/API/media.md +12 -0
  17. package/plugins/claude/dbo/docs/_audit_required/API/output_by_entity.md +12 -0
  18. package/plugins/claude/dbo/docs/_audit_required/API/upload.md +7 -0
  19. package/plugins/claude/dbo/docs/_audit_required/dbo-api-syntax.md +1487 -0
  20. package/plugins/claude/dbo/docs/_audit_required/dbo-problems-code.md +111 -0
  21. package/plugins/claude/dbo/docs/_audit_required/dbo-problems-performance.md +109 -0
  22. package/plugins/claude/dbo/docs/_audit_required/dbo-problems-syntax.md +97 -0
  23. package/plugins/claude/dbo/docs/_audit_required/dbo-product-market.md +119 -0
  24. package/plugins/claude/dbo/docs/_audit_required/dbo-white-paper.md +125 -0
  25. package/plugins/claude/dbo/docs/dbo-cheat-sheet.md +323 -0
  26. package/plugins/claude/dbo/docs/dbo-cli-readme.md +2279 -0
  27. package/plugins/claude/dbo/docs/dbo-core-entities.md +878 -0
  28. package/plugins/claude/dbo/docs/dbo-output-customsql.md +677 -0
  29. package/plugins/claude/dbo/docs/dbo-output-query.md +967 -0
  30. package/plugins/claude/dbo/skills/cli/SKILL.md +63 -246
  31. package/src/commands/add.js +373 -64
  32. package/src/commands/build.js +102 -0
  33. package/src/commands/clone.js +719 -212
  34. package/src/commands/deploy.js +9 -2
  35. package/src/commands/diff.js +7 -3
  36. package/src/commands/init.js +16 -2
  37. package/src/commands/input.js +3 -1
  38. package/src/commands/login.js +30 -4
  39. package/src/commands/mv.js +28 -7
  40. package/src/commands/push.js +298 -78
  41. package/src/commands/rm.js +21 -6
  42. package/src/commands/run.js +81 -0
  43. package/src/commands/tag.js +65 -0
  44. package/src/lib/config.js +67 -0
  45. package/src/lib/delta.js +7 -1
  46. package/src/lib/deploy-config.js +137 -0
  47. package/src/lib/diff.js +28 -5
  48. package/src/lib/filenames.js +198 -54
  49. package/src/lib/ignore.js +6 -0
  50. package/src/lib/input-parser.js +13 -4
  51. package/src/lib/scaffold.js +1 -1
  52. package/src/lib/scripts.js +232 -0
  53. package/src/lib/tagging.js +380 -0
  54. package/src/lib/toe-stepping.js +2 -1
  55. package/src/migrations/006-remove-uid-companion-filenames.js +181 -0
  56. package/src/migrations/007-natural-entity-companion-filenames.js +165 -0
  57. package/src/migrations/008-metadata-uid-in-suffix.js +70 -0
@@ -57,10 +57,17 @@ export const deployCommand = new Command('deploy')
57
57
  process.exit(1);
58
58
  }
59
59
 
60
+ // If name doesn't match a key directly, try scanning by UID value
61
+ let resolvedName = name;
62
+ if (name && !manifest.deployments[name]) {
63
+ const byUid = Object.entries(manifest.deployments).find(([, v]) => v && v.uid === name);
64
+ if (byUid) resolvedName = byUid[0];
65
+ }
66
+
60
67
  const entries = options.all
61
68
  ? Object.entries(manifest.deployments)
62
- : name
63
- ? [[name, manifest.deployments[name]]]
69
+ : resolvedName
70
+ ? [[resolvedName, manifest.deployments[resolvedName]]]
64
71
  : [];
65
72
 
66
73
  if (entries.length === 0 || (entries.length === 1 && !entries[0][1])) {
@@ -14,6 +14,7 @@ import {
14
14
  } from '../lib/diff.js';
15
15
  import { fetchServerRecordsBatch } from '../lib/toe-stepping.js';
16
16
  import { findBaselineEntry } from '../lib/delta.js';
17
+ import { isMetadataFile, parseMetaFilename, findMetadataForCompanion } from '../lib/filenames.js';
17
18
  import { runPendingMigrations } from '../lib/migrations.js';
18
19
 
19
20
  export const diffCommand = new Command('diff')
@@ -81,7 +82,7 @@ export const diffCommand = new Command('diff')
81
82
  let bulkAction = null;
82
83
 
83
84
  for (const metaPath of metaFiles) {
84
- const metaBase = basename(metaPath, '.metadata.json');
85
+ const metaBase = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
85
86
 
86
87
  const result = await compareRecord(metaPath, config, serverRecordsMap);
87
88
 
@@ -203,7 +204,7 @@ async function resolveTargetToMetaFiles(targetPath) {
203
204
  pathStat = await stat(targetPath);
204
205
  } catch {
205
206
  // Maybe it's a file without extension — try finding metadata
206
- const metaPath = targetPath.endsWith('.metadata.json')
207
+ const metaPath = (isMetadataFile(basename(targetPath)) || targetPath.endsWith('.metadata.json'))
207
208
  ? targetPath
208
209
  : `${targetPath}.metadata.json`;
209
210
 
@@ -220,7 +221,7 @@ async function resolveTargetToMetaFiles(targetPath) {
220
221
  }
221
222
 
222
223
  // Single file — find its companion metadata
223
- if (targetPath.endsWith('.metadata.json')) {
224
+ if (isMetadataFile(basename(targetPath)) || targetPath.endsWith('.metadata.json')) {
224
225
  return [targetPath];
225
226
  }
226
227
 
@@ -239,6 +240,9 @@ async function resolveTargetToMetaFiles(targetPath) {
239
240
  await stat(mediaMetaPath);
240
241
  return [mediaMetaPath];
241
242
  } catch {
243
+ // Fallback: scan sibling metadata files for @reference match
244
+ const found = await findMetadataForCompanion(targetPath);
245
+ if (found) return [found];
242
246
  log.warn(`No metadata found for "${targetPath}"`);
243
247
  return [];
244
248
  }
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { mkdir } from 'fs/promises';
2
+ import { mkdir, writeFile, access } 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';
@@ -102,7 +102,7 @@ export const initCommand = new Command('init')
102
102
  }
103
103
 
104
104
  // Ensure sensitive files are gitignored
105
- await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', '.dbo/errors.log', 'trash/', 'Icon\\r']);
105
+ await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', '.dbo/scripts.local.json', '.dbo/errors.log', 'trash/', 'Icon\\r']);
106
106
 
107
107
  const createdIgnore = await createDboignore();
108
108
  if (createdIgnore) log.dim(' Created .dboignore');
@@ -114,6 +114,20 @@ export const initCommand = new Command('init')
114
114
  }
115
115
  log.dim(' Created .claude/ directory structure');
116
116
 
117
+ // Create empty scripts.json and scripts.local.json if they don't exist
118
+ const emptyScripts = JSON.stringify({ scripts: {}, targets: {}, entities: {} }, null, 2) + '\n';
119
+ const dboDir = join(process.cwd(), '.dbo');
120
+ const scriptsPath = join(dboDir, 'scripts.json');
121
+ const scriptsLocalPath = join(dboDir, 'scripts.local.json');
122
+ try { await access(scriptsPath); } catch {
123
+ await writeFile(scriptsPath, emptyScripts);
124
+ log.dim(' Created .dbo/scripts.json');
125
+ }
126
+ try { await access(scriptsLocalPath); } catch {
127
+ await writeFile(scriptsLocalPath, emptyScripts);
128
+ log.dim(' Created .dbo/scripts.local.json');
129
+ }
130
+
117
131
  log.success(`Initialized .dbo/ for ${domain}`);
118
132
 
119
133
  // Authenticate early so the session is ready for subsequent operations
@@ -60,8 +60,10 @@ export const inputCommand = new Command('input')
60
60
  if (modifyKeyResult.modifyKey) extraParams['_modify_key'] = modifyKeyResult.modifyKey;
61
61
 
62
62
  // Check if data expressions include AppID; if not and config has one, prompt
63
+ // Skip AppID prompt for delete-only submissions — deletes don't need it
63
64
  const allDataText = options.data.join(' ');
64
- const hasAppId = /\.AppID[=@]/.test(allDataText) || /AppID=/.test(allDataText);
65
+ const isDeleteOnly = options.data.every(d => /RowID:del\d+/.test(d));
66
+ const hasAppId = isDeleteOnly || /\.AppID[=@]/.test(allDataText) || /AppID=/.test(allDataText);
65
67
  if (!hasAppId) {
66
68
  const appConfig = await loadAppConfig();
67
69
  if (appConfig.AppID) {
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { loadConfig, saveUserInfo, saveUserProfile, ensureGitignore } from '../lib/config.js';
2
+ import { loadConfig, initConfig, saveUserInfo, saveUserProfile, ensureGitignore } from '../lib/config.js';
3
3
  import { DboClient } from '../lib/client.js';
4
4
  import { log } from '../lib/logger.js';
5
5
 
@@ -13,13 +13,24 @@ import { log } from '../lib/logger.js';
13
13
  */
14
14
  export async function performLogin(domain, knownUsername) {
15
15
  const config = await loadConfig();
16
+ const inquirer = (await import('inquirer')).default;
17
+
18
+ // Prompt for domain if not provided and not configured
19
+ if (!domain && !config.domain) {
20
+ const { domain: inputDomain } = await inquirer.prompt([
21
+ { type: 'input', name: 'domain', message: 'Domain (e.g. myapp.dbo.io):' },
22
+ ]);
23
+ if (!inputDomain) throw new Error('Domain is required.');
24
+ domain = inputDomain.trim();
25
+ await initConfig(domain);
26
+ }
27
+
16
28
  const client = new DboClient({ domain });
17
29
 
18
30
  let username = knownUsername || config.username;
19
31
  let password;
20
32
 
21
33
  // Interactive prompt for missing credentials
22
- const inquirer = (await import('inquirer')).default;
23
34
  const answers = await inquirer.prompt([
24
35
  { type: 'input', name: 'username', message: 'Username (email):', default: username || undefined, when: !username },
25
36
  { type: 'password', name: 'password', message: 'Password:', mask: '*' },
@@ -83,7 +94,23 @@ export const loginCommand = new Command('login')
83
94
  .action(async (options) => {
84
95
  try {
85
96
  const config = await loadConfig();
86
- const client = new DboClient({ domain: options.domain });
97
+ const inquirer = (await import('inquirer')).default;
98
+ let domain = options.domain;
99
+
100
+ // Prompt for domain if not provided and not configured
101
+ if (!domain && !config.domain) {
102
+ const { domain: inputDomain } = await inquirer.prompt([
103
+ { type: 'input', name: 'domain', message: 'Domain (e.g. myapp.dbo.io):' },
104
+ ]);
105
+ if (!inputDomain) {
106
+ log.error('Domain is required.');
107
+ process.exit(1);
108
+ }
109
+ domain = inputDomain.trim();
110
+ await initConfig(domain);
111
+ }
112
+
113
+ const client = new DboClient({ domain });
87
114
 
88
115
  let username = options.username || options.email || options.phone;
89
116
  let password = options.password || options.passkey;
@@ -93,7 +120,6 @@ export const loginCommand = new Command('login')
93
120
 
94
121
  // Interactive prompt if still missing
95
122
  if (!username || !password) {
96
- const inquirer = (await import('inquirer')).default;
97
123
  const answers = await inquirer.prompt([
98
124
  { type: 'input', name: 'username', message: 'Username (email):', default: config.username, when: !username },
99
125
  { type: 'password', name: 'password', message: 'Password:', mask: '*', when: !password },
@@ -13,7 +13,9 @@ import {
13
13
  BINS_DIR
14
14
  } from '../lib/structure.js';
15
15
  import { findMetadataFiles } from '../lib/diff.js';
16
+ import { isMetadataFile, parseMetaFilename, findMetadataForCompanion } from '../lib/filenames.js';
16
17
  import { runPendingMigrations } from '../lib/migrations.js';
18
+ import { removeDeployEntry, upsertDeployEntry } from '../lib/deploy-config.js';
17
19
 
18
20
  export const mvCommand = new Command('mv')
19
21
  .description('Move files or bins to a new location and update metadata')
@@ -199,7 +201,7 @@ function checkCircularReference(sourceBinId, targetBinId, structure) {
199
201
  * Resolve a file path to its metadata.json path.
200
202
  */
201
203
  function resolveMetaPath(filePath) {
202
- if (filePath.endsWith('.metadata.json')) {
204
+ if (isMetadataFile(basename(filePath)) || filePath.endsWith('.metadata.json')) {
203
205
  return filePath;
204
206
  }
205
207
  const dir = dirname(filePath);
@@ -507,14 +509,21 @@ async function checkNameConflict(fileName, targetDir, options) {
507
509
  */
508
510
  async function mvFile(sourceFile, targetBinId, structure, options) {
509
511
  // Resolve metadata
510
- const metaPath = resolveMetaPath(sourceFile);
512
+ let metaPath = resolveMetaPath(sourceFile);
511
513
  let meta;
512
514
  try {
513
515
  meta = JSON.parse(await readFile(metaPath, 'utf8'));
514
516
  } catch {
515
- log.error(`No metadata found for "${sourceFile}"`);
516
- log.dim(' Use "dbo content pull" or "dbo output --save" to create metadata files.');
517
- process.exit(1);
517
+ const found = await findMetadataForCompanion(sourceFile);
518
+ if (found) {
519
+ metaPath = found;
520
+ try { meta = JSON.parse(await readFile(metaPath, 'utf8')); } catch {}
521
+ }
522
+ if (!meta) {
523
+ log.error(`No metadata found for "${basename(sourceFile)}"`);
524
+ log.dim(' Use "dbo pull" to create metadata files.');
525
+ process.exit(1);
526
+ }
518
527
  }
519
528
 
520
529
  const entity = meta._entity;
@@ -536,13 +545,13 @@ async function mvFile(sourceFile, targetBinId, structure, options) {
536
545
  // Calculate paths
537
546
  const targetDir = resolveBinPath(targetBinId, structure);
538
547
  const sourceDir = dirname(metaPath);
539
- const contentFileName = sourceFile.endsWith('.metadata.json')
548
+ const contentFileName = (isMetadataFile(basename(sourceFile)) || sourceFile.endsWith('.metadata.json'))
540
549
  ? null
541
550
  : basename(sourceFile);
542
551
  const metaFileName = basename(metaPath);
543
552
 
544
553
  // Determine display name
545
- const displayName = basename(metaPath, '.metadata.json');
554
+ const displayName = parseMetaFilename(basename(metaPath))?.naturalBase ?? basename(metaPath, '.metadata.json');
546
555
  const targetBin = structure[targetBinId];
547
556
  const targetBinName = targetBin ? targetBin.name : String(targetBinId);
548
557
  const targetBinFullPath = targetBin ? `${BINS_DIR}/${targetBin.fullPath}` : targetDir;
@@ -664,6 +673,18 @@ async function mvFile(sourceFile, targetBinId, structure, options) {
664
673
  }
665
674
  }
666
675
 
676
+ if (!options.dryRun) {
677
+ // Update deploy config: remove old entry (by UID), re-insert with new path + correct key
678
+ await removeDeployEntry(uid);
679
+ if (newContentPath) {
680
+ const col = (meta._contentColumns || [])[0] || 'Content';
681
+ await upsertDeployEntry(newContentPath, uid, entity, col);
682
+ } else if (meta._mediaFile && String(meta._mediaFile).startsWith('@')) {
683
+ const movedMediaPath = join(targetDir, String(meta._mediaFile).substring(1));
684
+ await upsertDeployEntry(movedMediaPath, uid, entity, 'File');
685
+ }
686
+ }
687
+
667
688
  if (options.dryRun) {
668
689
  log.info(`[DRY RUN] Would move "${displayName}" to "${targetBinName}" (${targetBinFullPath})`);
669
690
  } else {