@dboio/cli 0.6.14 → 0.8.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.
- package/README.md +313 -8
- package/package.json +3 -2
- package/src/commands/add.js +56 -11
- package/src/commands/clone.js +1294 -75
- package/src/commands/content.js +1 -1
- package/src/commands/deploy.js +1 -1
- package/src/commands/init.js +39 -4
- package/src/commands/install.js +10 -1
- package/src/commands/login.js +4 -9
- package/src/commands/output.js +56 -3
- package/src/commands/pull.js +3 -3
- package/src/commands/push.js +24 -12
- package/src/lib/config.js +175 -8
- package/src/lib/diff.js +72 -14
- package/src/lib/ignore.js +145 -0
- package/src/lib/input-parser.js +87 -38
- package/src/lib/structure.js +130 -0
- package/src/lib/timestamps.js +31 -9
package/src/commands/content.js
CHANGED
|
@@ -116,7 +116,7 @@ const deployCmd = new Command('deploy')
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
119
|
-
const errorResult = await checkSubmitErrors(result);
|
|
119
|
+
const errorResult = await checkSubmitErrors(result, { rowUid: uid });
|
|
120
120
|
if (errorResult) {
|
|
121
121
|
if (errorResult.skipRecord || errorResult.skipAll) {
|
|
122
122
|
log.info('Submission cancelled');
|
package/src/commands/deploy.js
CHANGED
|
@@ -156,7 +156,7 @@ export const deployCommand = new Command('deploy')
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
159
|
-
const errorResult = await checkSubmitErrors(result);
|
|
159
|
+
const errorResult = await checkSubmitErrors(result, { rowUid: uid });
|
|
160
160
|
if (errorResult) {
|
|
161
161
|
if (errorResult.skipRecord) {
|
|
162
162
|
log.warn(` Skipping "${entryName}"`);
|
package/src/commands/init.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { access, readdir } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { isInitialized, hasLegacyConfig, readLegacyConfig, initConfig, saveCredentials, ensureGitignore, saveTransactionKeyPreset } from '../lib/config.js';
|
|
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
8
|
import { log } from '../lib/logger.js';
|
|
8
9
|
import { checkDomainChange, writeAppJsonDomain } from '../lib/domain-guard.js';
|
|
9
10
|
|
|
@@ -19,10 +20,22 @@ export const initCommand = new Command('init')
|
|
|
19
20
|
.option('-g, --global', 'Install Claude commands to user home directory (~/.claude/commands/)')
|
|
20
21
|
.option('--local', 'Install Claude commands to project directory (.claude/commands/)')
|
|
21
22
|
.option('--scaffold', 'Create standard project directories (App Versions, Automations, Bins, …)')
|
|
23
|
+
.option('--dboignore', 'Create or reset .dboignore to defaults (use with --force to overwrite)')
|
|
22
24
|
.action(async (options) => {
|
|
23
25
|
// Merge --yes into nonInteractive
|
|
24
26
|
if (options.yes) options.nonInteractive = true;
|
|
25
27
|
try {
|
|
28
|
+
// --dboignore: standalone operation, works regardless of init state
|
|
29
|
+
if (options.dboignore) {
|
|
30
|
+
const created = await createDboignore(process.cwd(), { force: options.force });
|
|
31
|
+
if (created) {
|
|
32
|
+
log.success(options.force ? 'Reset .dboignore to default patterns' : 'Created .dboignore with default patterns');
|
|
33
|
+
} else {
|
|
34
|
+
log.warn('.dboignore already exists. Use --force to overwrite with defaults.');
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
if (await isInitialized() && !options.force) {
|
|
27
40
|
if (options.scaffold) {
|
|
28
41
|
const result = await scaffoldProjectDirs();
|
|
@@ -86,6 +99,9 @@ export const initCommand = new Command('init')
|
|
|
86
99
|
// Ensure sensitive files are gitignored
|
|
87
100
|
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/config.local.json', '.dbo/ticketing.local.json']);
|
|
88
101
|
|
|
102
|
+
const createdIgnore = await createDboignore();
|
|
103
|
+
if (createdIgnore) log.dim(' Created .dboignore');
|
|
104
|
+
|
|
89
105
|
log.success(`Initialized .dbo/ for ${domain}`);
|
|
90
106
|
log.dim(' Run "dbo login" to authenticate.');
|
|
91
107
|
|
|
@@ -107,13 +123,32 @@ export const initCommand = new Command('init')
|
|
|
107
123
|
await saveTransactionKeyPreset('RowUID');
|
|
108
124
|
}
|
|
109
125
|
|
|
126
|
+
// Prompt for TicketSuggestionOutput
|
|
127
|
+
const DEFAULT_TICKET_SUGGESTION_OUTPUT = 'ojaie9t3o0kfvliahnuuda';
|
|
128
|
+
if (!options.nonInteractive) {
|
|
129
|
+
const inquirer = (await import('inquirer')).default;
|
|
130
|
+
const { ticketSuggestionOutput } = await inquirer.prompt([{
|
|
131
|
+
type: 'input',
|
|
132
|
+
name: 'ticketSuggestionOutput',
|
|
133
|
+
message: 'Output UID for ticket suggestions (leave blank to skip):',
|
|
134
|
+
default: DEFAULT_TICKET_SUGGESTION_OUTPUT,
|
|
135
|
+
}]);
|
|
136
|
+
const tsVal = ticketSuggestionOutput.trim();
|
|
137
|
+
if (tsVal) {
|
|
138
|
+
await saveTicketSuggestionOutput(tsVal);
|
|
139
|
+
log.dim(` TicketSuggestionOutput: ${tsVal}`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
await saveTicketSuggestionOutput(DEFAULT_TICKET_SUGGESTION_OUTPUT);
|
|
143
|
+
}
|
|
144
|
+
|
|
110
145
|
// ─── Scaffold ─────────────────────────────────────────────────────────────
|
|
111
146
|
let shouldScaffold = options.scaffold;
|
|
112
147
|
|
|
113
148
|
if (!shouldScaffold && !options.nonInteractive) {
|
|
114
|
-
const entries = await readdir(process.cwd());
|
|
115
|
-
const
|
|
116
|
-
const isEmpty = entries.every(e =>
|
|
149
|
+
const entries = await readdir(process.cwd(), { withFileTypes: true });
|
|
150
|
+
const ig = await loadIgnore();
|
|
151
|
+
const isEmpty = entries.every(e => ig.ignores(e.isDirectory() ? e.name + '/' : e.name));
|
|
117
152
|
|
|
118
153
|
const inquirer = (await import('inquirer')).default;
|
|
119
154
|
const { doScaffold } = await inquirer.prompt([{
|
package/src/commands/install.js
CHANGED
|
@@ -235,7 +235,7 @@ async function promptForScope(pluginName) {
|
|
|
235
235
|
|
|
236
236
|
/**
|
|
237
237
|
* Resolve the scope for a plugin based on flags and stored preferences.
|
|
238
|
-
* Priority: explicit flag > stored preference > prompt.
|
|
238
|
+
* Priority: explicit flag > stored preference > existing installation > prompt.
|
|
239
239
|
* @param {string} pluginName - Plugin name without .md
|
|
240
240
|
* @param {object} options - Command options with global/local flags
|
|
241
241
|
* @returns {Promise<'project' | 'global'>}
|
|
@@ -247,6 +247,15 @@ async function resolvePluginScope(pluginName, options) {
|
|
|
247
247
|
const storedScope = await getPluginScope(pluginName);
|
|
248
248
|
if (storedScope) return storedScope;
|
|
249
249
|
|
|
250
|
+
// Infer from existing installation — avoids re-prompting on re-installs
|
|
251
|
+
// (e.g. postinstall after npm install when .dbo/ isn't in cwd)
|
|
252
|
+
const registry = await readPluginRegistry();
|
|
253
|
+
const key = `${pluginName}@${PLUGIN_MARKETPLACE}`;
|
|
254
|
+
if (registry.plugins[key]) return 'global';
|
|
255
|
+
|
|
256
|
+
const projectPluginDir = join(process.cwd(), '.claude', 'plugins', pluginName);
|
|
257
|
+
if (existsSync(projectPluginDir)) return 'project';
|
|
258
|
+
|
|
250
259
|
return await promptForScope(pluginName);
|
|
251
260
|
}
|
|
252
261
|
|
package/src/commands/login.js
CHANGED
|
@@ -50,22 +50,17 @@ export const loginCommand = new Command('login')
|
|
|
50
50
|
// Ensure sensitive files are gitignored
|
|
51
51
|
await ensureGitignore(['.dbo/credentials.json', '.dbo/cookies.txt', '.dbo/ticketing.local.json']);
|
|
52
52
|
|
|
53
|
-
// Fetch current user info to store ID
|
|
53
|
+
// Fetch current user info to store ID for future submissions
|
|
54
54
|
try {
|
|
55
55
|
const userResult = await client.get('/api/output/31799e38f0854956a47f10', { '_format': 'json_raw' });
|
|
56
56
|
const userData = userResult.payload || userResult.data;
|
|
57
57
|
const rows = Array.isArray(userData) ? userData : (userData?.Rows || userData?.rows || []);
|
|
58
58
|
if (rows.length > 0) {
|
|
59
59
|
const row = rows[0];
|
|
60
|
-
const userUid = row.UID || row.uid;
|
|
61
60
|
const userId = row.ID || row.id || row.UserID || row.userId;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (info.userUid || info.userId) {
|
|
66
|
-
await saveUserInfo(info);
|
|
67
|
-
if (info.userId) log.dim(` User ID: ${info.userId}`);
|
|
68
|
-
if (info.userUid) log.dim(` User UID: ${info.userUid}`);
|
|
61
|
+
if (userId) {
|
|
62
|
+
await saveUserInfo({ userId: String(userId) });
|
|
63
|
+
log.dim(` User ID: ${userId}`);
|
|
69
64
|
}
|
|
70
65
|
|
|
71
66
|
// Save user profile (name, email) for package.json population
|
package/src/commands/output.js
CHANGED
|
@@ -25,6 +25,22 @@ export const outputCommand = new Command('output')
|
|
|
25
25
|
.option('--template <value>', 'Custom template')
|
|
26
26
|
.option('--debug', 'Include debug info')
|
|
27
27
|
.option('--debug-sql', 'Include SQL debug info')
|
|
28
|
+
.option('--debug-verbose', 'Verbose debug output')
|
|
29
|
+
.option('--debug-analysis', 'Analysis debug output')
|
|
30
|
+
.option('--limit <n>', 'Maximum rows to return (_limit)')
|
|
31
|
+
.option('--rowcount <bool>', 'Include row count: true (default) or false for performance')
|
|
32
|
+
.option('--display <expr>', 'Show/hide template tags (repeatable), e.g., "sidebar=hide"', collect, [])
|
|
33
|
+
.option('--format-values', 'Enable value formatting in json_raw output')
|
|
34
|
+
.option('--empty-response-code <code>', 'HTTP status code when output returns no results')
|
|
35
|
+
.option('--fallback-content <expr>', 'Fallback content UID for error codes, e.g., "404=contentUID"')
|
|
36
|
+
.option('--escape-html <bool>', 'Control HTML escaping: true or false')
|
|
37
|
+
.option('--mime <type>', 'Override MIME/content type')
|
|
38
|
+
.option('--strict', 'Strict error mode')
|
|
39
|
+
.option('--confirm', 'Confirmation flag')
|
|
40
|
+
.option('--include <expr>', 'Include token')
|
|
41
|
+
.option('--no-transaction', 'Disable transaction wrapping')
|
|
42
|
+
.option('--skip <phase>', 'Skip execution phases (repeatable, admin-only)', collect, [])
|
|
43
|
+
.option('--profile', 'Enable MiniProfiler output')
|
|
28
44
|
.option('--meta', 'Use meta output endpoint')
|
|
29
45
|
.option('--meta-column <uid>', 'Column metadata')
|
|
30
46
|
.option('--save', 'Interactive save-to-disk mode')
|
|
@@ -66,8 +82,45 @@ export const outputCommand = new Command('output')
|
|
|
66
82
|
if (options.maxrows) params['_maxrows'] = options.maxrows;
|
|
67
83
|
if (options.rows) params['_rows'] = options.rows;
|
|
68
84
|
if (options.template) params['_template'] = options.template;
|
|
85
|
+
if (options.limit) params['_limit'] = options.limit;
|
|
86
|
+
if (options.rowcount !== undefined) params['_rowcount'] = options.rowcount;
|
|
69
87
|
if (options.debug) params['_debug'] = 'true';
|
|
70
88
|
if (options.debugSql) params['_debug_sql'] = 'true';
|
|
89
|
+
if (options.debugVerbose) params['_debug:verbose'] = 'true';
|
|
90
|
+
if (options.debugAnalysis) params['_debug:analysis'] = 'true';
|
|
91
|
+
if (options.formatValues) params['_format_values'] = 'true';
|
|
92
|
+
if (options.emptyResponseCode) params['_empty_response_code'] = options.emptyResponseCode;
|
|
93
|
+
if (options.escapeHtml !== undefined) params['_escape_html'] = options.escapeHtml;
|
|
94
|
+
if (options.mime) params['_mime'] = options.mime;
|
|
95
|
+
if (options.strict) params['_strict'] = 'true';
|
|
96
|
+
if (options.confirm) params['_confirm'] = 'true';
|
|
97
|
+
if (options.include) params['_include'] = options.include;
|
|
98
|
+
if (options.transaction === false) params['_no_transaction'] = 'true';
|
|
99
|
+
if (options.profile) params['_profile'] = 'true';
|
|
100
|
+
|
|
101
|
+
// --display: "tagName=show|hide" → _display@tagName=show|hide
|
|
102
|
+
for (const d of options.display) {
|
|
103
|
+
const eqIdx = d.indexOf('=');
|
|
104
|
+
if (eqIdx !== -1) {
|
|
105
|
+
params[`_display@${d.substring(0, eqIdx)}`] = d.substring(eqIdx + 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --fallback-content: "404=contentUID" → _fallback_content:404=contentUID
|
|
110
|
+
if (options.fallbackContent) {
|
|
111
|
+
const fb = options.fallbackContent;
|
|
112
|
+
if (fb.includes('=')) {
|
|
113
|
+
const [code, uid] = fb.split('=', 2);
|
|
114
|
+
params[`_fallback_content:${code}`] = uid;
|
|
115
|
+
} else {
|
|
116
|
+
params['_fallback_content'] = fb;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --skip: repeatable → array
|
|
121
|
+
if (options.skip.length > 0) {
|
|
122
|
+
params['_skip'] = options.skip;
|
|
123
|
+
}
|
|
71
124
|
|
|
72
125
|
for (const f of options.filter) {
|
|
73
126
|
const atIdx = f.indexOf('=');
|
|
@@ -84,9 +137,9 @@ export const outputCommand = new Command('output')
|
|
|
84
137
|
}
|
|
85
138
|
}
|
|
86
139
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
params[
|
|
140
|
+
// Pass sort values through directly — API expects _sort=Column:DIR
|
|
141
|
+
if (options.sort.length > 0) {
|
|
142
|
+
params['_sort'] = options.sort;
|
|
90
143
|
}
|
|
91
144
|
|
|
92
145
|
// For save mode or jq mode, force json_raw if user didn't set a custom format
|
package/src/commands/pull.js
CHANGED
|
@@ -40,9 +40,9 @@ export const pullCommand = new Command('pull')
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
params[
|
|
43
|
+
// Pass sort values through directly — API expects _sort=Column:DIR
|
|
44
|
+
if (options.sort.length > 0) {
|
|
45
|
+
params['_sort'] = options.sort;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
let path;
|
package/src/commands/push.js
CHANGED
|
@@ -13,7 +13,19 @@ import { resolveTransactionKey } from '../lib/transaction-key.js';
|
|
|
13
13
|
import { setFileTimestamps } from '../lib/timestamps.js';
|
|
14
14
|
import { findMetadataFiles } from '../lib/diff.js';
|
|
15
15
|
import { detectChangedColumns, findBaselineEntry } from '../lib/delta.js';
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve an @reference file path to an absolute filesystem path.
|
|
19
|
+
* "@filename.ext" → relative to the metadata file's directory (existing behaviour)
|
|
20
|
+
* "@/Documentation/..." → relative to project root (process.cwd())
|
|
21
|
+
*/
|
|
22
|
+
function resolveAtReference(refFile, metaDir) {
|
|
23
|
+
if (refFile.startsWith('/')) {
|
|
24
|
+
return join(process.cwd(), refFile);
|
|
25
|
+
}
|
|
26
|
+
return join(metaDir, refFile);
|
|
27
|
+
}
|
|
28
|
+
import { ENTITY_DEPENDENCIES } from '../lib/dependencies.js';
|
|
17
29
|
|
|
18
30
|
export const pushCommand = new Command('push')
|
|
19
31
|
.description('Push local files back to DBO.io using metadata from pull')
|
|
@@ -214,7 +226,7 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
214
226
|
for (const col of contentCols) {
|
|
215
227
|
const ref = meta[col];
|
|
216
228
|
if (ref && ref.startsWith('@')) {
|
|
217
|
-
const refPath =
|
|
229
|
+
const refPath = resolveAtReference(ref.substring(1), dirname(metaPath));
|
|
218
230
|
try {
|
|
219
231
|
await stat(refPath);
|
|
220
232
|
} catch {
|
|
@@ -262,13 +274,12 @@ async function pushDirectory(dirPath, client, options, modifyKey = null, transac
|
|
|
262
274
|
}
|
|
263
275
|
}
|
|
264
276
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
277
|
+
// Sort by dependency level: children first (ascending level) for add/edit operations
|
|
278
|
+
toPush.sort((a, b) => {
|
|
279
|
+
const levelA = ENTITY_DEPENDENCIES[a.meta._entity] || 0;
|
|
280
|
+
const levelB = ENTITY_DEPENDENCIES[b.meta._entity] || 0;
|
|
281
|
+
return levelA - levelB;
|
|
282
|
+
});
|
|
272
283
|
|
|
273
284
|
// Process in dependency order
|
|
274
285
|
let succeeded = 0;
|
|
@@ -361,6 +372,7 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
361
372
|
for (const [key, value] of Object.entries(meta)) {
|
|
362
373
|
if (shouldSkipColumn(key)) continue;
|
|
363
374
|
if (key === 'UID') continue; // UID is the identifier, not a column to update
|
|
375
|
+
if (key === 'children') continue; // Output hierarchy structural field, not a server column
|
|
364
376
|
if (value === null || value === undefined) continue;
|
|
365
377
|
|
|
366
378
|
// Delta sync: skip columns not in changedColumns
|
|
@@ -378,7 +390,7 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
378
390
|
if (strValue.startsWith('@')) {
|
|
379
391
|
// @filename reference — resolve to actual file path
|
|
380
392
|
const refFile = strValue.substring(1);
|
|
381
|
-
const refPath =
|
|
393
|
+
const refPath = resolveAtReference(refFile, metaDir);
|
|
382
394
|
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${entity}.${key}@${refPath}`);
|
|
383
395
|
} else {
|
|
384
396
|
dataExprs.push(`${rowKeyPrefix}:${rowKeyValue};column:${entity}.${key}=${strValue}`);
|
|
@@ -413,7 +425,7 @@ async function pushFromMetadata(meta, metaPath, client, options, changedColumns
|
|
|
413
425
|
}
|
|
414
426
|
|
|
415
427
|
// Retry with prompted params if needed (ticket, user, repo mismatch)
|
|
416
|
-
const retryResult = await checkSubmitErrors(result);
|
|
428
|
+
const retryResult = await checkSubmitErrors(result, { rowUid: uid });
|
|
417
429
|
if (retryResult) {
|
|
418
430
|
// Handle skip actions
|
|
419
431
|
if (retryResult.skipRecord) {
|
|
@@ -647,7 +659,7 @@ async function updateBaselineAfterPush(baseline, successfulPushes) {
|
|
|
647
659
|
if (strValue.startsWith('@')) {
|
|
648
660
|
try {
|
|
649
661
|
const refFile = strValue.substring(1);
|
|
650
|
-
const refPath =
|
|
662
|
+
const refPath = resolveAtReference(refFile, dirname(metaPath));
|
|
651
663
|
const fileContent = await readFile(refPath, 'utf8');
|
|
652
664
|
baselineEntry[col] = fileContent;
|
|
653
665
|
modified = true;
|
package/src/lib/config.js
CHANGED
|
@@ -69,7 +69,7 @@ export async function initConfig(domain) {
|
|
|
69
69
|
|
|
70
70
|
export async function saveCredentials(username) {
|
|
71
71
|
await mkdir(dboDir(), { recursive: true });
|
|
72
|
-
// Preserve existing fields (like
|
|
72
|
+
// Preserve existing fields (like userId) — never store password
|
|
73
73
|
let existing = {};
|
|
74
74
|
try {
|
|
75
75
|
existing = JSON.parse(await readFile(credentialsPath(), 'utf8'));
|
|
@@ -79,14 +79,13 @@ export async function saveCredentials(username) {
|
|
|
79
79
|
await writeFile(credentialsPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export async function saveUserInfo({ userId
|
|
82
|
+
export async function saveUserInfo({ userId }) {
|
|
83
83
|
await mkdir(dboDir(), { recursive: true });
|
|
84
84
|
let existing = {};
|
|
85
85
|
try {
|
|
86
86
|
existing = JSON.parse(await readFile(credentialsPath(), 'utf8'));
|
|
87
87
|
} catch { /* no existing file */ }
|
|
88
88
|
if (userId !== undefined) existing.userId = userId;
|
|
89
|
-
if (userUid !== undefined) existing.userUid = userUid;
|
|
90
89
|
await writeFile(credentialsPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
91
90
|
}
|
|
92
91
|
|
|
@@ -94,12 +93,9 @@ export async function loadUserInfo() {
|
|
|
94
93
|
try {
|
|
95
94
|
const raw = await readFile(credentialsPath(), 'utf8');
|
|
96
95
|
const creds = JSON.parse(raw);
|
|
97
|
-
return {
|
|
98
|
-
userId: creds.userId || null,
|
|
99
|
-
userUid: creds.userUid || null,
|
|
100
|
-
};
|
|
96
|
+
return { userId: creds.userId || null };
|
|
101
97
|
} catch {
|
|
102
|
-
return { userId: null
|
|
98
|
+
return { userId: null };
|
|
103
99
|
}
|
|
104
100
|
}
|
|
105
101
|
|
|
@@ -529,6 +525,50 @@ export async function getAllPluginScopes() {
|
|
|
529
525
|
return result;
|
|
530
526
|
}
|
|
531
527
|
|
|
528
|
+
// ─── Output Hierarchy Filename Preferences ────────────────────────────────
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Config keys for output hierarchy filename column preferences.
|
|
532
|
+
* Maps physical table names to config key names.
|
|
533
|
+
*/
|
|
534
|
+
const OUTPUT_FILENAME_CONFIG_KEYS = {
|
|
535
|
+
output: 'OutputFilenameCol',
|
|
536
|
+
output_value: 'OutputColumnFilenameCol',
|
|
537
|
+
output_value_filter: 'OutputFilterFilenameCol',
|
|
538
|
+
output_value_entity_column_rel: 'OutputJoinFilenameCol',
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Load output filename column preference for an entity type.
|
|
543
|
+
* @param {string} entityKey - Physical table name (e.g., 'output', 'output_value')
|
|
544
|
+
* @returns {Promise<string|null>}
|
|
545
|
+
*/
|
|
546
|
+
export async function loadOutputFilenamePreference(entityKey) {
|
|
547
|
+
try {
|
|
548
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
549
|
+
const config = JSON.parse(raw);
|
|
550
|
+
const configKey = OUTPUT_FILENAME_CONFIG_KEYS[entityKey];
|
|
551
|
+
return configKey ? (config[configKey] || null) : null;
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Save output filename column preference for an entity type.
|
|
559
|
+
* @param {string} entityKey - Physical table name
|
|
560
|
+
* @param {string} columnName - Column name to use as filename
|
|
561
|
+
*/
|
|
562
|
+
export async function saveOutputFilenamePreference(entityKey, columnName) {
|
|
563
|
+
const configKey = OUTPUT_FILENAME_CONFIG_KEYS[entityKey];
|
|
564
|
+
if (!configKey) return;
|
|
565
|
+
await mkdir(dboDir(), { recursive: true });
|
|
566
|
+
let existing = {};
|
|
567
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
568
|
+
existing[configKey] = columnName;
|
|
569
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
570
|
+
}
|
|
571
|
+
|
|
532
572
|
// ─── AppModifyKey ─────────────────────────────────────────────────────────
|
|
533
573
|
|
|
534
574
|
/**
|
|
@@ -580,6 +620,32 @@ export async function loadTransactionKeyPreset() {
|
|
|
580
620
|
} catch { return null; }
|
|
581
621
|
}
|
|
582
622
|
|
|
623
|
+
// ─── TicketSuggestionOutput ────────────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Save TicketSuggestionOutput to .dbo/config.json.
|
|
627
|
+
* This is the output UID used to fetch ticket suggestions.
|
|
628
|
+
*/
|
|
629
|
+
export async function saveTicketSuggestionOutput(outputUid) {
|
|
630
|
+
await mkdir(dboDir(), { recursive: true });
|
|
631
|
+
let existing = {};
|
|
632
|
+
try { existing = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
633
|
+
if (outputUid != null) existing.TicketSuggestionOutput = outputUid;
|
|
634
|
+
else delete existing.TicketSuggestionOutput;
|
|
635
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Load TicketSuggestionOutput from .dbo/config.json.
|
|
640
|
+
* Returns the output UID string or null if not set.
|
|
641
|
+
*/
|
|
642
|
+
export async function loadTicketSuggestionOutput() {
|
|
643
|
+
try {
|
|
644
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
645
|
+
return JSON.parse(raw).TicketSuggestionOutput || null;
|
|
646
|
+
} catch { return null; }
|
|
647
|
+
}
|
|
648
|
+
|
|
583
649
|
// ─── Gitignore ────────────────────────────────────────────────────────────
|
|
584
650
|
|
|
585
651
|
/**
|
|
@@ -658,3 +724,104 @@ export async function loadAppJsonBaseline() {
|
|
|
658
724
|
export async function saveAppJsonBaseline(data) {
|
|
659
725
|
await writeFile(baselinePath(), JSON.stringify(data, null, 2) + '\n');
|
|
660
726
|
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Save the clone source to .dbo/config.json.
|
|
730
|
+
* "default" = fetched from server via AppShortName.
|
|
731
|
+
* Any other value = explicit local file path or URL provided by the user.
|
|
732
|
+
*/
|
|
733
|
+
export async function saveCloneSource(source) {
|
|
734
|
+
await mkdir(dboDir(), { recursive: true });
|
|
735
|
+
let existing = {};
|
|
736
|
+
try {
|
|
737
|
+
existing = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
738
|
+
} catch { /* no existing config */ }
|
|
739
|
+
existing.cloneSource = source;
|
|
740
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Load the stored clone source from .dbo/config.json.
|
|
745
|
+
* Returns null if not set.
|
|
746
|
+
*/
|
|
747
|
+
export async function loadCloneSource() {
|
|
748
|
+
try {
|
|
749
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
750
|
+
const config = JSON.parse(raw);
|
|
751
|
+
return config.cloneSource || null;
|
|
752
|
+
} catch {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ─── Descriptor-level Extension Preferences ───────────────────────────────
|
|
758
|
+
|
|
759
|
+
/** Save filename column preference for a specific Descriptor value.
|
|
760
|
+
* Config key: "Extension_<descriptor>_FilenameCol"
|
|
761
|
+
*/
|
|
762
|
+
export async function saveDescriptorFilenamePreference(descriptor, columnName) {
|
|
763
|
+
await mkdir(dboDir(), { recursive: true });
|
|
764
|
+
let cfg = {};
|
|
765
|
+
try { cfg = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
766
|
+
if (columnName === null) {
|
|
767
|
+
delete cfg[`Extension_${descriptor}_FilenameCol`];
|
|
768
|
+
} else {
|
|
769
|
+
cfg[`Extension_${descriptor}_FilenameCol`] = columnName;
|
|
770
|
+
}
|
|
771
|
+
await writeFile(configPath(), JSON.stringify(cfg, null, 2) + '\n');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** Load filename column preference for a specific Descriptor value. Returns null if not set. */
|
|
775
|
+
export async function loadDescriptorFilenamePreference(descriptor) {
|
|
776
|
+
try {
|
|
777
|
+
const cfg = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
778
|
+
return cfg[`Extension_${descriptor}_FilenameCol`] || null;
|
|
779
|
+
} catch { return null; }
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Save content extraction preferences for a specific Descriptor value.
|
|
783
|
+
* Config key: "Extension_<descriptor>_ContentExtractions"
|
|
784
|
+
* Value: { "ColName": "css", "Other": false, ... }
|
|
785
|
+
*/
|
|
786
|
+
export async function saveDescriptorContentExtractions(descriptor, extractions) {
|
|
787
|
+
await mkdir(dboDir(), { recursive: true });
|
|
788
|
+
let cfg = {};
|
|
789
|
+
try { cfg = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
790
|
+
if (extractions === null) {
|
|
791
|
+
delete cfg[`Extension_${descriptor}_ContentExtractions`];
|
|
792
|
+
} else {
|
|
793
|
+
cfg[`Extension_${descriptor}_ContentExtractions`] = extractions;
|
|
794
|
+
}
|
|
795
|
+
await writeFile(configPath(), JSON.stringify(cfg, null, 2) + '\n');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** Load content extraction preferences for a specific Descriptor value. Returns null if not saved. */
|
|
799
|
+
export async function loadDescriptorContentExtractions(descriptor) {
|
|
800
|
+
try {
|
|
801
|
+
const cfg = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
802
|
+
return cfg[`Extension_${descriptor}_ContentExtractions`] || null;
|
|
803
|
+
} catch { return null; }
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** Save ExtensionDocumentationMDPlacement preference.
|
|
807
|
+
* @param {'inline'|'root'|null} placement — null clears the key
|
|
808
|
+
*/
|
|
809
|
+
export async function saveExtensionDocumentationMDPlacement(placement) {
|
|
810
|
+
await mkdir(dboDir(), { recursive: true });
|
|
811
|
+
let cfg = {};
|
|
812
|
+
try { cfg = JSON.parse(await readFile(configPath(), 'utf8')); } catch {}
|
|
813
|
+
if (placement === null) {
|
|
814
|
+
delete cfg.ExtensionDocumentationMDPlacement;
|
|
815
|
+
} else {
|
|
816
|
+
cfg.ExtensionDocumentationMDPlacement = placement;
|
|
817
|
+
}
|
|
818
|
+
await writeFile(configPath(), JSON.stringify(cfg, null, 2) + '\n');
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** Load ExtensionDocumentationMDPlacement preference. Returns 'inline', 'root', or null. */
|
|
822
|
+
export async function loadExtensionDocumentationMDPlacement() {
|
|
823
|
+
try {
|
|
824
|
+
const cfg = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
825
|
+
return cfg.ExtensionDocumentationMDPlacement || null;
|
|
826
|
+
} catch { return null; }
|
|
827
|
+
}
|