@dboio/cli 0.9.8 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/bin/dbo.js +2 -0
- package/package.json +1 -1
- package/src/commands/add.js +46 -0
- package/src/commands/clone.js +557 -239
- package/src/commands/init.js +30 -32
- package/src/commands/pull.js +264 -87
- package/src/commands/push.js +502 -57
- package/src/commands/rm.js +1 -1
- package/src/commands/sync.js +68 -0
- package/src/lib/config.js +49 -8
- package/src/lib/delta.js +86 -25
- package/src/lib/diff.js +9 -3
- package/src/lib/folder-icon.js +120 -0
- package/src/lib/ignore.js +1 -1
- package/src/lib/input-parser.js +37 -10
- package/src/lib/scaffold.js +82 -2
- package/src/lib/structure.js +2 -0
package/src/commands/rm.js
CHANGED
|
@@ -172,7 +172,7 @@ async function rmFile(filePath, options) {
|
|
|
172
172
|
process.exit(1);
|
|
173
173
|
}
|
|
174
174
|
if (!rowId) {
|
|
175
|
-
log.error(`No row ID found in "${metaPath}". Cannot build delete expression.`);
|
|
175
|
+
log.error(`No row ID found in "${metaPath}". Cannot build delete expression.\n Metadata needs _id or ${entity.charAt(0).toUpperCase() + entity.slice(1)}ID. Run "dbo pull" to populate.`);
|
|
176
176
|
process.exit(1);
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { log } from '../lib/logger.js';
|
|
3
|
+
import { loadConfig, loadAppConfig, saveAppJsonBaseline } from '../lib/config.js';
|
|
4
|
+
import { DboClient } from '../lib/client.js';
|
|
5
|
+
import { decodeBase64Fields } from './clone.js';
|
|
6
|
+
|
|
7
|
+
export const syncCommand = new Command('sync')
|
|
8
|
+
.description('Synchronise local state with the server')
|
|
9
|
+
.option('--baseline', 'Re-fetch server state and update .dbo/.app_baseline.json (does not modify local files)')
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
if (!options.baseline) {
|
|
12
|
+
log.warn('No sync mode specified. Use --baseline to reset the baseline file.');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const config = await loadConfig();
|
|
17
|
+
if (!config.domain) {
|
|
18
|
+
log.error('No domain configured. Run "dbo init" first.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const appConfig = await loadAppConfig();
|
|
23
|
+
if (!appConfig.AppShortName) {
|
|
24
|
+
log.error('No AppShortName found. Run "dbo clone" first.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ora = (await import('ora')).default;
|
|
29
|
+
const spinner = ora('Syncing baseline from server...').start();
|
|
30
|
+
|
|
31
|
+
const client = new DboClient({ domain: config.domain, verbose: options.verbose });
|
|
32
|
+
|
|
33
|
+
let result;
|
|
34
|
+
try {
|
|
35
|
+
result = await client.get(`/api/app/object/${appConfig.AppShortName}`);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
spinner.fail(`Failed to fetch app JSON: ${err.message}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const data = result.payload || result.data;
|
|
42
|
+
|
|
43
|
+
let appRecord;
|
|
44
|
+
if (Array.isArray(data)) {
|
|
45
|
+
appRecord = data.length > 0 ? data[0] : null;
|
|
46
|
+
} else if (data?.Rows?.length > 0) {
|
|
47
|
+
appRecord = data.Rows[0];
|
|
48
|
+
} else if (data?.rows?.length > 0) {
|
|
49
|
+
appRecord = data.rows[0];
|
|
50
|
+
} else if (data && typeof data === 'object' && (data.UID || data.ShortName)) {
|
|
51
|
+
appRecord = data;
|
|
52
|
+
} else {
|
|
53
|
+
appRecord = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!appRecord) {
|
|
57
|
+
spinner.fail(`No app found with ShortName "${appConfig.AppShortName}"`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Deep clone and decode base64 fields (same logic as clone's saveBaselineFile)
|
|
62
|
+
const baseline = JSON.parse(JSON.stringify(appRecord));
|
|
63
|
+
decodeBase64Fields(baseline);
|
|
64
|
+
|
|
65
|
+
await saveAppJsonBaseline(baseline);
|
|
66
|
+
spinner.succeed('.dbo/.app_baseline.json updated from server');
|
|
67
|
+
log.dim(' Run "dbo push" to sync local changes against the new baseline');
|
|
68
|
+
});
|
package/src/lib/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
1
|
+
import { readFile, writeFile, mkdir, access, chmod, unlink } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { log } from './logger.js';
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ const CONFIG_LOCAL_FILE = 'config.local.json';
|
|
|
8
8
|
const CREDENTIALS_FILE = 'credentials.json';
|
|
9
9
|
const COOKIES_FILE = 'cookies.txt';
|
|
10
10
|
const SYNCHRONIZE_FILE = 'synchronize.json';
|
|
11
|
-
const BASELINE_FILE = '.
|
|
11
|
+
const BASELINE_FILE = '.app_baseline.json';
|
|
12
12
|
|
|
13
13
|
function dboDir() {
|
|
14
14
|
return join(process.cwd(), DBO_DIR);
|
|
@@ -693,10 +693,10 @@ export async function ensureGitignore(patterns) {
|
|
|
693
693
|
for (const p of toAdd) log.dim(` Added ${p} to .gitignore`);
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
-
// ─── Baseline (.
|
|
696
|
+
// ─── Baseline (.dbo/.app_baseline.json) ───────────────────────────────────
|
|
697
697
|
|
|
698
|
-
function baselinePath() {
|
|
699
|
-
return join(
|
|
698
|
+
export function baselinePath() {
|
|
699
|
+
return join(dboDir(), BASELINE_FILE);
|
|
700
700
|
}
|
|
701
701
|
|
|
702
702
|
/**
|
|
@@ -707,11 +707,38 @@ export async function hasBaseline() {
|
|
|
707
707
|
}
|
|
708
708
|
|
|
709
709
|
/**
|
|
710
|
-
* Load .
|
|
710
|
+
* Load .dbo/.app_baseline.json baseline file.
|
|
711
|
+
* Auto-migrates from legacy root .app.json if the new path does not exist.
|
|
711
712
|
*/
|
|
712
713
|
export async function loadAppJsonBaseline() {
|
|
714
|
+
const newPath = baselinePath();
|
|
715
|
+
|
|
716
|
+
// Legacy migration: root .app.json → .dbo/.app_baseline.json
|
|
717
|
+
if (!(await exists(newPath))) {
|
|
718
|
+
const legacyPath = join(process.cwd(), '.app.json');
|
|
719
|
+
if (await exists(legacyPath)) {
|
|
720
|
+
let parsed;
|
|
721
|
+
try {
|
|
722
|
+
parsed = JSON.parse(await readFile(legacyPath, 'utf8'));
|
|
723
|
+
} catch {
|
|
724
|
+
log.warn('Could not migrate .app.json — file is not valid JSON. Delete it manually and run "dbo clone" to recreate the baseline.');
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
await mkdir(dboDir(), { recursive: true });
|
|
728
|
+
await writeFile(newPath, JSON.stringify(parsed, null, 2) + '\n');
|
|
729
|
+
try { await chmod(newPath, 0o444); } catch { /* ignore */ }
|
|
730
|
+
try {
|
|
731
|
+
await unlink(legacyPath);
|
|
732
|
+
} catch {
|
|
733
|
+
log.warn('Migrated baseline but could not delete root .app.json — please remove it manually.');
|
|
734
|
+
}
|
|
735
|
+
log.dim('Migrated .app.json → .dbo/.app_baseline.json (system-managed baseline)');
|
|
736
|
+
return parsed;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
713
740
|
try {
|
|
714
|
-
const raw = await readFile(
|
|
741
|
+
const raw = await readFile(newPath, 'utf8');
|
|
715
742
|
return JSON.parse(raw);
|
|
716
743
|
} catch {
|
|
717
744
|
return null;
|
|
@@ -719,10 +746,24 @@ export async function loadAppJsonBaseline() {
|
|
|
719
746
|
}
|
|
720
747
|
|
|
721
748
|
/**
|
|
722
|
-
* Save .
|
|
749
|
+
* Save .dbo/.app_baseline.json baseline file.
|
|
750
|
+
* Temporarily widens permissions before writing (chmod 0o644),
|
|
751
|
+
* then restores read-only (chmod 0o444) after writing.
|
|
723
752
|
*/
|
|
724
753
|
export async function saveAppJsonBaseline(data) {
|
|
754
|
+
await mkdir(dboDir(), { recursive: true });
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
await chmod(baselinePath(), 0o644);
|
|
758
|
+
} catch { /* file doesn't exist yet — first write */ }
|
|
759
|
+
|
|
725
760
|
await writeFile(baselinePath(), JSON.stringify(data, null, 2) + '\n');
|
|
761
|
+
|
|
762
|
+
try {
|
|
763
|
+
await chmod(baselinePath(), 0o444);
|
|
764
|
+
} catch {
|
|
765
|
+
log.warn('⚠ Could not set baseline file permissions — ensure .dbo/.app_baseline.json is not manually edited');
|
|
766
|
+
}
|
|
726
767
|
}
|
|
727
768
|
|
|
728
769
|
/**
|
package/src/lib/delta.js
CHANGED
|
@@ -1,37 +1,26 @@
|
|
|
1
1
|
import { readFile, stat } from 'fs/promises';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { log } from './logger.js';
|
|
4
|
+
import { loadAppJsonBaseline, saveAppJsonBaseline } from './config.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Load the baseline file
|
|
7
|
+
* Load the baseline file from disk.
|
|
8
|
+
* Delegates to loadAppJsonBaseline() in config.js for the canonical path.
|
|
7
9
|
*
|
|
8
|
-
* @param {string} cwd - Current working directory
|
|
9
10
|
* @returns {Promise<Object|null>} - Baseline JSON or null if not found
|
|
10
11
|
*/
|
|
11
|
-
export async function loadBaseline(
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const raw = await readFile(baselinePath, 'utf8');
|
|
15
|
-
return JSON.parse(raw);
|
|
16
|
-
} catch (err) {
|
|
17
|
-
if (err.code === 'ENOENT') {
|
|
18
|
-
return null; // Baseline doesn't exist
|
|
19
|
-
}
|
|
20
|
-
log.warn(`Failed to parse .app.json: ${err.message}`);
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
12
|
+
export async function loadBaseline() {
|
|
13
|
+
return loadAppJsonBaseline();
|
|
23
14
|
}
|
|
24
15
|
|
|
25
16
|
/**
|
|
26
|
-
* Save baseline data to .
|
|
17
|
+
* Save baseline data to disk.
|
|
18
|
+
* Delegates to saveAppJsonBaseline() in config.js for the canonical path.
|
|
27
19
|
*
|
|
28
20
|
* @param {Object} data - Baseline JSON data
|
|
29
|
-
* @param {string} cwd - Current working directory
|
|
30
21
|
*/
|
|
31
|
-
export async function saveBaseline(data
|
|
32
|
-
|
|
33
|
-
const baselinePath = join(cwd, '.app.json');
|
|
34
|
-
await writeFile(baselinePath, JSON.stringify(data, null, 2), 'utf8');
|
|
22
|
+
export async function saveBaseline(data) {
|
|
23
|
+
return saveAppJsonBaseline(data);
|
|
35
24
|
}
|
|
36
25
|
|
|
37
26
|
/**
|
|
@@ -180,7 +169,7 @@ export async function detectChangedColumns(metaPath, baseline) {
|
|
|
180
169
|
* @param {Object} metadata - Metadata object
|
|
181
170
|
* @returns {string[]} - Array of user column names
|
|
182
171
|
*/
|
|
183
|
-
function getAllUserColumns(metadata) {
|
|
172
|
+
export function getAllUserColumns(metadata) {
|
|
184
173
|
return Object.keys(metadata).filter(col => !shouldSkipColumn(col));
|
|
185
174
|
}
|
|
186
175
|
|
|
@@ -190,7 +179,7 @@ function getAllUserColumns(metadata) {
|
|
|
190
179
|
* @param {string} columnName - Column name
|
|
191
180
|
* @returns {boolean} - True if should skip
|
|
192
181
|
*/
|
|
193
|
-
function shouldSkipColumn(columnName) {
|
|
182
|
+
export function shouldSkipColumn(columnName) {
|
|
194
183
|
// Skip system columns starting with underscore, UID, and children
|
|
195
184
|
return columnName.startsWith('_') ||
|
|
196
185
|
columnName === 'UID' ||
|
|
@@ -203,7 +192,7 @@ function shouldSkipColumn(columnName) {
|
|
|
203
192
|
* @param {*} value - Value to check
|
|
204
193
|
* @returns {boolean} - True if reference
|
|
205
194
|
*/
|
|
206
|
-
function isReference(value) {
|
|
195
|
+
export function isReference(value) {
|
|
207
196
|
return typeof value === 'string' && value.startsWith('@');
|
|
208
197
|
}
|
|
209
198
|
|
|
@@ -215,7 +204,7 @@ function isReference(value) {
|
|
|
215
204
|
* @param {string} baseDir - Base directory containing metadata
|
|
216
205
|
* @returns {string} - Absolute file path
|
|
217
206
|
*/
|
|
218
|
-
function resolveReferencePath(reference, baseDir) {
|
|
207
|
+
export function resolveReferencePath(reference, baseDir) {
|
|
219
208
|
const refPath = reference.substring(1); // Strip leading @
|
|
220
209
|
if (refPath.startsWith('/')) {
|
|
221
210
|
return join(process.cwd(), refPath);
|
|
@@ -229,7 +218,7 @@ function resolveReferencePath(reference, baseDir) {
|
|
|
229
218
|
* @param {*} value - Value to normalize
|
|
230
219
|
* @returns {string} - Normalized string value
|
|
231
220
|
*/
|
|
232
|
-
function normalizeValue(value) {
|
|
221
|
+
export function normalizeValue(value) {
|
|
233
222
|
if (value === null || value === undefined) {
|
|
234
223
|
return '';
|
|
235
224
|
}
|
|
@@ -241,3 +230,75 @@ function normalizeValue(value) {
|
|
|
241
230
|
|
|
242
231
|
return String(value).trim();
|
|
243
232
|
}
|
|
233
|
+
|
|
234
|
+
// ─── Compound Output Delta Detection ────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
const _OUTPUT_DOC_KEYS = ['column', 'join', 'filter'];
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Detect changed columns across a compound output file (root + inline children).
|
|
240
|
+
*
|
|
241
|
+
* @param {string} metaPath - Path to the root output JSON file
|
|
242
|
+
* @param {Object} baseline - The baseline JSON
|
|
243
|
+
* @returns {Promise<{ root: string[], children: Object<string, string[]> }>}
|
|
244
|
+
* root: changed column names on the root output entity
|
|
245
|
+
* children: { uid: [changedCols] } for each inline child entity
|
|
246
|
+
*/
|
|
247
|
+
export async function detectOutputChanges(metaPath, baseline) {
|
|
248
|
+
const metaRaw = await readFile(metaPath, 'utf8');
|
|
249
|
+
const meta = JSON.parse(metaRaw);
|
|
250
|
+
const metaDir = dirname(metaPath);
|
|
251
|
+
|
|
252
|
+
// Detect root changes
|
|
253
|
+
const root = await _detectEntityChanges(meta, baseline, metaDir);
|
|
254
|
+
|
|
255
|
+
// Detect child changes recursively via children.{column,join,filter}
|
|
256
|
+
const children = {};
|
|
257
|
+
if (meta.children) {
|
|
258
|
+
await _walkChildrenForChanges(meta.children, baseline, metaDir, children);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { root, children };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function _detectEntityChanges(entity, baseline, metaDir) {
|
|
265
|
+
const { UID, _entity } = entity;
|
|
266
|
+
const baselineEntry = findBaselineEntry(baseline, _entity, UID);
|
|
267
|
+
if (!baselineEntry) {
|
|
268
|
+
return getAllUserColumns(entity);
|
|
269
|
+
}
|
|
270
|
+
const changed = [];
|
|
271
|
+
for (const [col, val] of Object.entries(entity)) {
|
|
272
|
+
if (shouldSkipColumn(col) || col === 'children') continue;
|
|
273
|
+
if (isReference(val)) {
|
|
274
|
+
const refPath = resolveReferencePath(val, metaDir);
|
|
275
|
+
if (await compareFileContent(refPath, baselineEntry[col])) changed.push(col);
|
|
276
|
+
} else {
|
|
277
|
+
if (normalizeValue(val) !== normalizeValue(baselineEntry[col])) {
|
|
278
|
+
// Skip Extension column when baseline is null/empty (same as detectChangedColumns)
|
|
279
|
+
if (col === 'Extension'
|
|
280
|
+
&& !normalizeValue(baselineEntry[col])
|
|
281
|
+
&& (baselineEntry[col] === null || baselineEntry[col] === undefined)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
changed.push(col);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return changed;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function _walkChildrenForChanges(childrenObj, baseline, metaDir, result) {
|
|
292
|
+
for (const docKey of _OUTPUT_DOC_KEYS) {
|
|
293
|
+
const entityArray = childrenObj[docKey];
|
|
294
|
+
if (!Array.isArray(entityArray) || entityArray.length === 0) continue;
|
|
295
|
+
for (const child of entityArray) {
|
|
296
|
+
const changed = await _detectEntityChanges(child, baseline, metaDir);
|
|
297
|
+
result[child.UID] = changed;
|
|
298
|
+
// Recurse into child's own children (e.g. column[0].children.filter)
|
|
299
|
+
if (child.children) {
|
|
300
|
+
await _walkChildrenForChanges(child.children, baseline, metaDir, result);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
package/src/lib/diff.js
CHANGED
|
@@ -48,9 +48,15 @@ export async function findMetadataFiles(dir, ig) {
|
|
|
48
48
|
const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
49
49
|
if (!ig.ignores(relPath)) results.push(fullPath);
|
|
50
50
|
} else if (entry.name.startsWith('_output~') && entry.name.endsWith('.json') && !entry.name.includes('.CustomSQL.')) {
|
|
51
|
-
// Output hierarchy files: _output~<name>~<uid>.json
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Output hierarchy root files only: _output~<name>~<uid>.json
|
|
52
|
+
// Exclude old-format child output files — they contain a dot-prefixed
|
|
53
|
+
// child-type segment (.column~, .join~, .filter~) before .json.
|
|
54
|
+
// Root output files never have these segments, even when <name> contains dots.
|
|
55
|
+
const isChildFile = /\.(column|join|filter)~/.test(entry.name);
|
|
56
|
+
if (!isChildFile) {
|
|
57
|
+
const relPath = relative(process.cwd(), fullPath).replace(/\\/g, '/');
|
|
58
|
+
if (!ig.ignores(relPath)) results.push(fullPath);
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { writeFile, stat, access } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Best-effort: apply the system trash/recycle-bin icon to a folder.
|
|
10
|
+
* Never throws — all errors are silently swallowed.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} folderPath Absolute path to the trash directory
|
|
13
|
+
*/
|
|
14
|
+
export async function applyTrashIcon(folderPath) {
|
|
15
|
+
try {
|
|
16
|
+
const s = await stat(folderPath);
|
|
17
|
+
if (!s.isDirectory()) return;
|
|
18
|
+
|
|
19
|
+
switch (process.platform) {
|
|
20
|
+
case 'darwin':
|
|
21
|
+
await applyMacIcon(folderPath);
|
|
22
|
+
break;
|
|
23
|
+
case 'win32':
|
|
24
|
+
await applyWindowsIcon(folderPath);
|
|
25
|
+
break;
|
|
26
|
+
case 'linux':
|
|
27
|
+
await applyLinuxIcon(folderPath);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Best-effort — never propagate
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Re-apply the trash icon only if the icon marker file is missing.
|
|
37
|
+
* Much cheaper than applyTrashIcon — skips the osascript/attrib call
|
|
38
|
+
* when the icon is already in place.
|
|
39
|
+
*
|
|
40
|
+
* Call this after moving files into trash/ to self-heal the icon
|
|
41
|
+
* in case the user cleared the directory contents.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} folderPath Absolute path to the trash directory
|
|
44
|
+
*/
|
|
45
|
+
export async function ensureTrashIcon(folderPath) {
|
|
46
|
+
try {
|
|
47
|
+
switch (process.platform) {
|
|
48
|
+
case 'darwin': {
|
|
49
|
+
// macOS: Icon\r file exists when icon is applied
|
|
50
|
+
const iconFile = join(folderPath, 'Icon\r');
|
|
51
|
+
try { await access(iconFile); return; } catch { /* missing — re-apply */ }
|
|
52
|
+
await applyMacIcon(folderPath);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case 'win32': {
|
|
56
|
+
const iniPath = join(folderPath, 'desktop.ini');
|
|
57
|
+
try { await access(iniPath); return; } catch { /* missing */ }
|
|
58
|
+
await applyWindowsIcon(folderPath);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case 'linux': {
|
|
62
|
+
const dirFile = join(folderPath, '.directory');
|
|
63
|
+
try { await access(dirFile); return; } catch { /* missing */ }
|
|
64
|
+
await applyLinuxIcon(folderPath);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// Best-effort — never propagate
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── macOS ──────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
const MACOS_TRASH_ICON =
|
|
76
|
+
'/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/TrashIcon.icns';
|
|
77
|
+
|
|
78
|
+
async function applyMacIcon(folderPath) {
|
|
79
|
+
await access(MACOS_TRASH_ICON);
|
|
80
|
+
|
|
81
|
+
const script =
|
|
82
|
+
'use framework "AppKit"\n' +
|
|
83
|
+
`set iconImage to (current application's NSImage's alloc()'s initWithContentsOfFile:"${MACOS_TRASH_ICON}")\n` +
|
|
84
|
+
`(current application's NSWorkspace's sharedWorkspace()'s setIcon:iconImage forFile:"${folderPath}" options:0)`;
|
|
85
|
+
|
|
86
|
+
await execFileAsync('osascript', ['-e', script], { timeout: 5000 });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Windows ────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
async function applyWindowsIcon(folderPath) {
|
|
92
|
+
const iniPath = join(folderPath, 'desktop.ini');
|
|
93
|
+
const iniContent =
|
|
94
|
+
'[.ShellClassInfo]\r\nIconResource=%SystemRoot%\\System32\\shell32.dll,31\r\n';
|
|
95
|
+
|
|
96
|
+
await writeFile(iniPath, iniContent);
|
|
97
|
+
await execFileAsync('attrib', ['+H', '+S', iniPath], { timeout: 5000 });
|
|
98
|
+
await execFileAsync('attrib', ['+S', folderPath], { timeout: 5000 });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Linux ──────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
async function applyLinuxIcon(folderPath) {
|
|
104
|
+
// KDE Dolphin — .directory file
|
|
105
|
+
await writeFile(
|
|
106
|
+
join(folderPath, '.directory'),
|
|
107
|
+
'[Desktop Entry]\nIcon=user-trash\n',
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// GNOME Nautilus — gio metadata (may not be available on KDE-only systems)
|
|
111
|
+
try {
|
|
112
|
+
await execFileAsync(
|
|
113
|
+
'gio',
|
|
114
|
+
['set', folderPath, 'metadata::custom-icon-name', 'user-trash'],
|
|
115
|
+
{ timeout: 5000 },
|
|
116
|
+
);
|
|
117
|
+
} catch {
|
|
118
|
+
// gio not available — .directory file is enough for KDE
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/lib/ignore.js
CHANGED
package/src/lib/input-parser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { log } from './logger.js';
|
|
3
|
-
import { loadUserInfo, loadTicketSuggestionOutput } from './config.js';
|
|
3
|
+
import { loadConfig, loadUserInfo, loadTicketSuggestionOutput } from './config.js';
|
|
4
4
|
import { buildTicketExpression, setGlobalTicket, setRecordTicket, getGlobalTicket, getRecordTicket, setTicketingRequired } from './ticketing.js';
|
|
5
5
|
import { DboClient } from './client.js';
|
|
6
6
|
|
|
@@ -186,7 +186,6 @@ export async function checkSubmitErrors(result, context = {}) {
|
|
|
186
186
|
if (needsUser && !userResolved) {
|
|
187
187
|
log.warn(`This operation requires an authenticated user (${matchedUserPattern}).`);
|
|
188
188
|
log.dim(' Your session may have expired, or you may not be logged in.');
|
|
189
|
-
log.dim(' You can log in with "dbo login" to avoid this prompt in the future.');
|
|
190
189
|
|
|
191
190
|
const stored = await loadUserInfo();
|
|
192
191
|
|
|
@@ -197,6 +196,7 @@ export async function checkSubmitErrors(result, context = {}) {
|
|
|
197
196
|
name: 'userChoice',
|
|
198
197
|
message: 'User ID Required:',
|
|
199
198
|
choices: [
|
|
199
|
+
{ name: 'Re-login now (recommended)', value: '_relogin' },
|
|
200
200
|
{ name: `Use session user (ID: ${stored.userId})`, value: stored.userId },
|
|
201
201
|
{ name: 'Enter a different User ID', value: '_custom' },
|
|
202
202
|
],
|
|
@@ -210,9 +210,19 @@ export async function checkSubmitErrors(result, context = {}) {
|
|
|
210
210
|
});
|
|
211
211
|
} else {
|
|
212
212
|
prompts.push({
|
|
213
|
-
type: '
|
|
214
|
-
name: '
|
|
213
|
+
type: 'list',
|
|
214
|
+
name: 'userChoice',
|
|
215
215
|
message: 'User ID Required:',
|
|
216
|
+
choices: [
|
|
217
|
+
{ name: 'Re-login now', value: '_relogin' },
|
|
218
|
+
{ name: 'Enter User ID manually', value: '_custom' },
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
prompts.push({
|
|
222
|
+
type: 'input',
|
|
223
|
+
name: 'customUserValue',
|
|
224
|
+
message: 'User ID:',
|
|
225
|
+
when: (answers) => answers.userChoice === '_custom',
|
|
216
226
|
validate: v => v.trim() ? true : 'User ID is required',
|
|
217
227
|
});
|
|
218
228
|
}
|
|
@@ -233,12 +243,29 @@ export async function checkSubmitErrors(result, context = {}) {
|
|
|
233
243
|
|
|
234
244
|
if (answers.ticketId) retryParams['_OverrideTicketID'] = answers.ticketId.trim();
|
|
235
245
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
246
|
+
if (answers.userChoice === '_relogin') {
|
|
247
|
+
// Run login flow inline, then use the stored user ID
|
|
248
|
+
try {
|
|
249
|
+
const config = await loadConfig();
|
|
250
|
+
const { performLogin } = await import('../commands/login.js');
|
|
251
|
+
await performLogin(config.domain);
|
|
252
|
+
const refreshed = await loadUserInfo();
|
|
253
|
+
if (refreshed.userId) {
|
|
254
|
+
retryParams['_OverrideUserID'] = refreshed.userId;
|
|
255
|
+
_sessionUserOverride = refreshed.userId;
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
log.error(`Re-login failed: ${err.message}`);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
const userValue = answers.userValue
|
|
263
|
+
|| (answers.userChoice === '_custom' ? answers.customUserValue : answers.userChoice);
|
|
264
|
+
if (userValue) {
|
|
265
|
+
const resolved = userValue.trim();
|
|
266
|
+
retryParams['_OverrideUserID'] = resolved;
|
|
267
|
+
_sessionUserOverride = resolved;
|
|
268
|
+
}
|
|
242
269
|
}
|
|
243
270
|
|
|
244
271
|
return retryParams;
|