@dboio/cli 0.10.1 → 0.11.2
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/README.md +135 -70
- package/bin/postinstall.js +9 -1
- package/package.json +3 -3
- package/plugins/claude/dbo/commands/dbo.md +3 -3
- package/plugins/claude/dbo/skills/cli/SKILL.md +3 -3
- package/src/commands/add.js +4 -0
- package/src/commands/clone.js +249 -399
- package/src/commands/content.js +7 -3
- package/src/commands/deploy.js +22 -7
- package/src/commands/diff.js +41 -3
- package/src/commands/init.js +25 -60
- package/src/commands/input.js +5 -0
- package/src/commands/login.js +2 -2
- package/src/commands/mv.js +3 -0
- package/src/commands/output.js +8 -10
- package/src/commands/pull.js +12 -8
- package/src/commands/push.js +317 -42
- package/src/commands/rm.js +3 -0
- package/src/commands/status.js +12 -1
- package/src/commands/sync.js +3 -0
- package/src/lib/client.js +10 -0
- package/src/lib/config.js +31 -0
- package/src/lib/delta.js +92 -0
- package/src/lib/diff.js +143 -19
- package/src/lib/ignore.js +1 -2
- package/src/lib/metadata-templates.js +21 -4
- package/src/lib/migrations.js +75 -0
- package/src/lib/save-to-disk.js +1 -1
- package/src/lib/scaffold.js +3 -28
- package/src/lib/structure.js +157 -22
- package/src/lib/toe-stepping.js +381 -0
- package/src/migrations/001-transaction-key-preset-scope.js +35 -0
- package/src/migrations/002-move-entity-dirs-to-lib.js +190 -0
- package/src/migrations/003-move-deploy-config.js +50 -0
- package/src/migrations/004-rename-output-files.js +101 -0
package/src/commands/content.js
CHANGED
|
@@ -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('--
|
|
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 = { '
|
|
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.
|
|
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);
|
package/src/commands/deploy.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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`);
|
package/src/commands/diff.js
CHANGED
|
@@ -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,
|
|
86
|
+
const result = await compareRecord(metaPath, config, serverRecordsMap);
|
|
49
87
|
|
|
50
88
|
if (result.error) {
|
|
51
89
|
log.warn(`${metaBase}: ${result.error}`);
|
package/src/commands/init.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdir } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset, saveTicketSuggestionOutput
|
|
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
|
|
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 });
|
|
@@ -40,13 +43,7 @@ export const initCommand = new Command('init')
|
|
|
40
43
|
|
|
41
44
|
if (await isInitialized() && !options.force) {
|
|
42
45
|
if (options.scaffold) {
|
|
43
|
-
|
|
44
|
-
let shortName;
|
|
45
|
-
try {
|
|
46
|
-
const cfg = await loadConfig();
|
|
47
|
-
shortName = cfg.AppShortName;
|
|
48
|
-
} catch { /* no config yet */ }
|
|
49
|
-
const result = await scaffoldProjectDirs(process.cwd(), { appShortName: shortName });
|
|
46
|
+
const result = await scaffoldProjectDirs();
|
|
50
47
|
logScaffoldResult(result);
|
|
51
48
|
return;
|
|
52
49
|
}
|
|
@@ -105,7 +102,7 @@ export const initCommand = new Command('init')
|
|
|
105
102
|
}
|
|
106
103
|
|
|
107
104
|
// Ensure sensitive files are gitignored
|
|
108
|
-
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', 'trash/', 'Icon\\r']);
|
|
105
|
+
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json', '.dbo/errors.log', 'trash/', 'Icon\\r']);
|
|
109
106
|
|
|
110
107
|
const createdIgnore = await createDboignore();
|
|
111
108
|
if (createdIgnore) log.dim(' Created .dboignore');
|
|
@@ -124,59 +121,27 @@ export const initCommand = new Command('init')
|
|
|
124
121
|
await performLogin(domain, username);
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const { preset } = await inquirer.prompt([{
|
|
131
|
-
type: 'list',
|
|
132
|
-
name: 'preset',
|
|
133
|
-
message: 'Which row key should the CLI use when building input expressions?',
|
|
134
|
-
choices: [
|
|
135
|
-
{ name: 'RowUID (recommended — stable across domains)', value: 'RowUID' },
|
|
136
|
-
{ name: 'RowID (numeric IDs)', value: 'RowID' },
|
|
137
|
-
],
|
|
138
|
-
}]);
|
|
139
|
-
await saveTransactionKeyPreset(preset);
|
|
140
|
-
log.dim(` TransactionKeyPreset: ${preset}`);
|
|
141
|
-
} else {
|
|
142
|
-
await saveTransactionKeyPreset('RowUID');
|
|
143
|
-
}
|
|
124
|
+
// TransactionKeyPreset — always RowUID (stable across domains)
|
|
125
|
+
await saveTransactionKeyPreset('RowUID');
|
|
126
|
+
log.dim(' TransactionKeyPreset: RowUID');
|
|
144
127
|
|
|
145
|
-
//
|
|
128
|
+
// TicketSuggestionOutput — auto-set default
|
|
146
129
|
const DEFAULT_TICKET_SUGGESTION_OUTPUT = 'ojaie9t3o0kfvliahnuuda';
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const { ticketSuggestionOutput } = await inquirer.prompt([{
|
|
150
|
-
type: 'input',
|
|
151
|
-
name: 'ticketSuggestionOutput',
|
|
152
|
-
message: 'Output UID for ticket suggestions (leave blank to skip):',
|
|
153
|
-
default: DEFAULT_TICKET_SUGGESTION_OUTPUT,
|
|
154
|
-
}]);
|
|
155
|
-
const tsVal = ticketSuggestionOutput.trim();
|
|
156
|
-
if (tsVal) {
|
|
157
|
-
await saveTicketSuggestionOutput(tsVal);
|
|
158
|
-
log.dim(` TicketSuggestionOutput: ${tsVal}`);
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
await saveTicketSuggestionOutput(DEFAULT_TICKET_SUGGESTION_OUTPUT);
|
|
162
|
-
}
|
|
130
|
+
await saveTicketSuggestionOutput(DEFAULT_TICKET_SUGGESTION_OUTPUT);
|
|
131
|
+
log.dim(` TicketSuggestionOutput: ${DEFAULT_TICKET_SUGGESTION_OUTPUT}`);
|
|
163
132
|
|
|
164
133
|
// ─── Scaffold ─────────────────────────────────────────────────────────────
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
message: 'Would you like to scaffold the standard project directory structure?',
|
|
177
|
-
default: isEmpty,
|
|
178
|
-
}]);
|
|
179
|
-
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
|
+
}
|
|
180
145
|
}
|
|
181
146
|
|
|
182
147
|
if (shouldScaffold) {
|
package/src/commands/input.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -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', { '
|
|
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', { '
|
|
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) {
|
package/src/commands/mv.js
CHANGED
|
@@ -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(/\/+$/, '');
|
package/src/commands/output.js
CHANGED
|
@@ -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('--
|
|
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
|
|
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 = { '
|
|
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
|
|
146
|
-
if ((options.save || options.saveFilename || options.jq) && !options.
|
|
147
|
-
params['
|
|
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 --
|
|
175
|
-
const jq = options.jq || (!options.
|
|
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) {
|
package/src/commands/pull.js
CHANGED
|
@@ -4,11 +4,12 @@ import { join } from 'path';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { performClone, resolveRecordPaths, resolveMediaPaths, resolveEntityDirPaths, resolveEntityFilter, buildOutputFilename } from './clone.js';
|
|
6
6
|
import { loadConfig, loadClonePlacement } from '../lib/config.js';
|
|
7
|
-
import { loadStructureFile, resolveBinPath, BINS_DIR,
|
|
7
|
+
import { loadStructureFile, resolveBinPath, BINS_DIR, EXTENSION_DESCRIPTORS_DIR, EXTENSION_UNSUPPORTED_DIR, resolveEntityDirPath } from '../lib/structure.js';
|
|
8
8
|
import { log } from '../lib/logger.js';
|
|
9
9
|
import { formatError } from '../lib/formatter.js';
|
|
10
|
-
import { getLocalSyncTime, isServerNewer } from '../lib/diff.js';
|
|
10
|
+
import { getLocalSyncTime, isServerNewer, loadBaselineForComparison } from '../lib/diff.js';
|
|
11
11
|
import { DboClient } from '../lib/client.js';
|
|
12
|
+
import { runPendingMigrations } from '../lib/migrations.js';
|
|
12
13
|
|
|
13
14
|
async function fileExists(path) {
|
|
14
15
|
try { await access(path); return true; } catch { return false; }
|
|
@@ -117,10 +118,12 @@ async function dryRunScan(appJson, options) {
|
|
|
117
118
|
const serverTz = config.ServerTimezone || 'America/Los_Angeles';
|
|
118
119
|
const configWithTz = { ...config, ServerTimezone: serverTz };
|
|
119
120
|
|
|
121
|
+
// Pre-load baseline for fast _LastUpdated string comparison in isServerNewer
|
|
122
|
+
await loadBaselineForComparison();
|
|
123
|
+
|
|
120
124
|
const structure = await loadStructureFile();
|
|
121
125
|
const saved = await loadClonePlacement();
|
|
122
126
|
const contentPlacement = saved.contentPlacement || 'bin';
|
|
123
|
-
const mediaPlacement = saved.mediaPlacement || 'bin';
|
|
124
127
|
|
|
125
128
|
const entityFilter = resolveEntityFilter(options.entity);
|
|
126
129
|
|
|
@@ -138,7 +141,7 @@ async function dryRunScan(appJson, options) {
|
|
|
138
141
|
// Scan media records
|
|
139
142
|
if (!entityFilter || entityFilter.has('media')) {
|
|
140
143
|
for (const record of (appJson.children.media || [])) {
|
|
141
|
-
const { metaPath } = resolveMediaPaths(record, structure
|
|
144
|
+
const { metaPath } = resolveMediaPaths(record, structure);
|
|
142
145
|
await scanRecord(record, metaPath, serverTz, configWithTz, counts, details, 'media');
|
|
143
146
|
}
|
|
144
147
|
}
|
|
@@ -156,13 +159,12 @@ async function dryRunScan(appJson, options) {
|
|
|
156
159
|
const { metaPath } = resolveEntityDirPaths(entityName, record, dirName);
|
|
157
160
|
await scanRecord(record, metaPath, serverTz, configWithTz, counts, details, entityName);
|
|
158
161
|
}
|
|
159
|
-
} else
|
|
162
|
+
} else {
|
|
160
163
|
for (const record of entries) {
|
|
161
|
-
const { metaPath } = resolveEntityDirPaths(entityName, record, entityName);
|
|
164
|
+
const { metaPath } = resolveEntityDirPaths(entityName, record, resolveEntityDirPath(entityName));
|
|
162
165
|
await scanRecord(record, metaPath, serverTz, configWithTz, counts, details, entityName);
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
|
-
// Generic entities in bins — skip for simplicity in dry-run (they use content placement)
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
// Scan output hierarchy (compound single-file format)
|
|
@@ -229,7 +231,7 @@ async function scanRecord(record, metaPath, _serverTz, configWithTz, counts, det
|
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
const localSyncTime = await getLocalSyncTime(metaPath);
|
|
232
|
-
if (record._LastUpdated && isServerNewer(localSyncTime, record._LastUpdated, configWithTz)) {
|
|
234
|
+
if (record._LastUpdated && isServerNewer(localSyncTime, record._LastUpdated, configWithTz, entityName, record.UID)) {
|
|
233
235
|
counts.changed++;
|
|
234
236
|
details.changed.push({ entity: entityName, name });
|
|
235
237
|
} else {
|
|
@@ -258,8 +260,10 @@ export const pullCommand = new Command('pull')
|
|
|
258
260
|
.option('--force', 'Force re-processing, skip change detection')
|
|
259
261
|
.option('--descriptor-types <bool>', 'Sort extensions into descriptor sub-directories (default: true)', 'true')
|
|
260
262
|
.option('--documentation-only', 'When used with -e extension, pull only documentation extensions')
|
|
263
|
+
.option('--no-migrate', 'Skip pending migrations for this invocation')
|
|
261
264
|
.action(async (options) => {
|
|
262
265
|
try {
|
|
266
|
+
await runPendingMigrations(options);
|
|
263
267
|
const config = await loadConfig();
|
|
264
268
|
|
|
265
269
|
if (!config.AppShortName) {
|