@oussema_mili/test-pkg-123 1.1.22
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.
Potentially problematic release.
This version of @oussema_mili/test-pkg-123 might be problematic. Click here for more details.
- package/LICENSE +29 -0
- package/README.md +220 -0
- package/auth-callback.html +97 -0
- package/auth.js +276 -0
- package/cli-commands.js +1923 -0
- package/containerManager.js +304 -0
- package/daemon/agentRunner.js +429 -0
- package/daemon/daemonEntry.js +64 -0
- package/daemon/daemonManager.js +271 -0
- package/daemon/logManager.js +227 -0
- package/dist/styles.css +504 -0
- package/docker-actions/apps.js +3938 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +355 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +224 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/setup-tasks.js +859 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +696 -0
- package/helper-functions.js +193 -0
- package/index.html +83 -0
- package/index.js +341 -0
- package/package.json +82 -0
- package/postcss.config.mjs +5 -0
- package/scripts/release.sh +212 -0
- package/setup/setupWizard.js +403 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +171 -0
- package/store/daemonStore.js +217 -0
- package/store/deviceCredentialStore.js +107 -0
- package/store/npmTokenStore.js +65 -0
- package/store/registryStore.js +329 -0
- package/store/setupState.js +147 -0
- package/styles.css +1 -0
- package/utils/appLogger.js +223 -0
- package/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +44 -0
- package/utils/errorHandler.js +327 -0
- package/utils/portUtils.js +59 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/utils/ssl-certificates.js +256 -0
- package/websocket-server.js +415 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import dotenv from "dotenv";
|
|
6
|
+
dotenv.config();
|
|
7
|
+
|
|
8
|
+
const fsPromises = fs.promises;
|
|
9
|
+
|
|
10
|
+
const AGENT_ROOT_DIR = process.env.AGENT_ROOT_DIR || ".fenwave";
|
|
11
|
+
const REGISTRIES_DIR = process.env.REGISTRIES_DIR || "registries";
|
|
12
|
+
const REGISTRIES_FILE = process.env.REGISTRIES_FILE || "credentials.json";
|
|
13
|
+
const REGISTRY_FILE = path.join(
|
|
14
|
+
os.homedir(),
|
|
15
|
+
AGENT_ROOT_DIR,
|
|
16
|
+
REGISTRIES_DIR,
|
|
17
|
+
REGISTRIES_FILE
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const SCHEMA_VERSION = 1;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Registry Store for managing registry credentials
|
|
24
|
+
*/
|
|
25
|
+
class RegistryStore {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.registries = new Map();
|
|
28
|
+
this.initialized = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the store - load registries
|
|
33
|
+
*/
|
|
34
|
+
async initialize() {
|
|
35
|
+
if (this.initialized) return;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Ensure directory exists
|
|
39
|
+
await this.ensureDirectory();
|
|
40
|
+
|
|
41
|
+
// Load existing registries
|
|
42
|
+
await this.loadRegistries();
|
|
43
|
+
|
|
44
|
+
this.initialized = true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(
|
|
47
|
+
chalk.red("❌ Failed to initialize registry store:", error.message)
|
|
48
|
+
);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Ensure the .fenwave/registries directory exists
|
|
55
|
+
*/
|
|
56
|
+
async ensureDirectory() {
|
|
57
|
+
const dir = path.dirname(REGISTRY_FILE);
|
|
58
|
+
try {
|
|
59
|
+
await fsPromises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.code !== "EEXIST") {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Load registries from file
|
|
69
|
+
*/
|
|
70
|
+
async loadRegistries() {
|
|
71
|
+
try {
|
|
72
|
+
const data = await fsPromises.readFile(REGISTRY_FILE, "utf8");
|
|
73
|
+
const parsed = JSON.parse(data);
|
|
74
|
+
|
|
75
|
+
if (parsed.version !== SCHEMA_VERSION) {
|
|
76
|
+
console.warn(
|
|
77
|
+
`⚠️ Registry file version mismatch. Expected ${SCHEMA_VERSION}, got ${parsed.version}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Load registries with plain text credentials
|
|
82
|
+
let hasActiveRegistry = false;
|
|
83
|
+
|
|
84
|
+
for (const registry of parsed.registries || []) {
|
|
85
|
+
try {
|
|
86
|
+
// Ensure active field exists (for backward compatibility)
|
|
87
|
+
if (registry.active === undefined) {
|
|
88
|
+
registry.active = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (registry.active) {
|
|
92
|
+
hasActiveRegistry = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Store registry with credentials directly
|
|
96
|
+
this.registries.set(registry.id, registry);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(
|
|
99
|
+
chalk.red(`❌ Failed to load registry ${registry.id}:`),
|
|
100
|
+
error.message
|
|
101
|
+
);
|
|
102
|
+
// Skip this registry but continue with others
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If no registry is marked as active, make the first one active
|
|
107
|
+
if (!hasActiveRegistry && this.registries.size > 0) {
|
|
108
|
+
const firstRegistry = this.registries.values().next().value;
|
|
109
|
+
if (firstRegistry) {
|
|
110
|
+
firstRegistry.active = true;
|
|
111
|
+
firstRegistry.updatedAt = new Date().toISOString();
|
|
112
|
+
// Save the updated registries with the active flag
|
|
113
|
+
await this.saveRegistries();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error.code === "ENOENT") {
|
|
118
|
+
// File doesn't exist yet, start with empty registries
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Save registries to file with plain text credentials
|
|
127
|
+
*/
|
|
128
|
+
async saveRegistries() {
|
|
129
|
+
try {
|
|
130
|
+
const registriesToSave = [];
|
|
131
|
+
|
|
132
|
+
for (const [id, registry] of this.registries.entries()) {
|
|
133
|
+
// Save registry with credentials directly
|
|
134
|
+
registriesToSave.push(registry);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const data = {
|
|
138
|
+
version: SCHEMA_VERSION,
|
|
139
|
+
registries: registriesToSave,
|
|
140
|
+
updatedAt: new Date().toISOString(),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Write atomically
|
|
144
|
+
const tempFile = REGISTRY_FILE + ".tmp";
|
|
145
|
+
await fsPromises.writeFile(tempFile, JSON.stringify(data, null, 2), {
|
|
146
|
+
mode: 0o600,
|
|
147
|
+
});
|
|
148
|
+
await fsPromises.rename(tempFile, REGISTRY_FILE);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(chalk.red("❌ Failed to save registries:", error.message));
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Add or update a registry
|
|
157
|
+
*/
|
|
158
|
+
async upsertRegistry(registryData) {
|
|
159
|
+
await this.initialize();
|
|
160
|
+
|
|
161
|
+
const { id, credentials, ...metadata } = registryData;
|
|
162
|
+
|
|
163
|
+
const registry = {
|
|
164
|
+
id,
|
|
165
|
+
...metadata,
|
|
166
|
+
credentials,
|
|
167
|
+
updatedAt: new Date().toISOString(),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (!this.registries.has(id)) {
|
|
171
|
+
registry.createdAt = registry.updatedAt;
|
|
172
|
+
// If this is the first registry, make it active by default
|
|
173
|
+
if (this.registries.size === 0) {
|
|
174
|
+
registry.active = true;
|
|
175
|
+
} else {
|
|
176
|
+
registry.active = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.registries.set(id, registry);
|
|
181
|
+
await this.saveRegistries();
|
|
182
|
+
|
|
183
|
+
return registry;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get all registries (returns metadata only, no credentials)
|
|
188
|
+
*/
|
|
189
|
+
async getAllRegistries() {
|
|
190
|
+
await this.initialize();
|
|
191
|
+
|
|
192
|
+
const result = [];
|
|
193
|
+
for (const [id, registry] of this.registries.entries()) {
|
|
194
|
+
const { credentials, ...safeRegistry } = registry;
|
|
195
|
+
|
|
196
|
+
// Add type-specific safe metadata
|
|
197
|
+
const metadata = { ...safeRegistry };
|
|
198
|
+
if (registry.type === "ecr" && credentials) {
|
|
199
|
+
metadata.accessKeyId = credentials.accessKeyId;
|
|
200
|
+
metadata.region = registry.region;
|
|
201
|
+
} else if (registry.type === "gcr" && credentials?.serviceAccountJson) {
|
|
202
|
+
try {
|
|
203
|
+
const serviceAccount = JSON.parse(credentials.serviceAccountJson);
|
|
204
|
+
metadata.projectId = serviceAccount.project_id;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error("Error parsing service account JSON:", error);
|
|
207
|
+
}
|
|
208
|
+
} else if (credentials?.username) {
|
|
209
|
+
metadata.username = credentials.username;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Ensure active field is included (default to false if not set)
|
|
213
|
+
metadata.active = registry.active || false;
|
|
214
|
+
|
|
215
|
+
result.push(metadata);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get registry with credentials (for agent operations)
|
|
223
|
+
*/
|
|
224
|
+
async getRegistryWithCredentials(id) {
|
|
225
|
+
await this.initialize();
|
|
226
|
+
return this.registries.get(id);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Remove a registry
|
|
231
|
+
*/
|
|
232
|
+
async removeRegistry(id) {
|
|
233
|
+
await this.initialize();
|
|
234
|
+
|
|
235
|
+
const registryToRemove = this.registries.get(id);
|
|
236
|
+
const deleted = this.registries.delete(id);
|
|
237
|
+
|
|
238
|
+
if (deleted) {
|
|
239
|
+
// If we removed the active registry, set another one as active
|
|
240
|
+
if (
|
|
241
|
+
registryToRemove &&
|
|
242
|
+
registryToRemove.active &&
|
|
243
|
+
this.registries.size > 0
|
|
244
|
+
) {
|
|
245
|
+
// Set the first remaining registry as active
|
|
246
|
+
const firstRegistry = this.registries.values().next().value;
|
|
247
|
+
if (firstRegistry) {
|
|
248
|
+
firstRegistry.active = true;
|
|
249
|
+
firstRegistry.updatedAt = new Date().toISOString();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await this.saveRegistries();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return deleted;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get registry count
|
|
261
|
+
*/
|
|
262
|
+
async getRegistryCount() {
|
|
263
|
+
await this.initialize();
|
|
264
|
+
return this.registries.size;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Clear all registries (for logout/cleanup)
|
|
269
|
+
*/
|
|
270
|
+
async clearAll() {
|
|
271
|
+
await this.initialize();
|
|
272
|
+
this.registries.clear();
|
|
273
|
+
await this.saveRegistries();
|
|
274
|
+
console.log("🧹 All registries cleared");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Set active registry
|
|
279
|
+
*/
|
|
280
|
+
async setActiveRegistry(id) {
|
|
281
|
+
await this.initialize();
|
|
282
|
+
|
|
283
|
+
// First, set all registries to inactive
|
|
284
|
+
for (const [registryId, registry] of this.registries.entries()) {
|
|
285
|
+
registry.active = false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Then set the specified registry as active
|
|
289
|
+
if (id && this.registries.has(id)) {
|
|
290
|
+
const registry = this.registries.get(id);
|
|
291
|
+
registry.active = true;
|
|
292
|
+
registry.updatedAt = new Date().toISOString();
|
|
293
|
+
await this.saveRegistries();
|
|
294
|
+
return registry;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// If no valid id provided, save anyway to clear all active states
|
|
298
|
+
await this.saveRegistries();
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get active registry
|
|
304
|
+
*/
|
|
305
|
+
async getActiveRegistry() {
|
|
306
|
+
await this.initialize();
|
|
307
|
+
|
|
308
|
+
for (const [id, registry] of this.registries.entries()) {
|
|
309
|
+
if (registry.active) {
|
|
310
|
+
return registry;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get active registry ID
|
|
319
|
+
*/
|
|
320
|
+
async getActiveRegistryId() {
|
|
321
|
+
const activeRegistry = await this.getActiveRegistry();
|
|
322
|
+
return activeRegistry ? activeRegistry.id : null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Singleton instance
|
|
327
|
+
const registryStore = new RegistryStore();
|
|
328
|
+
|
|
329
|
+
export default registryStore;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const FENWAVE_DIR = path.join(os.homedir(), '.fenwave');
|
|
6
|
+
const SETUP_DIR = path.join(FENWAVE_DIR, 'setup');
|
|
7
|
+
const STATE_FILE = path.join(SETUP_DIR, 'state.json');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Setup steps enum
|
|
11
|
+
*/
|
|
12
|
+
const SetupStep = {
|
|
13
|
+
PREREQUISITES: 'prerequisites',
|
|
14
|
+
REGISTRATION: 'registration',
|
|
15
|
+
DOCKER_REGISTRY: 'docker_registry',
|
|
16
|
+
PULL_IMAGE: 'pull_image',
|
|
17
|
+
NPM_CONFIG: 'npm_config',
|
|
18
|
+
START_AGENT: 'start_agent',
|
|
19
|
+
COMPLETE: 'complete',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ensure setup directory exists
|
|
24
|
+
*/
|
|
25
|
+
function ensureSetupDir() {
|
|
26
|
+
if (!fs.existsSync(FENWAVE_DIR)) {
|
|
27
|
+
fs.mkdirSync(FENWAVE_DIR, { recursive: true, mode: 0o700 });
|
|
28
|
+
}
|
|
29
|
+
if (!fs.existsSync(SETUP_DIR)) {
|
|
30
|
+
fs.mkdirSync(SETUP_DIR, { recursive: true, mode: 0o700 });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Save setup progress
|
|
36
|
+
*
|
|
37
|
+
* @param {string} step - Current step
|
|
38
|
+
* @param {Object} data - Step data
|
|
39
|
+
*/
|
|
40
|
+
function saveSetupProgress(step, data = {}) {
|
|
41
|
+
ensureSetupDir();
|
|
42
|
+
|
|
43
|
+
const state = loadSetupProgress() || {
|
|
44
|
+
startedAt: new Date().toISOString(),
|
|
45
|
+
steps: {},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
state.steps[step] = {
|
|
49
|
+
completedAt: new Date().toISOString(),
|
|
50
|
+
data,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
state.currentStep = step;
|
|
54
|
+
state.updatedAt = new Date().toISOString();
|
|
55
|
+
|
|
56
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), {
|
|
57
|
+
mode: 0o600,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load setup progress
|
|
63
|
+
*
|
|
64
|
+
* @returns {Object|null} Setup state or null if not found
|
|
65
|
+
*/
|
|
66
|
+
function loadSetupProgress() {
|
|
67
|
+
if (!fs.existsSync(STATE_FILE)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Failed to load setup progress:', error.message);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mark step as complete
|
|
81
|
+
*
|
|
82
|
+
* @param {string} step - Step to mark as complete
|
|
83
|
+
*/
|
|
84
|
+
function markStepComplete(step) {
|
|
85
|
+
saveSetupProgress(step);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if step is complete
|
|
90
|
+
*
|
|
91
|
+
* @param {string} step - Step to check
|
|
92
|
+
* @returns {boolean} True if step is complete
|
|
93
|
+
*/
|
|
94
|
+
function isStepComplete(step) {
|
|
95
|
+
const state = loadSetupProgress();
|
|
96
|
+
return state?.steps?.[step] !== undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get current step
|
|
101
|
+
*
|
|
102
|
+
* @returns {string|null} Current step or null
|
|
103
|
+
*/
|
|
104
|
+
function getCurrentStep() {
|
|
105
|
+
const state = loadSetupProgress();
|
|
106
|
+
return state?.currentStep || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear setup state
|
|
111
|
+
*/
|
|
112
|
+
function clearSetupState() {
|
|
113
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
114
|
+
fs.unlinkSync(STATE_FILE);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if setup is in progress
|
|
120
|
+
*
|
|
121
|
+
* @returns {boolean} True if setup state exists
|
|
122
|
+
*/
|
|
123
|
+
function isSetupInProgress() {
|
|
124
|
+
return fs.existsSync(STATE_FILE);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get completed steps
|
|
129
|
+
*
|
|
130
|
+
* @returns {Array<string>} List of completed step names
|
|
131
|
+
*/
|
|
132
|
+
function getCompletedSteps() {
|
|
133
|
+
const state = loadSetupProgress();
|
|
134
|
+
return state?.steps ? Object.keys(state.steps) : [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
SetupStep,
|
|
139
|
+
saveSetupProgress,
|
|
140
|
+
loadSetupProgress,
|
|
141
|
+
markStepComplete,
|
|
142
|
+
isStepComplete,
|
|
143
|
+
getCurrentStep,
|
|
144
|
+
clearSetupState,
|
|
145
|
+
isSetupInProgress,
|
|
146
|
+
getCompletedSteps,
|
|
147
|
+
};
|
package/styles.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
const LOGS_DIR = path.join(os.homedir(), ".fenwave", "logs");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ensure logs directory exists
|
|
10
|
+
*/
|
|
11
|
+
function ensureLogsDir() {
|
|
12
|
+
if (!fs.existsSync(LOGS_DIR)) {
|
|
13
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true, mode: 0o700 });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a log filename for an app
|
|
19
|
+
* @param {string} appName - Application name
|
|
20
|
+
* @param {string} version - Application version
|
|
21
|
+
* @returns {string} Log filename
|
|
22
|
+
*/
|
|
23
|
+
function generateLogFilename(appName, version) {
|
|
24
|
+
const sanitizedName = appName.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
25
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
26
|
+
return `${sanitizedName}-${version || "latest"}-${timestamp}.log`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create an app logger instance
|
|
31
|
+
* @param {string} appName - Application name
|
|
32
|
+
* @param {string} version - Application version
|
|
33
|
+
* @returns {Object} Logger instance with methods and logPath
|
|
34
|
+
*/
|
|
35
|
+
export function createAppLogger(appName, version) {
|
|
36
|
+
ensureLogsDir();
|
|
37
|
+
|
|
38
|
+
const filename = generateLogFilename(appName, version);
|
|
39
|
+
const logPath = path.join(LOGS_DIR, filename);
|
|
40
|
+
|
|
41
|
+
// Create log file with header
|
|
42
|
+
const header = `
|
|
43
|
+
================================================================================
|
|
44
|
+
Fenwave App Startup Log
|
|
45
|
+
================================================================================
|
|
46
|
+
Application: ${appName}
|
|
47
|
+
Version: ${version || "latest"}
|
|
48
|
+
Started: ${new Date().toISOString()}
|
|
49
|
+
================================================================================
|
|
50
|
+
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
fs.writeFileSync(logPath, header, { mode: 0o600 });
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
logPath,
|
|
57
|
+
filename,
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Log an info message
|
|
61
|
+
* @param {string} message - Message to log
|
|
62
|
+
*/
|
|
63
|
+
info(message) {
|
|
64
|
+
const timestamp = new Date().toISOString();
|
|
65
|
+
const line = `[${timestamp}] [INFO] ${message}\n`;
|
|
66
|
+
fs.appendFileSync(logPath, line);
|
|
67
|
+
console.log(chalk.blue(`ℹ️ ${message}`));
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Log a success message
|
|
72
|
+
* @param {string} message - Message to log
|
|
73
|
+
*/
|
|
74
|
+
success(message) {
|
|
75
|
+
const timestamp = new Date().toISOString();
|
|
76
|
+
const line = `[${timestamp}] [SUCCESS] ${message}\n`;
|
|
77
|
+
fs.appendFileSync(logPath, line);
|
|
78
|
+
console.log(chalk.green(`✅ ${message}`));
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Log an error message
|
|
83
|
+
* @param {string} message - Message to log
|
|
84
|
+
*/
|
|
85
|
+
error(message) {
|
|
86
|
+
const timestamp = new Date().toISOString();
|
|
87
|
+
const line = `[${timestamp}] [ERROR] ${message}\n`;
|
|
88
|
+
fs.appendFileSync(logPath, line);
|
|
89
|
+
console.error(chalk.red(`❌ ${message}`));
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Log a warning message
|
|
94
|
+
* @param {string} message - Message to log
|
|
95
|
+
*/
|
|
96
|
+
warn(message) {
|
|
97
|
+
const timestamp = new Date().toISOString();
|
|
98
|
+
const line = `[${timestamp}] [WARN] ${message}\n`;
|
|
99
|
+
fs.appendFileSync(logPath, line);
|
|
100
|
+
console.warn(chalk.yellow(`⚠️ ${message}`));
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Log docker output (stdout/stderr)
|
|
105
|
+
* @param {string} output - Docker output
|
|
106
|
+
* @param {string} type - Output type ('stdout' or 'stderr')
|
|
107
|
+
*/
|
|
108
|
+
docker(output, type = "stdout") {
|
|
109
|
+
if (!output || !output.trim()) return;
|
|
110
|
+
|
|
111
|
+
const timestamp = new Date().toISOString();
|
|
112
|
+
const prefix = type === "stderr" ? "[DOCKER STDERR]" : "[DOCKER]";
|
|
113
|
+
const lines = output.split("\n").filter(line => line.trim());
|
|
114
|
+
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
fs.appendFileSync(logPath, `[${timestamp}] ${prefix} ${line}\n`);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Log completion status
|
|
122
|
+
* @param {boolean} success - Whether the operation succeeded
|
|
123
|
+
* @param {string} message - Completion message
|
|
124
|
+
*/
|
|
125
|
+
complete(success, message) {
|
|
126
|
+
const timestamp = new Date().toISOString();
|
|
127
|
+
const status = success ? "COMPLETED SUCCESSFULLY" : "FAILED";
|
|
128
|
+
const footer = `
|
|
129
|
+
================================================================================
|
|
130
|
+
${status}
|
|
131
|
+
${message}
|
|
132
|
+
Finished: ${timestamp}
|
|
133
|
+
================================================================================
|
|
134
|
+
`;
|
|
135
|
+
fs.appendFileSync(logPath, footer);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get recent logs for an app
|
|
142
|
+
* @param {string} appName - Application name (optional, filters by app)
|
|
143
|
+
* @param {number} limit - Number of logs to return
|
|
144
|
+
* @returns {Array} Array of log file info
|
|
145
|
+
*/
|
|
146
|
+
export function getRecentLogs(appName = null, limit = 20) {
|
|
147
|
+
ensureLogsDir();
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
let files = fs.readdirSync(LOGS_DIR)
|
|
151
|
+
.filter(f => f.endsWith(".log"))
|
|
152
|
+
.map(f => {
|
|
153
|
+
const filePath = path.join(LOGS_DIR, f);
|
|
154
|
+
const stats = fs.statSync(filePath);
|
|
155
|
+
return {
|
|
156
|
+
filename: f,
|
|
157
|
+
path: filePath,
|
|
158
|
+
createdAt: stats.birthtime,
|
|
159
|
+
size: stats.size,
|
|
160
|
+
};
|
|
161
|
+
})
|
|
162
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
163
|
+
|
|
164
|
+
if (appName) {
|
|
165
|
+
const sanitizedName = appName.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
166
|
+
files = files.filter(f => f.filename.startsWith(sanitizedName));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return files.slice(0, limit);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error("Error reading logs directory:", error);
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Read a log file content
|
|
178
|
+
* @param {string} filename - Log filename
|
|
179
|
+
* @returns {string|null} Log content or null if not found
|
|
180
|
+
*/
|
|
181
|
+
export function readLogFile(filename) {
|
|
182
|
+
const logPath = path.join(LOGS_DIR, filename);
|
|
183
|
+
|
|
184
|
+
if (!fs.existsSync(logPath)) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
return fs.readFileSync(logPath, "utf8");
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error("Error reading log file:", error);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Clean up old logs (keep last N days)
|
|
198
|
+
* @param {number} daysToKeep - Number of days to keep logs
|
|
199
|
+
*/
|
|
200
|
+
export function cleanupOldLogs(daysToKeep = 7) {
|
|
201
|
+
ensureLogsDir();
|
|
202
|
+
|
|
203
|
+
const cutoffDate = new Date();
|
|
204
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const files = fs.readdirSync(LOGS_DIR);
|
|
208
|
+
|
|
209
|
+
for (const file of files) {
|
|
210
|
+
const filePath = path.join(LOGS_DIR, file);
|
|
211
|
+
const stats = fs.statSync(filePath);
|
|
212
|
+
|
|
213
|
+
if (stats.birthtime < cutoffDate) {
|
|
214
|
+
fs.unlinkSync(filePath);
|
|
215
|
+
console.log(chalk.gray(`🧹 Cleaned up old log: ${file}`));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error("Error cleaning up logs:", error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export { LOGS_DIR };
|