@dboio/cli 0.4.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 +1161 -0
- package/bin/dbo.js +51 -0
- package/package.json +22 -0
- package/src/commands/add.js +374 -0
- package/src/commands/cache.js +49 -0
- package/src/commands/clone.js +742 -0
- package/src/commands/content.js +143 -0
- package/src/commands/deploy.js +89 -0
- package/src/commands/init.js +105 -0
- package/src/commands/input.js +111 -0
- package/src/commands/install.js +186 -0
- package/src/commands/instance.js +44 -0
- package/src/commands/login.js +97 -0
- package/src/commands/logout.js +22 -0
- package/src/commands/media.js +46 -0
- package/src/commands/message.js +28 -0
- package/src/commands/output.js +129 -0
- package/src/commands/pull.js +109 -0
- package/src/commands/push.js +309 -0
- package/src/commands/status.js +41 -0
- package/src/commands/update.js +168 -0
- package/src/commands/upload.js +37 -0
- package/src/lib/client.js +161 -0
- package/src/lib/columns.js +30 -0
- package/src/lib/config.js +269 -0
- package/src/lib/cookie-jar.js +104 -0
- package/src/lib/formatter.js +310 -0
- package/src/lib/input-parser.js +212 -0
- package/src/lib/logger.js +12 -0
- package/src/lib/save-to-disk.js +383 -0
- package/src/lib/structure.js +129 -0
- package/src/lib/timestamps.js +67 -0
- package/src/plugins/claudecommands/dbo.md +248 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { log } from './logger.js';
|
|
3
|
+
|
|
4
|
+
export function formatResponse(result, options = {}) {
|
|
5
|
+
// --jq mode: filter then print
|
|
6
|
+
if (options.jq) {
|
|
7
|
+
const filtered = jqFilter(result.data, options.jq);
|
|
8
|
+
printJson(filtered);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// --json mode: colorized full JSON
|
|
13
|
+
if (options.json) {
|
|
14
|
+
printJson(result.data);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (result.successful) {
|
|
19
|
+
log.success(`Request successful ${chalk.dim(result.url || '')}`);
|
|
20
|
+
} else {
|
|
21
|
+
log.error(`Request failed ${chalk.dim(result.url || '')}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (result.messages && result.messages.length > 0) {
|
|
25
|
+
for (const msg of result.messages) {
|
|
26
|
+
log.label('Message', msg);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (result.payload) {
|
|
31
|
+
formatPayload(result.payload, options);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatPayload(payload, options = {}) {
|
|
36
|
+
if (!payload) return;
|
|
37
|
+
|
|
38
|
+
// Input API results
|
|
39
|
+
if (payload.Results) {
|
|
40
|
+
const r = payload.Results;
|
|
41
|
+
if (r.Add && r.Add.length > 0) {
|
|
42
|
+
log.info(`Added ${r.Add.length} record(s)`);
|
|
43
|
+
for (const rec of r.Add) {
|
|
44
|
+
const uid = rec.UID || rec.uid || '';
|
|
45
|
+
const id = rec.RowID || rec.rowid || rec.ID || '';
|
|
46
|
+
log.label(' UID', uid);
|
|
47
|
+
if (id) log.label(' RowID', id.toString());
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (r.Edit && r.Edit.length > 0) {
|
|
51
|
+
log.info(`Edited ${r.Edit.length} record(s)`);
|
|
52
|
+
}
|
|
53
|
+
if (r.Delete && r.Delete.length > 0) {
|
|
54
|
+
log.info(`Deleted ${r.Delete.length} record(s)`);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Output/query results — show row count or raw data
|
|
60
|
+
if (Array.isArray(payload)) {
|
|
61
|
+
log.info(`${payload.length} row(s) returned`);
|
|
62
|
+
if (payload.length > 0) {
|
|
63
|
+
formatTable(payload, options);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Generic object payload
|
|
69
|
+
if (typeof payload === 'object') {
|
|
70
|
+
printJson(payload);
|
|
71
|
+
} else {
|
|
72
|
+
log.plain(String(payload));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Sensitive column names — values get masked
|
|
77
|
+
const SENSITIVE_COLUMNS = new Set([
|
|
78
|
+
'password', 'passkey', 'secret', 'token', 'apikey', 'api_key',
|
|
79
|
+
'secureconnectionstring', 'connectionstring', 'customencryption',
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
function formatTable(rows, options = {}) {
|
|
83
|
+
if (rows.length === 0) return;
|
|
84
|
+
|
|
85
|
+
const termWidth = process.stdout.columns || 120;
|
|
86
|
+
let allColumns = Object.keys(rows[0]);
|
|
87
|
+
|
|
88
|
+
// --columns filter: only show specified columns
|
|
89
|
+
if (options.columns) {
|
|
90
|
+
const picked = options.columns.split(',').map(c => c.trim());
|
|
91
|
+
allColumns = allColumns.filter(c => picked.some(p =>
|
|
92
|
+
c.toLowerCase() === p.toLowerCase()
|
|
93
|
+
));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Hide columns where ALL values are null/empty (unless explicitly requested)
|
|
97
|
+
if (!options.columns) {
|
|
98
|
+
allColumns = allColumns.filter(col => {
|
|
99
|
+
return rows.some(r => r[col] !== null && r[col] !== undefined && r[col] !== '');
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (allColumns.length === 0) return;
|
|
104
|
+
|
|
105
|
+
// Calculate column widths
|
|
106
|
+
const widths = {};
|
|
107
|
+
for (const col of allColumns) {
|
|
108
|
+
const maxData = Math.max(...rows.map(r => displayValue(r[col], col).length));
|
|
109
|
+
widths[col] = Math.max(col.length, Math.min(maxData, 30));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Shrink columns to fit terminal width
|
|
113
|
+
const gap = 2;
|
|
114
|
+
let totalWidth = allColumns.reduce((sum, c) => sum + widths[c], 0) + (allColumns.length - 1) * gap;
|
|
115
|
+
|
|
116
|
+
if (totalWidth > termWidth && allColumns.length > 1) {
|
|
117
|
+
// First pass: shrink any column wider than 20 chars
|
|
118
|
+
while (totalWidth > termWidth) {
|
|
119
|
+
const widest = allColumns.reduce((a, b) => widths[a] > widths[b] ? a : b);
|
|
120
|
+
if (widths[widest] <= 6) break; // can't shrink further
|
|
121
|
+
widths[widest] = Math.max(6, widths[widest] - 1);
|
|
122
|
+
totalWidth = allColumns.reduce((sum, c) => sum + widths[c], 0) + (allColumns.length - 1) * gap;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Header
|
|
127
|
+
const header = allColumns.map(c => c.substring(0, widths[c]).padEnd(widths[c])).join(' ');
|
|
128
|
+
log.plain(chalk.cyan(header));
|
|
129
|
+
log.plain(chalk.dim('─'.repeat(Math.min(header.length, termWidth))));
|
|
130
|
+
|
|
131
|
+
// Rows
|
|
132
|
+
for (const row of rows) {
|
|
133
|
+
const line = allColumns.map(c => {
|
|
134
|
+
const val = displayValue(row[c], c);
|
|
135
|
+
return val.substring(0, widths[c]).padEnd(widths[c]);
|
|
136
|
+
}).join(' ');
|
|
137
|
+
log.plain(line);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function displayValue(value, columnName) {
|
|
142
|
+
if (value === null || value === undefined) return '';
|
|
143
|
+
|
|
144
|
+
// Mask sensitive columns
|
|
145
|
+
if (SENSITIVE_COLUMNS.has(columnName.toLowerCase())) {
|
|
146
|
+
const str = String(value);
|
|
147
|
+
if (str.length === 0) return '';
|
|
148
|
+
return str.substring(0, 3) + '***';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const str = String(value);
|
|
152
|
+
|
|
153
|
+
// Truncate very long values (base64, blobs, etc.)
|
|
154
|
+
if (str.length > 60) {
|
|
155
|
+
return str.substring(0, 57) + '...';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return str;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Lightweight jq-style JSON filter.
|
|
163
|
+
*
|
|
164
|
+
* Supported expressions:
|
|
165
|
+
* . identity
|
|
166
|
+
* .key access key
|
|
167
|
+
* .key.nested nested access
|
|
168
|
+
* .key[].field iterate array, pluck field
|
|
169
|
+
* .[0] array index
|
|
170
|
+
* .[] iterate array
|
|
171
|
+
* .[] | .field pipe: iterate then access
|
|
172
|
+
* .["key.with.dots"] bracket notation
|
|
173
|
+
*/
|
|
174
|
+
export function jqFilter(data, expr) {
|
|
175
|
+
if (!expr || expr.trim() === '.') return data;
|
|
176
|
+
|
|
177
|
+
// Handle pipe: .[] | .field
|
|
178
|
+
if (expr.includes(' | ')) {
|
|
179
|
+
const [left, right] = expr.split(' | ', 2);
|
|
180
|
+
const intermediate = jqFilter(data, left.trim());
|
|
181
|
+
if (Array.isArray(intermediate)) {
|
|
182
|
+
return intermediate.map(item => jqFilter(item, right.trim()));
|
|
183
|
+
}
|
|
184
|
+
return jqFilter(intermediate, right.trim());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let current = data;
|
|
188
|
+
// Tokenize the expression: split by . but respect brackets
|
|
189
|
+
const tokens = tokenize(expr);
|
|
190
|
+
|
|
191
|
+
for (const token of tokens) {
|
|
192
|
+
if (current === undefined || current === null) return null;
|
|
193
|
+
|
|
194
|
+
// Array iteration: []
|
|
195
|
+
if (token === '[]') {
|
|
196
|
+
if (!Array.isArray(current)) return null;
|
|
197
|
+
// If there are more tokens after [], we need to map
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Array iteration with remaining path: [].field
|
|
202
|
+
if (token.startsWith('[]')) {
|
|
203
|
+
if (!Array.isArray(current)) return null;
|
|
204
|
+
const remaining = token.substring(2);
|
|
205
|
+
current = current.map(item => jqFilter(item, '.' + remaining));
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Array index: [0], [1], etc.
|
|
210
|
+
const indexMatch = token.match(/^\[(\d+)]$/);
|
|
211
|
+
if (indexMatch) {
|
|
212
|
+
const idx = parseInt(indexMatch[1], 10);
|
|
213
|
+
current = Array.isArray(current) ? current[idx] : null;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Bracket notation: ["key.name"]
|
|
218
|
+
const bracketMatch = token.match(/^\["(.+)"]$/);
|
|
219
|
+
if (bracketMatch) {
|
|
220
|
+
current = current[bracketMatch[1]];
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Plain key access
|
|
225
|
+
if (typeof current === 'object' && current !== null) {
|
|
226
|
+
current = current[token];
|
|
227
|
+
} else {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// If current is still an array and we had [], flatten single-field plucks
|
|
233
|
+
return current;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function tokenize(expr) {
|
|
237
|
+
const tokens = [];
|
|
238
|
+
let i = expr.startsWith('.') ? 1 : 0; // skip leading dot
|
|
239
|
+
|
|
240
|
+
while (i < expr.length) {
|
|
241
|
+
if (expr[i] === '.') {
|
|
242
|
+
i++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (expr[i] === '[') {
|
|
247
|
+
// Find matching ]
|
|
248
|
+
const end = expr.indexOf(']', i);
|
|
249
|
+
if (end === -1) break;
|
|
250
|
+
let token = expr.substring(i, end + 1);
|
|
251
|
+
// Check if there's more after ] (like [].field)
|
|
252
|
+
if (end + 1 < expr.length && expr[end + 1] === '.') {
|
|
253
|
+
// Collect remaining path segment for array iteration
|
|
254
|
+
const nextDot = expr.indexOf('.', end + 2);
|
|
255
|
+
const nextBracket = expr.indexOf('[', end + 2);
|
|
256
|
+
let segEnd;
|
|
257
|
+
if (nextDot === -1 && nextBracket === -1) segEnd = expr.length;
|
|
258
|
+
else if (nextDot === -1) segEnd = nextBracket;
|
|
259
|
+
else if (nextBracket === -1) segEnd = nextDot;
|
|
260
|
+
else segEnd = Math.min(nextDot, nextBracket);
|
|
261
|
+
token += expr.substring(end + 1, segEnd);
|
|
262
|
+
i = segEnd;
|
|
263
|
+
} else {
|
|
264
|
+
i = end + 1;
|
|
265
|
+
}
|
|
266
|
+
tokens.push(token);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Regular key: read until next . or [
|
|
271
|
+
let end = i;
|
|
272
|
+
while (end < expr.length && expr[end] !== '.' && expr[end] !== '[') end++;
|
|
273
|
+
tokens.push(expr.substring(i, end));
|
|
274
|
+
i = end;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return tokens;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Print JSON with syntax highlighting.
|
|
282
|
+
*/
|
|
283
|
+
function printJson(data) {
|
|
284
|
+
const json = JSON.stringify(data, null, 2);
|
|
285
|
+
if (!json) { log.plain('null'); return; }
|
|
286
|
+
|
|
287
|
+
const colored = json.replace(
|
|
288
|
+
/("(?:[^"\\]|\\.)*")\s*:/g, // keys
|
|
289
|
+
(match, key) => chalk.cyan(key) + ':'
|
|
290
|
+
).replace(
|
|
291
|
+
/:\s*("(?:[^"\\]|\\.)*")/g, // string values
|
|
292
|
+
(match, val) => ': ' + chalk.green(val)
|
|
293
|
+
).replace(
|
|
294
|
+
/:\s*(\d+\.?\d*)/g, // numbers
|
|
295
|
+
(match, num) => ': ' + chalk.yellow(num)
|
|
296
|
+
).replace(
|
|
297
|
+
/:\s*(true|false|null)/g, // booleans/null
|
|
298
|
+
(match, val) => ': ' + chalk.magenta(val)
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
log.plain(colored);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function formatError(err) {
|
|
305
|
+
if (err.message) {
|
|
306
|
+
log.error(err.message);
|
|
307
|
+
} else {
|
|
308
|
+
log.error(String(err));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { log } from './logger.js';
|
|
3
|
+
import { loadUserInfo } from './config.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse DBO input syntax and build form data.
|
|
7
|
+
*
|
|
8
|
+
* Syntax: RowID:add1;column:entity.Column=value
|
|
9
|
+
* RowUID:abc123;column:content.Content@filepath
|
|
10
|
+
* RowID:del123;entity:user=true
|
|
11
|
+
*
|
|
12
|
+
* The @filepath syntax means "read file contents and use as value" (URL-encoded, not multipart).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export async function buildInputBody(dataExpressions, extraParams = {}) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
|
|
18
|
+
// Add extra params first (_confirm, _OverrideTicketID, etc.)
|
|
19
|
+
for (const [key, value] of Object.entries(extraParams)) {
|
|
20
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const expr of dataExpressions) {
|
|
24
|
+
// Split by & to handle multiple ops in one -d string
|
|
25
|
+
const ops = expr.split('&');
|
|
26
|
+
for (const op of ops) {
|
|
27
|
+
const encoded = await encodeInputExpression(op.trim());
|
|
28
|
+
parts.push(encoded);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parts.join('&');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function encodeInputExpression(expr) {
|
|
36
|
+
// Check if value part contains @filepath
|
|
37
|
+
// Format: key=value or key@filepath
|
|
38
|
+
// The key is everything before the first = or @ that acts as value separator
|
|
39
|
+
// DBO syntax: RowID:add1;column:content.Content@path/to/file.css
|
|
40
|
+
// RowID:add1;column:user.Name=John
|
|
41
|
+
|
|
42
|
+
// Find the value separator: last @ or = in the expression
|
|
43
|
+
// But we need to be careful: the @ in column:entity.Column@file is the value separator,
|
|
44
|
+
// while : and ; are part of the DBO key syntax
|
|
45
|
+
|
|
46
|
+
// Strategy: find the position of the first = or the last @ that comes after a column reference
|
|
47
|
+
const eqIdx = expr.indexOf('=');
|
|
48
|
+
const atIdx = findValueAtSign(expr);
|
|
49
|
+
|
|
50
|
+
if (atIdx !== -1 && (eqIdx === -1 || atIdx < eqIdx)) {
|
|
51
|
+
// @filepath syntax: read file and URL-encode
|
|
52
|
+
const key = expr.substring(0, atIdx);
|
|
53
|
+
const filePath = expr.substring(atIdx + 1);
|
|
54
|
+
const content = await readFile(filePath, 'utf8');
|
|
55
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(content)}`;
|
|
56
|
+
} else if (eqIdx !== -1) {
|
|
57
|
+
// =value syntax: direct value
|
|
58
|
+
const key = expr.substring(0, eqIdx);
|
|
59
|
+
const value = expr.substring(eqIdx + 1);
|
|
60
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
61
|
+
} else {
|
|
62
|
+
// No value separator, encode the whole thing
|
|
63
|
+
return encodeURIComponent(expr);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find the @ sign that acts as a value separator (file reference),
|
|
69
|
+
* not the @ in email addresses or other contexts.
|
|
70
|
+
*
|
|
71
|
+
* In DBO syntax, the @ follows the pattern:
|
|
72
|
+
* column:entity.ColumnName@filepath
|
|
73
|
+
* So it comes after a dot-separated entity.column reference.
|
|
74
|
+
*/
|
|
75
|
+
function findValueAtSign(expr) {
|
|
76
|
+
// Look for @ that appears after column:entity.Column pattern
|
|
77
|
+
// The pattern is: ...column:word.Word@...
|
|
78
|
+
const match = expr.match(/;column:[a-zA-Z_]+\.[a-zA-Z_]+@/);
|
|
79
|
+
if (match) {
|
|
80
|
+
return expr.indexOf('@', match.index);
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Patterns that indicate a missing user identity in the server response
|
|
86
|
+
const USER_ID_PATTERNS = [
|
|
87
|
+
'LoggedInUser_UID',
|
|
88
|
+
'LoggedInUserID',
|
|
89
|
+
'CurrentUserID',
|
|
90
|
+
'CurrentUser_UID',
|
|
91
|
+
'UserID',
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a submission result contains recoverable errors that can be
|
|
96
|
+
* resolved by prompting the user for missing parameters.
|
|
97
|
+
*
|
|
98
|
+
* Detects:
|
|
99
|
+
* - ticket_lookup_required_error → prompts for Ticket ID
|
|
100
|
+
* - LoggedInUser_UID, LoggedInUserID, CurrentUserID, UserID not found
|
|
101
|
+
* → prompts for User ID or UID (session not authenticated)
|
|
102
|
+
*
|
|
103
|
+
* Returns an object of extra params to add on retry, or null if no
|
|
104
|
+
* recoverable errors were found.
|
|
105
|
+
*/
|
|
106
|
+
export async function checkSubmitErrors(result) {
|
|
107
|
+
const messages = result.messages || result.data?.Messages || [];
|
|
108
|
+
const allText = messages.filter(m => typeof m === 'string').join(' ');
|
|
109
|
+
|
|
110
|
+
const needsTicket = allText.includes('ticket_lookup_required_error');
|
|
111
|
+
|
|
112
|
+
// Detect which user identity variant is needed
|
|
113
|
+
const matchedUserPattern = USER_ID_PATTERNS.find(p => allText.includes(p));
|
|
114
|
+
const needsUser = !!matchedUserPattern;
|
|
115
|
+
const needsUserUid = matchedUserPattern && matchedUserPattern.includes('UID');
|
|
116
|
+
const needsUserId = matchedUserPattern && !needsUserUid;
|
|
117
|
+
|
|
118
|
+
if (!needsTicket && !needsUser) return null;
|
|
119
|
+
|
|
120
|
+
const prompts = [];
|
|
121
|
+
const retryParams = {};
|
|
122
|
+
|
|
123
|
+
if (needsUser) {
|
|
124
|
+
const idType = needsUserUid ? 'UID' : 'ID';
|
|
125
|
+
log.warn(`This operation requires an authenticated user (${matchedUserPattern}).`);
|
|
126
|
+
log.dim(' Your session may have expired, or you may not be logged in.');
|
|
127
|
+
log.dim(' You can log in with "dbo login" to avoid this prompt in the future.');
|
|
128
|
+
|
|
129
|
+
// Check for stored user info from a previous login
|
|
130
|
+
const stored = await loadUserInfo();
|
|
131
|
+
const storedValue = needsUserUid ? stored.userUid : (stored.userId || stored.userUid);
|
|
132
|
+
const storedLabel = needsUserUid
|
|
133
|
+
? (stored.userUid ? `UID: ${stored.userUid}` : null)
|
|
134
|
+
: (stored.userId ? `ID: ${stored.userId}` : (stored.userUid ? `UID: ${stored.userUid}` : null));
|
|
135
|
+
|
|
136
|
+
if (storedValue) {
|
|
137
|
+
log.dim(` Stored session user ${storedLabel}`);
|
|
138
|
+
prompts.push({
|
|
139
|
+
type: 'list',
|
|
140
|
+
name: 'userChoice',
|
|
141
|
+
message: `User ${idType} Required:`,
|
|
142
|
+
choices: [
|
|
143
|
+
{ name: `Use session user (${storedLabel})`, value: storedValue },
|
|
144
|
+
{ name: `Enter a different User ${idType}`, value: '_custom' },
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
prompts.push({
|
|
148
|
+
type: 'input',
|
|
149
|
+
name: 'customUserValue',
|
|
150
|
+
message: `Custom User ${idType}:`,
|
|
151
|
+
when: (answers) => answers.userChoice === '_custom',
|
|
152
|
+
validate: v => v.trim() ? true : `User ${idType} is required`,
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
prompts.push({
|
|
156
|
+
type: 'input',
|
|
157
|
+
name: 'userValue',
|
|
158
|
+
message: `User ${idType} Required:`,
|
|
159
|
+
validate: v => v.trim() ? true : `User ${idType} is required`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (needsTicket) {
|
|
165
|
+
log.warn('This operation requires a Ticket ID.');
|
|
166
|
+
prompts.push({
|
|
167
|
+
type: 'input',
|
|
168
|
+
name: 'ticketId',
|
|
169
|
+
message: 'Ticket ID Required:',
|
|
170
|
+
validate: v => v.trim() ? true : 'Ticket ID is required',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const inquirer = (await import('inquirer')).default;
|
|
175
|
+
const answers = await inquirer.prompt(prompts);
|
|
176
|
+
|
|
177
|
+
if (answers.ticketId) retryParams['_OverrideTicketID'] = answers.ticketId.trim();
|
|
178
|
+
|
|
179
|
+
// Resolve user identity from choice or direct input
|
|
180
|
+
const userValue = answers.userValue
|
|
181
|
+
|| (answers.userChoice === '_custom' ? answers.customUserValue : answers.userChoice);
|
|
182
|
+
if (userValue) {
|
|
183
|
+
// Use the appropriate override param based on what the server asked for
|
|
184
|
+
if (needsUserUid) {
|
|
185
|
+
retryParams['_OverrideUserUID'] = userValue.trim();
|
|
186
|
+
} else {
|
|
187
|
+
retryParams['_OverrideUserID'] = userValue.trim();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return retryParams;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Parse file arguments in the format: field=@path or just @path
|
|
196
|
+
* Returns objects suitable for multipart upload.
|
|
197
|
+
*/
|
|
198
|
+
export function parseFileArg(arg) {
|
|
199
|
+
if (arg.includes('=@')) {
|
|
200
|
+
const [field, path] = arg.split('=@', 2);
|
|
201
|
+
const fileName = path.split('/').pop();
|
|
202
|
+
return { fieldName: field, filePath: path, fileName };
|
|
203
|
+
}
|
|
204
|
+
if (arg.startsWith('@')) {
|
|
205
|
+
const path = arg.substring(1);
|
|
206
|
+
const fileName = path.split('/').pop();
|
|
207
|
+
return { fieldName: 'file', filePath: path, fileName };
|
|
208
|
+
}
|
|
209
|
+
// Treat as: file=@path
|
|
210
|
+
const fileName = arg.split('/').pop();
|
|
211
|
+
return { fieldName: 'file', filePath: arg, fileName };
|
|
212
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export const log = {
|
|
4
|
+
info(msg) { console.log(chalk.blue('ℹ'), msg); },
|
|
5
|
+
success(msg) { console.log(chalk.green('✓'), msg); },
|
|
6
|
+
warn(msg) { console.log(chalk.yellow('⚠'), msg); },
|
|
7
|
+
error(msg) { console.error(chalk.red('✗'), msg); },
|
|
8
|
+
dim(msg) { console.log(chalk.dim(msg)); },
|
|
9
|
+
plain(msg) { console.log(msg); },
|
|
10
|
+
verbose(msg) { console.log(chalk.dim(' →'), chalk.dim(msg)); },
|
|
11
|
+
label(label, value) { console.log(chalk.dim(` ${label}:`), value); },
|
|
12
|
+
};
|