@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,161 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { loadConfig, getActiveCookiesPath, getCookiesPath } from './config.js';
|
|
3
|
+
import { loadCookies, buildCookieHeader, parseSetCookieHeaders, saveCookies, mergeCookies } from './cookie-jar.js';
|
|
4
|
+
import { log } from './logger.js';
|
|
5
|
+
|
|
6
|
+
export class DboClient {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.domainOverride = options.domain || null;
|
|
9
|
+
this.verbose = options.verbose || false;
|
|
10
|
+
this._config = null;
|
|
11
|
+
this._cookiesPath = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getConfig() {
|
|
15
|
+
if (!this._config) this._config = await loadConfig();
|
|
16
|
+
return this._config;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getDomain() {
|
|
20
|
+
if (this.domainOverride) return this.domainOverride;
|
|
21
|
+
const config = await this.getConfig();
|
|
22
|
+
if (!config.domain) {
|
|
23
|
+
throw new Error('No domain configured. Run "dbo init" first.');
|
|
24
|
+
}
|
|
25
|
+
return config.domain;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async getBaseUrl() {
|
|
29
|
+
const domain = await this.getDomain();
|
|
30
|
+
return `https://${domain}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getCookiesPath() {
|
|
34
|
+
if (!this._cookiesPath) this._cookiesPath = await getActiveCookiesPath();
|
|
35
|
+
return this._cookiesPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async request(path, options = {}) {
|
|
39
|
+
const baseUrl = await this.getBaseUrl();
|
|
40
|
+
let url = `${baseUrl}${path}`;
|
|
41
|
+
const cookiesPath = await this.getCookiesPath();
|
|
42
|
+
let cookies = await loadCookies(cookiesPath);
|
|
43
|
+
|
|
44
|
+
const maxRedirects = 5;
|
|
45
|
+
for (let attempt = 0; attempt <= maxRedirects; attempt++) {
|
|
46
|
+
const headers = { ...options.headers };
|
|
47
|
+
const cookieHeader = buildCookieHeader(cookies, url);
|
|
48
|
+
if (cookieHeader) headers['Cookie'] = cookieHeader;
|
|
49
|
+
|
|
50
|
+
if (this.verbose) {
|
|
51
|
+
log.verbose(`${options.method || 'GET'} ${url}`);
|
|
52
|
+
if (options.body && typeof options.body === 'string') {
|
|
53
|
+
log.verbose(`Body: ${options.body.substring(0, 200)}${options.body.length > 200 ? '...' : ''}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = await fetch(url, { ...options, headers, redirect: 'manual' });
|
|
58
|
+
|
|
59
|
+
// Persist any Set-Cookie headers
|
|
60
|
+
const newCookies = parseSetCookieHeaders(response.headers, url);
|
|
61
|
+
if (newCookies.length > 0) {
|
|
62
|
+
cookies = mergeCookies(cookies, newCookies);
|
|
63
|
+
const savePath = getCookiesPath().dbo;
|
|
64
|
+
await saveCookies(savePath, cookies);
|
|
65
|
+
this._cookiesPath = savePath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle redirects manually to preserve cookies across domains
|
|
69
|
+
if (response.status >= 300 && response.status < 400) {
|
|
70
|
+
const location = response.headers.get('location');
|
|
71
|
+
if (!location) return response;
|
|
72
|
+
// Resolve relative redirects
|
|
73
|
+
url = location.startsWith('http') ? location : new URL(location, url).toString();
|
|
74
|
+
if (this.verbose) {
|
|
75
|
+
log.verbose(`Redirect ${response.status} → ${url}`);
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Attach the final URL to the response for display purposes
|
|
81
|
+
response._finalUrl = url;
|
|
82
|
+
return response;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw new Error(`Too many redirects (>${maxRedirects})`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async get(path, params = {}) {
|
|
89
|
+
const query = new URLSearchParams();
|
|
90
|
+
for (const [key, values] of Object.entries(params)) {
|
|
91
|
+
const arr = Array.isArray(values) ? values : [values];
|
|
92
|
+
for (const v of arr) {
|
|
93
|
+
if (v !== undefined && v !== null) query.append(key, v);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const qs = query.toString();
|
|
97
|
+
const fullPath = qs ? `${path}?${qs}` : path;
|
|
98
|
+
const response = await this.request(fullPath);
|
|
99
|
+
return this._parseResponse(response);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async postUrlEncoded(path, data) {
|
|
103
|
+
const body = typeof data === 'string' ? data : new URLSearchParams(data).toString();
|
|
104
|
+
const response = await this.request(path, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
107
|
+
body,
|
|
108
|
+
});
|
|
109
|
+
return this._parseResponse(response);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async postMultipart(path, fields, files) {
|
|
113
|
+
const formData = new FormData();
|
|
114
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
115
|
+
formData.append(key, value);
|
|
116
|
+
}
|
|
117
|
+
for (const { fieldName, filePath, fileName } of files) {
|
|
118
|
+
const buffer = await readFile(filePath);
|
|
119
|
+
const blob = new Blob([buffer]);
|
|
120
|
+
formData.append(fieldName, blob, fileName);
|
|
121
|
+
}
|
|
122
|
+
const response = await this.request(path, { method: 'POST', body: formData });
|
|
123
|
+
return this._parseResponse(response);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fetch a URL and return the raw response as a Buffer (for binary downloads).
|
|
128
|
+
*/
|
|
129
|
+
async getBuffer(path) {
|
|
130
|
+
const response = await this.request(path);
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new Error(`Download failed: ${response.status} for ${path}`);
|
|
133
|
+
}
|
|
134
|
+
return Buffer.from(await response.arrayBuffer());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async _parseResponse(response) {
|
|
138
|
+
const text = await response.text();
|
|
139
|
+
let data;
|
|
140
|
+
try {
|
|
141
|
+
data = JSON.parse(text);
|
|
142
|
+
} catch {
|
|
143
|
+
data = { raw: text };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// DBO API responses come in two shapes:
|
|
147
|
+
// 1. Standard envelope: { Successful: true, Messages: [], Payload: ... }
|
|
148
|
+
// 2. Raw data (json_raw format): array of rows or object with _id keys
|
|
149
|
+
const isEnvelope = data !== null && typeof data === 'object' && 'Successful' in data;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
url: response._finalUrl || response.url,
|
|
153
|
+
status: response.status,
|
|
154
|
+
ok: response.ok,
|
|
155
|
+
data,
|
|
156
|
+
successful: isEnvelope ? data.Successful === true : response.ok,
|
|
157
|
+
messages: isEnvelope ? (data.Messages || []) : [],
|
|
158
|
+
payload: isEnvelope ? (data.Payload || null) : data,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared column filtering for DBO.io input submissions.
|
|
3
|
+
*
|
|
4
|
+
* These columns are server-managed or session-provided and must never
|
|
5
|
+
* be included in push/add payloads.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Columns that are read-only / auto-generated — never submitted
|
|
9
|
+
const SKIP_COLUMNS = new Set([
|
|
10
|
+
'_id',
|
|
11
|
+
'_CreatedOn',
|
|
12
|
+
'_LastUpdated',
|
|
13
|
+
'_LastUpdatedUserID',
|
|
14
|
+
'_LastUpdatedTicketID',
|
|
15
|
+
'_entity',
|
|
16
|
+
'_contentColumns',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if the given metadata key should be excluded from input submissions.
|
|
21
|
+
* Skips all SKIP_COLUMNS plus any key starting with _ (except UID).
|
|
22
|
+
*/
|
|
23
|
+
export function shouldSkipColumn(key) {
|
|
24
|
+
if (SKIP_COLUMNS.has(key)) return true;
|
|
25
|
+
// Skip system columns starting with _ except UID
|
|
26
|
+
if (key.startsWith('_') && key !== 'UID') return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { SKIP_COLUMNS };
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, access } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { log } from './logger.js';
|
|
4
|
+
|
|
5
|
+
const DBO_DIR = '.dbo';
|
|
6
|
+
const CONFIG_FILE = 'config.json';
|
|
7
|
+
const CREDENTIALS_FILE = 'credentials.json';
|
|
8
|
+
const COOKIES_FILE = 'cookies.txt';
|
|
9
|
+
|
|
10
|
+
function dboDir() {
|
|
11
|
+
return join(process.cwd(), DBO_DIR);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function configPath() {
|
|
15
|
+
return join(dboDir(), CONFIG_FILE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function credentialsPath() {
|
|
19
|
+
return join(dboDir(), CREDENTIALS_FILE);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cookiesPath() {
|
|
23
|
+
return join(dboDir(), COOKIES_FILE);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function exists(path) {
|
|
27
|
+
try {
|
|
28
|
+
await access(path);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function isInitialized() {
|
|
36
|
+
return exists(dboDir());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function hasLegacyConfig() {
|
|
40
|
+
return exists(join(process.cwd(), '.domain'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function readLegacyConfig() {
|
|
44
|
+
const cwd = process.cwd();
|
|
45
|
+
const config = {};
|
|
46
|
+
try {
|
|
47
|
+
config.domain = (await readFile(join(cwd, '.domain'), 'utf8')).trim();
|
|
48
|
+
} catch { /* no legacy domain */ }
|
|
49
|
+
try {
|
|
50
|
+
config.username = (await readFile(join(cwd, '.username'), 'utf8')).trim();
|
|
51
|
+
} catch { /* no legacy username */ }
|
|
52
|
+
try {
|
|
53
|
+
config.password = (await readFile(join(cwd, '.password'), 'utf8')).trim();
|
|
54
|
+
} catch { /* no legacy password */ }
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function initConfig(domain) {
|
|
59
|
+
await mkdir(dboDir(), { recursive: true });
|
|
60
|
+
await writeFile(configPath(), JSON.stringify({ domain }, null, 2) + '\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function saveCredentials(username) {
|
|
64
|
+
await mkdir(dboDir(), { recursive: true });
|
|
65
|
+
// Preserve existing fields (like userUid) — never store password
|
|
66
|
+
let existing = {};
|
|
67
|
+
try {
|
|
68
|
+
existing = JSON.parse(await readFile(credentialsPath(), 'utf8'));
|
|
69
|
+
} catch { /* no existing file */ }
|
|
70
|
+
existing.username = username;
|
|
71
|
+
delete existing.password; // Remove password if present from older versions
|
|
72
|
+
await writeFile(credentialsPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function saveUserInfo({ userId, userUid }) {
|
|
76
|
+
await mkdir(dboDir(), { recursive: true });
|
|
77
|
+
let existing = {};
|
|
78
|
+
try {
|
|
79
|
+
existing = JSON.parse(await readFile(credentialsPath(), 'utf8'));
|
|
80
|
+
} catch { /* no existing file */ }
|
|
81
|
+
if (userId !== undefined) existing.userId = userId;
|
|
82
|
+
if (userUid !== undefined) existing.userUid = userUid;
|
|
83
|
+
await writeFile(credentialsPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function loadUserInfo() {
|
|
87
|
+
try {
|
|
88
|
+
const raw = await readFile(credentialsPath(), 'utf8');
|
|
89
|
+
const creds = JSON.parse(raw);
|
|
90
|
+
return {
|
|
91
|
+
userId: creds.userId || null,
|
|
92
|
+
userUid: creds.userUid || null,
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return { userId: null, userUid: null };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function loadConfig() {
|
|
100
|
+
const config = { domain: null, username: null, password: null, ServerTimezone: 'UTC' };
|
|
101
|
+
|
|
102
|
+
// Try .dbo/config.json
|
|
103
|
+
try {
|
|
104
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
105
|
+
Object.assign(config, JSON.parse(raw));
|
|
106
|
+
} catch {
|
|
107
|
+
// Try legacy .domain file
|
|
108
|
+
try {
|
|
109
|
+
config.domain = (await readFile(join(process.cwd(), '.domain'), 'utf8')).trim();
|
|
110
|
+
} catch { /* no config found */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Try .dbo/credentials.json (username only — password is never stored)
|
|
114
|
+
try {
|
|
115
|
+
const raw = await readFile(credentialsPath(), 'utf8');
|
|
116
|
+
const creds = JSON.parse(raw);
|
|
117
|
+
config.username = creds.username;
|
|
118
|
+
} catch {
|
|
119
|
+
// Try legacy .username file
|
|
120
|
+
try {
|
|
121
|
+
config.username = (await readFile(join(process.cwd(), '.username'), 'utf8')).trim();
|
|
122
|
+
} catch { /* no legacy username */ }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Environment variables override (DBO_PASSWORD supported for CI/scripts)
|
|
126
|
+
if (process.env.DBO_DOMAIN) config.domain = process.env.DBO_DOMAIN;
|
|
127
|
+
if (process.env.DBO_USERNAME) config.username = process.env.DBO_USERNAME;
|
|
128
|
+
if (process.env.DBO_PASSWORD) config.password = process.env.DBO_PASSWORD;
|
|
129
|
+
|
|
130
|
+
return config;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getCookiesPath() {
|
|
134
|
+
// Prefer .dbo/cookies.txt, fall back to legacy .cookies
|
|
135
|
+
const dboCookies = join(dboDir(), COOKIES_FILE);
|
|
136
|
+
const legacyCookies = join(process.cwd(), '.cookies');
|
|
137
|
+
return { dbo: dboCookies, legacy: legacyCookies };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function getActiveCookiesPath() {
|
|
141
|
+
const paths = getCookiesPath();
|
|
142
|
+
if (await exists(paths.dbo)) return paths.dbo;
|
|
143
|
+
if (await exists(paths.legacy)) return paths.legacy;
|
|
144
|
+
return paths.dbo; // default for new sessions
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Merge app metadata into .dbo/config.json.
|
|
149
|
+
* Preserves existing fields; only sets new ones.
|
|
150
|
+
*/
|
|
151
|
+
export async function updateConfigWithApp({ AppID, AppUID, AppName, AppShortName, ServerTimezone }) {
|
|
152
|
+
await mkdir(dboDir(), { recursive: true });
|
|
153
|
+
let existing = {};
|
|
154
|
+
try {
|
|
155
|
+
existing = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
156
|
+
} catch { /* no existing config */ }
|
|
157
|
+
if (AppID !== undefined) existing.AppID = AppID;
|
|
158
|
+
if (AppUID !== undefined) existing.AppUID = AppUID;
|
|
159
|
+
if (AppName !== undefined) existing.AppName = AppName;
|
|
160
|
+
if (AppShortName !== undefined) existing.AppShortName = AppShortName;
|
|
161
|
+
if (ServerTimezone !== undefined) existing.ServerTimezone = ServerTimezone;
|
|
162
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Load app-related fields from .dbo/config.json.
|
|
167
|
+
*/
|
|
168
|
+
export async function loadAppConfig() {
|
|
169
|
+
try {
|
|
170
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
171
|
+
const config = JSON.parse(raw);
|
|
172
|
+
return {
|
|
173
|
+
AppID: config.AppID || null,
|
|
174
|
+
AppUID: config.AppUID || null,
|
|
175
|
+
AppName: config.AppName || null,
|
|
176
|
+
AppShortName: config.AppShortName || null,
|
|
177
|
+
};
|
|
178
|
+
} catch {
|
|
179
|
+
return { AppID: null, AppUID: null, AppName: null, AppShortName: null };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Save clone placement preferences to .dbo/config.json.
|
|
185
|
+
* mediaPlacement: 'fullpath' | 'bin' | 'ask'
|
|
186
|
+
* contentPlacement: 'path' | 'bin' | 'ask'
|
|
187
|
+
*/
|
|
188
|
+
export async function saveClonePlacement({ mediaPlacement, contentPlacement }) {
|
|
189
|
+
await mkdir(dboDir(), { recursive: true });
|
|
190
|
+
let existing = {};
|
|
191
|
+
try {
|
|
192
|
+
existing = JSON.parse(await readFile(configPath(), 'utf8'));
|
|
193
|
+
} catch { /* no existing config */ }
|
|
194
|
+
if (mediaPlacement !== undefined) existing.MediaPlacement = mediaPlacement;
|
|
195
|
+
if (contentPlacement !== undefined) existing.ContentPlacement = contentPlacement;
|
|
196
|
+
await writeFile(configPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Load clone placement preferences from .dbo/config.json.
|
|
201
|
+
*/
|
|
202
|
+
export async function loadClonePlacement() {
|
|
203
|
+
try {
|
|
204
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
205
|
+
const config = JSON.parse(raw);
|
|
206
|
+
return {
|
|
207
|
+
mediaPlacement: config.MediaPlacement || null,
|
|
208
|
+
contentPlacement: config.ContentPlacement || null,
|
|
209
|
+
};
|
|
210
|
+
} catch {
|
|
211
|
+
return { mediaPlacement: null, contentPlacement: null };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Save user profile fields (FirstName, LastName, Email) into credentials.json.
|
|
217
|
+
*/
|
|
218
|
+
export async function saveUserProfile({ FirstName, LastName, Email }) {
|
|
219
|
+
await mkdir(dboDir(), { recursive: true });
|
|
220
|
+
let existing = {};
|
|
221
|
+
try {
|
|
222
|
+
existing = JSON.parse(await readFile(credentialsPath(), 'utf8'));
|
|
223
|
+
} catch { /* no existing file */ }
|
|
224
|
+
if (FirstName !== undefined) existing.FirstName = FirstName;
|
|
225
|
+
if (LastName !== undefined) existing.LastName = LastName;
|
|
226
|
+
if (Email !== undefined) existing.Email = Email;
|
|
227
|
+
await writeFile(credentialsPath(), JSON.stringify(existing, null, 2) + '\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Load user profile from credentials.json.
|
|
232
|
+
*/
|
|
233
|
+
export async function loadUserProfile() {
|
|
234
|
+
try {
|
|
235
|
+
const raw = await readFile(credentialsPath(), 'utf8');
|
|
236
|
+
const creds = JSON.parse(raw);
|
|
237
|
+
return {
|
|
238
|
+
FirstName: creds.FirstName || null,
|
|
239
|
+
LastName: creds.LastName || null,
|
|
240
|
+
Email: creds.Email || null,
|
|
241
|
+
};
|
|
242
|
+
} catch {
|
|
243
|
+
return { FirstName: null, LastName: null, Email: null };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Ensure patterns are in .gitignore. Creates .gitignore if it doesn't exist.
|
|
249
|
+
*/
|
|
250
|
+
export async function ensureGitignore(patterns) {
|
|
251
|
+
const gitignorePath = join(process.cwd(), '.gitignore');
|
|
252
|
+
let content = '';
|
|
253
|
+
try {
|
|
254
|
+
content = await readFile(gitignorePath, 'utf8');
|
|
255
|
+
} catch { /* no .gitignore yet */ }
|
|
256
|
+
|
|
257
|
+
const toAdd = patterns.filter(p => !content.includes(p));
|
|
258
|
+
if (toAdd.length === 0) return;
|
|
259
|
+
|
|
260
|
+
const isNew = content.length === 0;
|
|
261
|
+
const needsNewline = !isNew && !content.endsWith('\n');
|
|
262
|
+
const hasHeader = content.includes('# DBO CLI');
|
|
263
|
+
const section = needsNewline ? '\n' : '';
|
|
264
|
+
const header = hasHeader ? '' : (isNew ? '# DBO CLI (sensitive files)\n' : '\n# DBO CLI (sensitive files)\n');
|
|
265
|
+
const addition = `${section}${header}${toAdd.join('\n')}\n`;
|
|
266
|
+
|
|
267
|
+
await writeFile(gitignorePath, content + addition);
|
|
268
|
+
for (const p of toAdd) log.dim(` Added ${p} to .gitignore`);
|
|
269
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read/write Netscape cookie format for curl compatibility.
|
|
6
|
+
* Format: domain\tinclude_subdomains\tpath\tsecure\texpiry\tname\tvalue
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export async function loadCookies(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
const text = await readFile(filePath, 'utf8');
|
|
12
|
+
const cookies = [];
|
|
13
|
+
for (const line of text.split('\n')) {
|
|
14
|
+
const trimmed = line.trim();
|
|
15
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
16
|
+
const parts = trimmed.split('\t');
|
|
17
|
+
if (parts.length >= 7) {
|
|
18
|
+
cookies.push({
|
|
19
|
+
domain: parts[0],
|
|
20
|
+
includeSubdomains: parts[1] === 'TRUE',
|
|
21
|
+
path: parts[2],
|
|
22
|
+
secure: parts[3] === 'TRUE',
|
|
23
|
+
expiry: parseInt(parts[4], 10),
|
|
24
|
+
name: parts[5],
|
|
25
|
+
value: parts[6],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return cookies;
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildCookieHeader(cookies, url) {
|
|
36
|
+
const urlObj = new URL(url);
|
|
37
|
+
const matching = cookies.filter(c => {
|
|
38
|
+
const domainMatch = urlObj.hostname === c.domain || urlObj.hostname.endsWith('.' + c.domain);
|
|
39
|
+
const pathMatch = urlObj.pathname.startsWith(c.path);
|
|
40
|
+
const secureMatch = !c.secure || urlObj.protocol === 'https:';
|
|
41
|
+
const notExpired = c.expiry === 0 || c.expiry > Math.floor(Date.now() / 1000);
|
|
42
|
+
return domainMatch && pathMatch && secureMatch && notExpired;
|
|
43
|
+
});
|
|
44
|
+
if (matching.length === 0) return null;
|
|
45
|
+
return matching.map(c => `${c.name}=${c.value}`).join('; ');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseSetCookieHeaders(headers, url) {
|
|
49
|
+
const urlObj = new URL(url);
|
|
50
|
+
const cookies = [];
|
|
51
|
+
const raw = headers.getSetCookie?.() || [];
|
|
52
|
+
for (const header of raw) {
|
|
53
|
+
const parts = header.split(';').map(s => s.trim());
|
|
54
|
+
const [nameValue, ...attrs] = parts;
|
|
55
|
+
const eqIdx = nameValue.indexOf('=');
|
|
56
|
+
if (eqIdx === -1) continue;
|
|
57
|
+
const name = nameValue.slice(0, eqIdx);
|
|
58
|
+
const value = nameValue.slice(eqIdx + 1);
|
|
59
|
+
const cookie = {
|
|
60
|
+
domain: urlObj.hostname,
|
|
61
|
+
includeSubdomains: false,
|
|
62
|
+
path: '/',
|
|
63
|
+
secure: false,
|
|
64
|
+
expiry: 0,
|
|
65
|
+
name,
|
|
66
|
+
value,
|
|
67
|
+
};
|
|
68
|
+
for (const attr of attrs) {
|
|
69
|
+
const [key, val] = attr.split('=').map(s => s.trim());
|
|
70
|
+
const lk = key.toLowerCase();
|
|
71
|
+
if (lk === 'domain') { cookie.domain = val.replace(/^\./, ''); cookie.includeSubdomains = true; }
|
|
72
|
+
else if (lk === 'path') cookie.path = val;
|
|
73
|
+
else if (lk === 'secure') cookie.secure = true;
|
|
74
|
+
else if (lk === 'expires') cookie.expiry = Math.floor(new Date(val).getTime() / 1000);
|
|
75
|
+
else if (lk === 'max-age') cookie.expiry = Math.floor(Date.now() / 1000) + parseInt(val, 10);
|
|
76
|
+
}
|
|
77
|
+
cookies.push(cookie);
|
|
78
|
+
}
|
|
79
|
+
return cookies;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function saveCookies(filePath, cookies) {
|
|
83
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
84
|
+
const lines = ['# Netscape HTTP Cookie File', '# Generated by dbo-cli', ''];
|
|
85
|
+
for (const c of cookies) {
|
|
86
|
+
lines.push([
|
|
87
|
+
c.domain,
|
|
88
|
+
c.includeSubdomains ? 'TRUE' : 'FALSE',
|
|
89
|
+
c.path,
|
|
90
|
+
c.secure ? 'TRUE' : 'FALSE',
|
|
91
|
+
c.expiry.toString(),
|
|
92
|
+
c.name,
|
|
93
|
+
c.value,
|
|
94
|
+
].join('\t'));
|
|
95
|
+
}
|
|
96
|
+
await writeFile(filePath, lines.join('\n') + '\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function mergeCookies(existing, incoming) {
|
|
100
|
+
const map = new Map();
|
|
101
|
+
for (const c of existing) map.set(`${c.domain}:${c.path}:${c.name}`, c);
|
|
102
|
+
for (const c of incoming) map.set(`${c.domain}:${c.path}:${c.name}`, c);
|
|
103
|
+
return [...map.values()];
|
|
104
|
+
}
|