@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.
- package/.claude/settings.local.json +11 -0
- package/Dockerfile +12 -0
- package/LICENSE +29 -0
- package/README.md +434 -0
- package/auth.js +276 -0
- package/cli-commands.js +1185 -0
- package/containerManager.js +385 -0
- package/convert-to-esm.sh +62 -0
- package/docker-actions/apps.js +3256 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +346 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +188 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +696 -0
- package/helper-functions.js +193 -0
- package/index.html +60 -0
- package/index.js +988 -0
- package/package.json +49 -0
- package/setup/setupWizard.js +499 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +174 -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/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +54 -0
- package/utils/errorHandler.js +327 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/websocket-server.js +364 -0
|
@@ -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;
|