@fenwave/agent 1.1.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.
@@ -0,0 +1,174 @@
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 CONFIG_DIR = path.join(FENWAVE_DIR, 'config');
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'agent.json');
8
+
9
+ /**
10
+ * Default configuration values
11
+ */
12
+ const DEFAULT_CONFIG = {
13
+ // Backstage URLs
14
+ backendUrl: 'http://localhost:7007',
15
+ frontendUrl: 'http://localhost:3000',
16
+ appBuilderUrl: null, // Will default to backendUrl if not set
17
+
18
+ // Container Configuration
19
+ containerName: 'fenwave-devapp',
20
+ containerPort: 3003,
21
+ wsPort: 3001,
22
+
23
+ // Directory Configuration
24
+ agentRootDir: '.fenwave',
25
+ registriesDir: 'registries',
26
+ containerDataDir: '/data',
27
+
28
+ // AWS/ECR Configuration
29
+ awsRegion: 'eu-west-1',
30
+ awsAccountId: null, // Set during registration from Backstage
31
+ dockerImage: null, // Will be computed if awsAccountId is set
32
+
33
+ // Other Settings
34
+ authTimeoutMs: 60000,
35
+ };
36
+
37
+ /**
38
+ * Ensure config directory exists
39
+ */
40
+ function ensureConfigDir() {
41
+ if (!fs.existsSync(FENWAVE_DIR)) {
42
+ fs.mkdirSync(FENWAVE_DIR, { recursive: true, mode: 0o700 });
43
+ }
44
+ if (!fs.existsSync(CONFIG_DIR)) {
45
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Load configuration from disk
51
+ * @returns {Object} Configuration object with defaults applied
52
+ */
53
+ export function loadConfig() {
54
+ let config = { ...DEFAULT_CONFIG };
55
+
56
+ // Load from file if exists
57
+ if (fs.existsSync(CONFIG_FILE)) {
58
+ try {
59
+ const fileConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
60
+ config = { ...config, ...fileConfig };
61
+ } catch (error) {
62
+ console.error('Failed to load agent config:', error.message);
63
+ }
64
+ }
65
+
66
+ // Apply computed defaults
67
+ if (!config.appBuilderUrl) {
68
+ config.appBuilderUrl = config.backendUrl;
69
+ }
70
+
71
+ // Only compute dockerImage if awsAccountId is configured
72
+ if (!config.dockerImage && config.awsAccountId && config.awsRegion) {
73
+ config.dockerImage = `${config.awsAccountId}.dkr.ecr.${config.awsRegion}.amazonaws.com/fenwave/devapp:latest`;
74
+ }
75
+
76
+ // Fall back to environment variables if config values are not set
77
+ config.backendUrl = config.backendUrl || process.env.BACKEND_URL || DEFAULT_CONFIG.backendUrl;
78
+ config.frontendUrl = config.frontendUrl || process.env.FRONTEND_URL || DEFAULT_CONFIG.frontendUrl;
79
+ config.wsPort = config.wsPort || Number(process.env.WS_PORT) || DEFAULT_CONFIG.wsPort;
80
+ config.containerPort = config.containerPort || Number(process.env.CONTAINER_PORT) || DEFAULT_CONFIG.containerPort;
81
+
82
+ return config;
83
+ }
84
+
85
+ /**
86
+ * Save configuration to disk
87
+ * @param {Object} config - Configuration object to save
88
+ */
89
+ export function saveConfig(config) {
90
+ ensureConfigDir();
91
+
92
+ const configToSave = {
93
+ ...config,
94
+ updatedAt: new Date().toISOString(),
95
+ };
96
+
97
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(configToSave, null, 2), {
98
+ mode: 0o600,
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Update specific configuration values
104
+ * @param {Object} updates - Configuration updates
105
+ */
106
+ export function updateConfig(updates) {
107
+ const currentConfig = loadConfig();
108
+ const newConfig = { ...currentConfig, ...updates };
109
+ saveConfig(newConfig);
110
+ return newConfig;
111
+ }
112
+
113
+ /**
114
+ * Get a specific configuration value
115
+ * @param {string} key - Configuration key
116
+ * @param {any} defaultValue - Default value if key not found
117
+ * @returns {any} Configuration value
118
+ */
119
+ export function getConfig(key, defaultValue = null) {
120
+ const config = loadConfig();
121
+ return config[key] !== undefined ? config[key] : defaultValue;
122
+ }
123
+
124
+ /**
125
+ * Check if configuration file exists
126
+ * @returns {boolean} True if config file exists
127
+ */
128
+ export function configExists() {
129
+ return fs.existsSync(CONFIG_FILE);
130
+ }
131
+
132
+ /**
133
+ * Clear configuration file
134
+ */
135
+ export function clearConfig() {
136
+ if (fs.existsSync(CONFIG_FILE)) {
137
+ fs.unlinkSync(CONFIG_FILE);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Get configuration file path
143
+ * @returns {string} Path to config file
144
+ */
145
+ export function getConfigPath() {
146
+ return CONFIG_FILE;
147
+ }
148
+
149
+ /**
150
+ * Initialize configuration with values from init wizard
151
+ * @param {Object} initConfig - Configuration from init wizard
152
+ */
153
+ export function initializeConfig(initConfig) {
154
+ const config = {
155
+ ...DEFAULT_CONFIG,
156
+ ...initConfig,
157
+ createdAt: new Date().toISOString(),
158
+ };
159
+
160
+ saveConfig(config);
161
+ return config;
162
+ }
163
+
164
+ export default {
165
+ loadConfig,
166
+ saveConfig,
167
+ updateConfig,
168
+ getConfig,
169
+ configExists,
170
+ clearConfig,
171
+ getConfigPath,
172
+ initializeConfig,
173
+ DEFAULT_CONFIG,
174
+ };
@@ -0,0 +1,107 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { encrypt, decrypt } from '../utils/encryption.js';
5
+
6
+ const FENWAVE_DIR = path.join(os.homedir(), '.fenwave');
7
+ const DEVICE_DIR = path.join(FENWAVE_DIR, 'device');
8
+ const CREDENTIAL_FILE = path.join(DEVICE_DIR, 'credential.json');
9
+
10
+ /**
11
+ * Ensure device directory exists
12
+ */
13
+ function ensureDeviceDir() {
14
+ if (!fs.existsSync(FENWAVE_DIR)) {
15
+ fs.mkdirSync(FENWAVE_DIR, { recursive: true, mode: 0o700 });
16
+ }
17
+ if (!fs.existsSync(DEVICE_DIR)) {
18
+ fs.mkdirSync(DEVICE_DIR, { recursive: true, mode: 0o700 });
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Save device credential to disk (encrypted)
24
+ *
25
+ * @param {Object} credentialData - Device credential data
26
+ * @param {string} credentialData.deviceId - Device ID
27
+ * @param {string} credentialData.deviceCredential - Device credential
28
+ * @param {string} credentialData.userEntityRef - User entity reference
29
+ * @param {string} credentialData.deviceName - Device name
30
+ * @param {string} credentialData.platform - Platform
31
+ * @param {string} credentialData.agentVersion - Agent version
32
+ */
33
+ export function saveDeviceCredential(credentialData) {
34
+ ensureDeviceDir();
35
+
36
+ const data = {
37
+ deviceId: credentialData.deviceId,
38
+ deviceCredential: encrypt(credentialData.deviceCredential), // Encrypt credential
39
+ userEntityRef: credentialData.userEntityRef,
40
+ deviceName: credentialData.deviceName,
41
+ platform: credentialData.platform,
42
+ agentVersion: credentialData.agentVersion,
43
+ registeredAt: new Date().toISOString(),
44
+ };
45
+
46
+ fs.writeFileSync(CREDENTIAL_FILE, JSON.stringify(data, null, 2), {
47
+ mode: 0o600,
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Load device credential from disk
53
+ *
54
+ * @returns {Object|null} Device credential data or null if not found
55
+ */
56
+ export function loadDeviceCredential() {
57
+ if (!fs.existsSync(CREDENTIAL_FILE)) {
58
+ return null;
59
+ }
60
+
61
+ try {
62
+ const data = JSON.parse(fs.readFileSync(CREDENTIAL_FILE, 'utf8'));
63
+
64
+ // Decrypt the credential
65
+ const decryptedCredential = decrypt(data.deviceCredential);
66
+
67
+ return {
68
+ deviceId: data.deviceId,
69
+ deviceCredential: decryptedCredential,
70
+ userEntityRef: data.userEntityRef,
71
+ deviceName: data.deviceName,
72
+ platform: data.platform,
73
+ agentVersion: data.agentVersion,
74
+ registeredAt: data.registeredAt,
75
+ };
76
+ } catch (error) {
77
+ console.error('Failed to load device credential:', error.message);
78
+ return null;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Check if device is registered
84
+ *
85
+ * @returns {boolean} True if device credential exists
86
+ */
87
+ export function isDeviceRegistered() {
88
+ return fs.existsSync(CREDENTIAL_FILE);
89
+ }
90
+
91
+ /**
92
+ * Clear device credential (delete file)
93
+ */
94
+ export function clearDeviceCredential() {
95
+ if (fs.existsSync(CREDENTIAL_FILE)) {
96
+ fs.unlinkSync(CREDENTIAL_FILE);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Get device credential file path
102
+ *
103
+ * @returns {string} Path to credential file
104
+ */
105
+ export function getCredentialFilePath() {
106
+ return CREDENTIAL_FILE;
107
+ }
@@ -0,0 +1,65 @@
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 NPM_DIR = path.join(FENWAVE_DIR, 'npm');
7
+ const NPM_TOKEN_FILE = path.join(NPM_DIR, 'token');
8
+
9
+ /**
10
+ * Ensure NPM directory exists
11
+ */
12
+ function ensureNpmDir() {
13
+ if (!fs.existsSync(FENWAVE_DIR)) {
14
+ fs.mkdirSync(FENWAVE_DIR, { recursive: true, mode: 0o700 });
15
+ }
16
+ if (!fs.existsSync(NPM_DIR)) {
17
+ fs.mkdirSync(NPM_DIR, { recursive: true, mode: 0o700 });
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Save NPM token to disk
23
+ *
24
+ * @param {string} token - NPM token
25
+ */
26
+ export function saveNpmToken(token) {
27
+ ensureNpmDir();
28
+ fs.writeFileSync(NPM_TOKEN_FILE, token, { mode: 0o600 });
29
+ }
30
+
31
+ /**
32
+ * Load NPM token from disk
33
+ *
34
+ * @returns {string|null} NPM token or null if not found
35
+ */
36
+ export function loadNpmToken() {
37
+ if (!fs.existsSync(NPM_TOKEN_FILE)) {
38
+ return null;
39
+ }
40
+
41
+ try {
42
+ return fs.readFileSync(NPM_TOKEN_FILE, 'utf8').trim();
43
+ } catch (error) {
44
+ console.error('Failed to load NPM token:', error.message);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Check if NPM token exists
51
+ *
52
+ * @returns {boolean} True if NPM token is configured
53
+ */
54
+ export function hasNpmToken() {
55
+ return fs.existsSync(NPM_TOKEN_FILE);
56
+ }
57
+
58
+ /**
59
+ * Clear NPM token (delete file)
60
+ */
61
+ export function clearNpmToken() {
62
+ if (fs.existsSync(NPM_TOKEN_FILE)) {
63
+ fs.unlinkSync(NPM_TOKEN_FILE);
64
+ }
65
+ }
@@ -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;