@push.rocks/smartconfig 6.0.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/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.appdata.d.ts +72 -0
- package/dist_ts/classes.appdata.js +454 -0
- package/dist_ts/classes.keyvaluestore.d.ts +63 -0
- package/dist_ts/classes.keyvaluestore.js +169 -0
- package/dist_ts/classes.smartconfig.d.ts +29 -0
- package/dist_ts/classes.smartconfig.js +67 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -0
- package/dist_ts/paths.d.ts +8 -0
- package/dist_ts/paths.js +15 -0
- package/dist_ts/plugins.d.ts +12 -0
- package/dist_ts/plugins.js +13 -0
- package/package.json +73 -0
- package/readme.hints.md +0 -0
- package/readme.md +517 -0
- package/readme.plan.md +225 -0
- package/smartconfig.json +42 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.appdata.ts +548 -0
- package/ts/classes.keyvaluestore.ts +222 -0
- package/ts/classes.smartconfig.ts +79 -0
- package/ts/index.ts +3 -0
- package/ts/paths.ts +22 -0
- package/ts/plugins.ts +25 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { KeyValueStore } from './classes.keyvaluestore.js';
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Singleton Qenv Provider
|
|
6
|
+
// ============================================================================
|
|
7
|
+
let sharedQenv: plugins.qenv.Qenv | undefined;
|
|
8
|
+
|
|
9
|
+
function getQenv(): plugins.qenv.Qenv {
|
|
10
|
+
if (!sharedQenv) {
|
|
11
|
+
sharedQenv = new plugins.qenv.Qenv(
|
|
12
|
+
process.cwd(),
|
|
13
|
+
plugins.path.join(process.cwd(), '.nogit')
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return sharedQenv;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Security - Redaction for sensitive data
|
|
21
|
+
// ============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Redacts sensitive values in logs to prevent exposure of secrets
|
|
24
|
+
*/
|
|
25
|
+
function redactSensitiveValue(key: string, value: unknown): string {
|
|
26
|
+
// List of patterns that indicate sensitive data
|
|
27
|
+
const sensitivePatterns = [
|
|
28
|
+
/secret/i, /token/i, /key/i, /password/i, /pass/i,
|
|
29
|
+
/api/i, /credential/i, /auth/i, /private/i, /jwt/i,
|
|
30
|
+
/cert/i, /signature/i, /bearer/i
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Check if key contains sensitive pattern
|
|
34
|
+
const isSensitive = sensitivePatterns.some(pattern => pattern.test(key));
|
|
35
|
+
|
|
36
|
+
if (isSensitive) {
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
// Show first 3 chars and length for debugging
|
|
39
|
+
return value.length > 3
|
|
40
|
+
? `${value.substring(0, 3)}...[${value.length} chars]`
|
|
41
|
+
: '[redacted]';
|
|
42
|
+
}
|
|
43
|
+
return '[redacted]';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if value looks like a JWT token or base64 secret
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
// JWT tokens start with eyJ
|
|
49
|
+
if (value.startsWith('eyJ')) {
|
|
50
|
+
return `eyJ...[${value.length} chars]`;
|
|
51
|
+
}
|
|
52
|
+
// Very long strings might be encoded secrets
|
|
53
|
+
if (value.length > 100) {
|
|
54
|
+
return `${value.substring(0, 50)}...[${value.length} chars total]`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return JSON.stringify(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Type Converters - Centralized conversion logic
|
|
63
|
+
// ============================================================================
|
|
64
|
+
function toBoolean(value: unknown): boolean {
|
|
65
|
+
// If already boolean, return as-is
|
|
66
|
+
if (typeof value === 'boolean') {
|
|
67
|
+
console.log(` 🔹 toBoolean: value is already boolean: ${value}`);
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle null/undefined
|
|
72
|
+
if (value == null) {
|
|
73
|
+
console.log(` 🔹 toBoolean: value is null/undefined, returning false`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle string representations
|
|
78
|
+
const s = String(value).toLowerCase().trim();
|
|
79
|
+
|
|
80
|
+
// True values: "true", "1", "yes", "y", "on"
|
|
81
|
+
if (['true', '1', 'yes', 'y', 'on'].includes(s)) {
|
|
82
|
+
console.log(` 🔹 toBoolean: converting "${value}" to true`);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// False values: "false", "0", "no", "n", "off"
|
|
87
|
+
if (['false', '0', 'no', 'n', 'off'].includes(s)) {
|
|
88
|
+
console.log(` 🔹 toBoolean: converting "${value}" to false`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default: non-empty string = true, empty = false
|
|
93
|
+
const result = s.length > 0;
|
|
94
|
+
console.log(` 🔹 toBoolean: defaulting "${value}" to ${result}`);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toJson<T = any>(value: unknown): T | undefined {
|
|
99
|
+
if (value == null) return undefined;
|
|
100
|
+
if (typeof value === 'string') {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(value);
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return value as T;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function fromBase64(value: unknown): string | undefined {
|
|
111
|
+
if (value == null) return undefined;
|
|
112
|
+
try {
|
|
113
|
+
return Buffer.from(String(value), 'base64').toString('utf8');
|
|
114
|
+
} catch {
|
|
115
|
+
return String(value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toNumber(value: unknown): number | undefined {
|
|
120
|
+
if (value == null) return undefined;
|
|
121
|
+
const num = Number(value);
|
|
122
|
+
return Number.isNaN(num) ? undefined : num;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toString(value: unknown): string | undefined {
|
|
126
|
+
if (value == null) return undefined;
|
|
127
|
+
return String(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Declarative Pipeline Architecture
|
|
132
|
+
// ============================================================================
|
|
133
|
+
type Transform = 'boolean' | 'json' | 'base64' | 'number';
|
|
134
|
+
|
|
135
|
+
type MappingSpec = {
|
|
136
|
+
source:
|
|
137
|
+
| { type: 'env'; key: string }
|
|
138
|
+
| { type: 'hard'; value: string };
|
|
139
|
+
transforms: Transform[];
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Transform registry for extensibility
|
|
143
|
+
const transformRegistry: Record<string, (v: unknown) => unknown> = {
|
|
144
|
+
boolean: toBoolean,
|
|
145
|
+
json: toJson,
|
|
146
|
+
base64: fromBase64,
|
|
147
|
+
number: toNumber,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse a mapping string into a declarative spec
|
|
152
|
+
*/
|
|
153
|
+
function parseMappingSpec(input: string): MappingSpec {
|
|
154
|
+
const transforms: Transform[] = [];
|
|
155
|
+
let remaining = input;
|
|
156
|
+
|
|
157
|
+
// Check for hardcoded prefixes with type conversion
|
|
158
|
+
if (remaining.startsWith('hard_boolean:')) {
|
|
159
|
+
return {
|
|
160
|
+
source: { type: 'hard', value: remaining.slice(13) },
|
|
161
|
+
transforms: ['boolean']
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (remaining.startsWith('hard_json:')) {
|
|
166
|
+
return {
|
|
167
|
+
source: { type: 'hard', value: remaining.slice(10) },
|
|
168
|
+
transforms: ['json']
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (remaining.startsWith('hard_base64:')) {
|
|
173
|
+
return {
|
|
174
|
+
source: { type: 'hard', value: remaining.slice(12) },
|
|
175
|
+
transforms: ['base64']
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for generic hard: prefix
|
|
180
|
+
if (remaining.startsWith('hard:')) {
|
|
181
|
+
remaining = remaining.slice(5);
|
|
182
|
+
// Check for legacy suffixes on hardcoded values
|
|
183
|
+
if (remaining.endsWith('_JSON')) {
|
|
184
|
+
transforms.push('json');
|
|
185
|
+
remaining = remaining.slice(0, -5);
|
|
186
|
+
} else if (remaining.endsWith('_BASE64')) {
|
|
187
|
+
transforms.push('base64');
|
|
188
|
+
remaining = remaining.slice(0, -7);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
source: { type: 'hard', value: remaining },
|
|
192
|
+
transforms
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check for env var prefixes
|
|
197
|
+
if (remaining.startsWith('boolean:')) {
|
|
198
|
+
transforms.push('boolean');
|
|
199
|
+
remaining = remaining.slice(8);
|
|
200
|
+
} else if (remaining.startsWith('json:')) {
|
|
201
|
+
transforms.push('json');
|
|
202
|
+
remaining = remaining.slice(5);
|
|
203
|
+
} else if (remaining.startsWith('base64:')) {
|
|
204
|
+
transforms.push('base64');
|
|
205
|
+
remaining = remaining.slice(7);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check for legacy suffixes on env vars
|
|
209
|
+
if (remaining.endsWith('_JSON')) {
|
|
210
|
+
transforms.push('json');
|
|
211
|
+
remaining = remaining.slice(0, -5);
|
|
212
|
+
} else if (remaining.endsWith('_BASE64')) {
|
|
213
|
+
transforms.push('base64');
|
|
214
|
+
remaining = remaining.slice(0, -7);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
source: { type: 'env', key: remaining },
|
|
219
|
+
transforms
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolve the source value (env var or hardcoded)
|
|
225
|
+
*/
|
|
226
|
+
async function resolveSource(source: MappingSpec['source']): Promise<unknown> {
|
|
227
|
+
if (source.type === 'hard') {
|
|
228
|
+
return source.value;
|
|
229
|
+
}
|
|
230
|
+
// source.type === 'env'
|
|
231
|
+
// Workaround for Qenv bug where empty strings are treated as undefined
|
|
232
|
+
// Check process.env directly first to preserve empty strings
|
|
233
|
+
if (Object.prototype.hasOwnProperty.call(process.env, source.key)) {
|
|
234
|
+
return process.env[source.key];
|
|
235
|
+
}
|
|
236
|
+
// Fall back to Qenv for other sources (env.json, docker secrets, etc.)
|
|
237
|
+
return await getQenv().getEnvVarOnDemand(source.key);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Apply transformations in sequence
|
|
242
|
+
*/
|
|
243
|
+
function applyTransforms(value: unknown, transforms: Transform[]): unknown {
|
|
244
|
+
return transforms.reduce((acc, transform) => {
|
|
245
|
+
const fn = transformRegistry[transform];
|
|
246
|
+
return fn ? fn(acc) : acc;
|
|
247
|
+
}, value);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Process a mapping value through the complete pipeline
|
|
252
|
+
*/
|
|
253
|
+
async function processMappingValue(mappingString: string): Promise<unknown> {
|
|
254
|
+
const spec = parseMappingSpec(mappingString);
|
|
255
|
+
const keyName = spec.source.type === 'env' ? spec.source.key : 'hardcoded';
|
|
256
|
+
|
|
257
|
+
console.log(` 🔍 Processing mapping: "${mappingString}"`);
|
|
258
|
+
console.log(` Source: ${spec.source.type === 'env' ? `env:${spec.source.key}` : `hard:${spec.source.value}`}`);
|
|
259
|
+
console.log(` Transforms: ${spec.transforms.length > 0 ? spec.transforms.join(', ') : 'none'}`);
|
|
260
|
+
|
|
261
|
+
const rawValue = await resolveSource(spec.source);
|
|
262
|
+
console.log(` Raw value: ${redactSensitiveValue(keyName, rawValue)} (type: ${typeof rawValue})`);
|
|
263
|
+
|
|
264
|
+
if (rawValue === undefined || rawValue === null) {
|
|
265
|
+
console.log(` ⚠️ Raw value is undefined/null, returning undefined`);
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const result = applyTransforms(rawValue, spec.transforms);
|
|
270
|
+
console.log(` Final value: ${redactSensitiveValue(keyName, result)} (type: ${typeof result})`);
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Recursively evaluate mapping values (strings or nested objects)
|
|
276
|
+
*/
|
|
277
|
+
async function evaluateMappingValue(mappingValue: any): Promise<any> {
|
|
278
|
+
// Handle null explicitly - it should return null, not be treated as object
|
|
279
|
+
if (mappingValue === null) {
|
|
280
|
+
console.log(` 📌 Value is null, returning null`);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Handle strings (mapping specs)
|
|
285
|
+
if (typeof mappingValue === 'string') {
|
|
286
|
+
return processMappingValue(mappingValue);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Handle objects (but not arrays or null)
|
|
290
|
+
if (mappingValue && typeof mappingValue === 'object' && !Array.isArray(mappingValue)) {
|
|
291
|
+
console.log(` 📂 Processing nested object with ${Object.keys(mappingValue).length} keys`);
|
|
292
|
+
const result: any = {};
|
|
293
|
+
for (const [key, value] of Object.entries(mappingValue)) {
|
|
294
|
+
console.log(` → Processing nested key "${key}"`);
|
|
295
|
+
const evaluated = await evaluateMappingValue(value);
|
|
296
|
+
// Important: Don't filter out false or other falsy values!
|
|
297
|
+
// Only skip if explicitly undefined
|
|
298
|
+
if (evaluated !== undefined) {
|
|
299
|
+
result[key] = evaluated;
|
|
300
|
+
console.log(` ✓ Nested key "${key}" = ${redactSensitiveValue(key, evaluated)} (type: ${typeof evaluated})`);
|
|
301
|
+
} else {
|
|
302
|
+
console.log(` ⚠️ Nested key "${key}" evaluated to undefined, skipping`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// For any other type (numbers, booleans, etc.), return as-is
|
|
309
|
+
// Note: We don't have key context here, so we'll just indicate the type
|
|
310
|
+
console.log(` 📎 Returning value as-is: [value] (type: ${typeof mappingValue})`);
|
|
311
|
+
return mappingValue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================================================================
|
|
315
|
+
// AppData Interface and Class
|
|
316
|
+
// ============================================================================
|
|
317
|
+
export interface IAppDataOptions<T = any> {
|
|
318
|
+
dirPath?: string;
|
|
319
|
+
requiredKeys?: Array<keyof T>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Whether keys should be persisted on disk or not
|
|
323
|
+
*/
|
|
324
|
+
ephemeral?: boolean;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @deprecated Use 'ephemeral' instead
|
|
328
|
+
*/
|
|
329
|
+
ephermal?: boolean;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* kvStoreKey: 'MY_ENV_VAR'
|
|
333
|
+
*/
|
|
334
|
+
envMapping?: plugins.tsclass.typeFest.PartialDeep<T>;
|
|
335
|
+
overwriteObject?: plugins.tsclass.typeFest.PartialDeep<T>;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export class AppData<T = any> {
|
|
339
|
+
/**
|
|
340
|
+
* creates appdata. If no pathArg is given, data will be stored here:
|
|
341
|
+
* ${PWD}/.nogit/appdata
|
|
342
|
+
* @param pathArg
|
|
343
|
+
* @returns
|
|
344
|
+
*/
|
|
345
|
+
public static async createAndInit<T = any>(
|
|
346
|
+
optionsArg: IAppDataOptions<T> = {},
|
|
347
|
+
): Promise<AppData<T>> {
|
|
348
|
+
const appData = new AppData<T>(optionsArg);
|
|
349
|
+
await appData.readyDeferred.promise;
|
|
350
|
+
return appData;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Static helper to get an environment variable as a boolean
|
|
355
|
+
* @param envVarName The name of the environment variable
|
|
356
|
+
* @returns boolean value (true if env var is "true", false otherwise)
|
|
357
|
+
*/
|
|
358
|
+
public static async valueAsBoolean(envVarName: string): Promise<boolean> {
|
|
359
|
+
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
360
|
+
return toBoolean(value);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Static helper to get an environment variable as parsed JSON
|
|
365
|
+
* @param envVarName The name of the environment variable
|
|
366
|
+
* @returns Parsed JSON object/array
|
|
367
|
+
*/
|
|
368
|
+
public static async valueAsJson<R = any>(envVarName: string): Promise<R | undefined> {
|
|
369
|
+
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
370
|
+
return toJson<R>(value);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Static helper to get an environment variable as base64 decoded string
|
|
375
|
+
* @param envVarName The name of the environment variable
|
|
376
|
+
* @returns Decoded string
|
|
377
|
+
*/
|
|
378
|
+
public static async valueAsBase64(envVarName: string): Promise<string | undefined> {
|
|
379
|
+
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
380
|
+
return fromBase64(value);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Static helper to get an environment variable as a string
|
|
385
|
+
* @param envVarName The name of the environment variable
|
|
386
|
+
* @returns String value
|
|
387
|
+
*/
|
|
388
|
+
public static async valueAsString(envVarName: string): Promise<string | undefined> {
|
|
389
|
+
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
390
|
+
return toString(value);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Static helper to get an environment variable as a number
|
|
395
|
+
* @param envVarName The name of the environment variable
|
|
396
|
+
* @returns Number value
|
|
397
|
+
*/
|
|
398
|
+
public static async valueAsNumber(envVarName: string): Promise<number | undefined> {
|
|
399
|
+
const value = await getQenv().getEnvVarOnDemand(envVarName);
|
|
400
|
+
return toNumber(value);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// instance
|
|
404
|
+
public readyDeferred = plugins.smartpromise.defer<void>();
|
|
405
|
+
public options: IAppDataOptions<T>;
|
|
406
|
+
private kvStore: KeyValueStore<T>;
|
|
407
|
+
|
|
408
|
+
constructor(optionsArg: IAppDataOptions<T> = {}) {
|
|
409
|
+
this.options = optionsArg;
|
|
410
|
+
this.init();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* inits app data
|
|
415
|
+
*/
|
|
416
|
+
private async init() {
|
|
417
|
+
console.log('🚀 Initializing AppData...');
|
|
418
|
+
|
|
419
|
+
// Handle backward compatibility for typo
|
|
420
|
+
const isEphemeral = this.options.ephemeral ?? this.options.ephermal ?? false;
|
|
421
|
+
if (this.options.ephermal && !this.options.ephemeral) {
|
|
422
|
+
console.warn('⚠️ Option "ephermal" is deprecated, use "ephemeral" instead.');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (this.options.dirPath) {
|
|
426
|
+
console.log(` 📁 Using custom directory: ${this.options.dirPath}`);
|
|
427
|
+
} else if (isEphemeral) {
|
|
428
|
+
console.log(` 💨 Using ephemeral storage (in-memory only)`);
|
|
429
|
+
} else {
|
|
430
|
+
const appDataDir = '/app/data';
|
|
431
|
+
const dataDir = '/data';
|
|
432
|
+
const nogitAppData = '.nogit/appdata';
|
|
433
|
+
const appDataExists = plugins.smartfile.fs.isDirectory(appDataDir);
|
|
434
|
+
const dataExists = plugins.smartfile.fs.isDirectory(dataDir);
|
|
435
|
+
if (appDataExists) {
|
|
436
|
+
this.options.dirPath = appDataDir;
|
|
437
|
+
console.log(` 📁 Auto-selected container directory: ${appDataDir}`);
|
|
438
|
+
} else if (dataExists) {
|
|
439
|
+
this.options.dirPath = dataDir;
|
|
440
|
+
console.log(` 📁 Auto-selected data directory: ${dataDir}`);
|
|
441
|
+
} else {
|
|
442
|
+
await plugins.smartfile.fs.ensureDir(nogitAppData);
|
|
443
|
+
this.options.dirPath = nogitAppData;
|
|
444
|
+
console.log(` 📁 Auto-selected local directory: ${nogitAppData}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.kvStore = new KeyValueStore<T>({
|
|
449
|
+
typeArg: isEphemeral ? 'ephemeral' : 'custom',
|
|
450
|
+
identityArg: 'appkv',
|
|
451
|
+
customPath: this.options.dirPath,
|
|
452
|
+
mandatoryKeys: this.options.requiredKeys as Array<keyof T>,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
if (this.options.envMapping) {
|
|
456
|
+
console.log(`📦 Processing envMapping for AppData...`);
|
|
457
|
+
const totalKeys = Object.keys(this.options.envMapping).length;
|
|
458
|
+
let processedCount = 0;
|
|
459
|
+
|
|
460
|
+
// Process each top-level key in envMapping
|
|
461
|
+
for (const key in this.options.envMapping) {
|
|
462
|
+
try {
|
|
463
|
+
const mappingSpec = this.options.envMapping[key];
|
|
464
|
+
const specType = mappingSpec === null ? 'null' :
|
|
465
|
+
typeof mappingSpec === 'string' ? mappingSpec :
|
|
466
|
+
typeof mappingSpec === 'object' ? 'nested object' :
|
|
467
|
+
typeof mappingSpec;
|
|
468
|
+
console.log(` → Processing key "${key}" with spec: ${specType}`);
|
|
469
|
+
|
|
470
|
+
const evaluated = await evaluateMappingValue(mappingSpec);
|
|
471
|
+
// Important: Don't skip false, 0, empty string, or null values!
|
|
472
|
+
// Only skip if explicitly undefined
|
|
473
|
+
if (evaluated !== undefined) {
|
|
474
|
+
await this.kvStore.writeKey(key as keyof T, evaluated);
|
|
475
|
+
processedCount++;
|
|
476
|
+
const valueType = evaluated === null ? 'null' :
|
|
477
|
+
Array.isArray(evaluated) ? 'array' :
|
|
478
|
+
typeof evaluated;
|
|
479
|
+
const valuePreview = evaluated === null ? 'null' :
|
|
480
|
+
typeof evaluated === 'object' ?
|
|
481
|
+
(Array.isArray(evaluated) ? `[${evaluated.length} items]` : `{${Object.keys(evaluated).length} keys}`) :
|
|
482
|
+
redactSensitiveValue(key, evaluated);
|
|
483
|
+
console.log(` ✅ Successfully processed key "${key}" = ${valuePreview} (type: ${valueType})`);
|
|
484
|
+
} else {
|
|
485
|
+
console.log(` ⚠️ Key "${key}" evaluated to undefined, skipping`);
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.error(` ❌ Failed to evaluate envMapping for key "${key}":`, err);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
console.log(`📊 EnvMapping complete: ${processedCount}/${totalKeys} keys successfully processed`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Apply overwrite object after env mapping
|
|
496
|
+
if (this.options.overwriteObject) {
|
|
497
|
+
const overwriteKeys = Object.keys(this.options.overwriteObject);
|
|
498
|
+
console.log(`🔄 Applying overwriteObject with ${overwriteKeys.length} key(s)...`);
|
|
499
|
+
|
|
500
|
+
for (const key of overwriteKeys) {
|
|
501
|
+
const value = this.options.overwriteObject[key];
|
|
502
|
+
const valueType = Array.isArray(value) ? 'array' : typeof value;
|
|
503
|
+
console.log(` 🔧 Overwriting key "${key}" with ${valueType} value`);
|
|
504
|
+
|
|
505
|
+
await this.kvStore.writeKey(
|
|
506
|
+
key as keyof T,
|
|
507
|
+
value,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
console.log(`✅ OverwriteObject complete: ${overwriteKeys.length} key(s) overwritten`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.readyDeferred.resolve();
|
|
515
|
+
console.log('✨ AppData initialization complete!');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* returns a kvstore that resides in appdata
|
|
520
|
+
*/
|
|
521
|
+
public async getKvStore(): Promise<KeyValueStore<T>> {
|
|
522
|
+
await this.readyDeferred.promise;
|
|
523
|
+
return this.kvStore;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
public async logMissingKeys(): Promise<Array<keyof T>> {
|
|
527
|
+
const kvStore = await this.getKvStore();
|
|
528
|
+
const missingMandatoryKeys = await kvStore.getMissingMandatoryKeys();
|
|
529
|
+
if (missingMandatoryKeys.length > 0) {
|
|
530
|
+
console.log(
|
|
531
|
+
`The following mandatory keys are missing in the appdata:\n -> ${missingMandatoryKeys.join(
|
|
532
|
+
',\n -> ',
|
|
533
|
+
)}`,
|
|
534
|
+
);
|
|
535
|
+
} else {
|
|
536
|
+
console.log('All mandatory keys are present in the appdata');
|
|
537
|
+
}
|
|
538
|
+
return missingMandatoryKeys;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
public async waitForAndGetKey<K extends keyof T>(
|
|
542
|
+
keyArg: K,
|
|
543
|
+
): Promise<T[K] | undefined> {
|
|
544
|
+
await this.readyDeferred.promise;
|
|
545
|
+
await this.kvStore.waitForKeysPresent([keyArg]);
|
|
546
|
+
return this.kvStore.readKey(keyArg);
|
|
547
|
+
}
|
|
548
|
+
}
|