@super-protocol/sp-cli 0.0.8 → 0.0.10-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -167
- package/dist/commands/account/base.d.ts +3 -4
- package/dist/commands/account/base.js +12 -9
- package/dist/commands/account/forget.d.ts +1 -1
- package/dist/commands/account/forget.js +66 -17
- package/dist/commands/account/get-sppi.js +7 -11
- package/dist/commands/account/info.d.ts +1 -13
- package/dist/commands/account/info.js +25 -45
- package/dist/commands/account/list.js +6 -6
- package/dist/commands/account/login.d.ts +10 -3
- package/dist/commands/account/login.js +143 -87
- package/dist/commands/account/switch.d.ts +3 -0
- package/dist/commands/account/switch.js +31 -11
- package/dist/commands/assets/base.d.ts +39 -0
- package/dist/commands/assets/base.js +217 -0
- package/dist/commands/assets/create.d.ts +41 -0
- package/dist/commands/assets/create.js +277 -0
- package/dist/commands/assets/delete.d.ts +14 -0
- package/dist/commands/assets/delete.js +69 -0
- package/dist/commands/assets/get.d.ts +14 -0
- package/dist/commands/assets/get.js +79 -0
- package/dist/commands/assets/list.d.ts +7 -0
- package/dist/commands/assets/list.js +33 -0
- package/dist/commands/assets/update.d.ts +44 -0
- package/dist/commands/assets/update.js +321 -0
- package/dist/commands/base.d.ts +1 -0
- package/dist/commands/base.js +6 -0
- package/dist/config/config.schema.d.ts +2 -11
- package/dist/config/config.schema.js +2 -7
- package/dist/constants.d.ts +3 -3
- package/dist/constants.js +3 -3
- package/dist/errors.d.ts +0 -2
- package/dist/errors.js +0 -2
- package/dist/hooks/prerun/auth.js +5 -9
- package/dist/interfaces/config-manager.interface.d.ts +3 -1
- package/dist/lib/container.d.ts +4 -12
- package/dist/lib/container.js +28 -113
- package/dist/lib/swarm-client/fetch-api.d.ts +7 -0
- package/dist/lib/swarm-client/fetch-api.js +41 -0
- package/dist/lib/swarm-client/fetch-timeout.client.d.ts +1 -0
- package/dist/lib/swarm-client/fetch-timeout.client.js +32 -0
- package/dist/lib/swarm-client/index.d.ts +6 -0
- package/dist/lib/swarm-client/index.js +31 -0
- package/dist/lib/swarm-client/middlewares/authorization.middleware.d.ts +2 -0
- package/dist/lib/swarm-client/middlewares/authorization.middleware.js +12 -0
- package/dist/lib/swarm-client/middlewares/index.d.ts +6 -0
- package/dist/lib/swarm-client/middlewares/index.js +5 -0
- package/dist/lib/swarm-client/middlewares/logger.middleware.d.ts +2 -0
- package/dist/lib/swarm-client/middlewares/logger.middleware.js +30 -0
- package/dist/lib/swarm-client/middlewares/request-id.middleware.d.ts +2 -0
- package/dist/lib/swarm-client/middlewares/request-id.middleware.js +13 -0
- package/dist/lib/swarm-client/types.d.ts +23 -0
- package/dist/lib/swarm-client/types.js +1 -0
- package/dist/managers/account-manager.d.ts +1 -0
- package/dist/managers/account-manager.js +13 -18
- package/dist/managers/config-file-manager.d.ts +24 -17
- package/dist/managers/config-file-manager.js +285 -161
- package/dist/managers/config-manager.d.ts +6 -6
- package/dist/managers/config-manager.js +8 -8
- package/dist/services/account.service.d.ts +42 -0
- package/dist/services/account.service.js +140 -0
- package/dist/services/asset.service.d.ts +35 -0
- package/dist/services/asset.service.js +120 -0
- package/dist/services/auth.service.d.ts +4 -6
- package/dist/services/auth.service.js +108 -118
- package/dist/utils/helper.js +2 -2
- package/dist/utils/progress.js +1 -0
- package/dist/utils/prompt.service.d.ts +8 -1
- package/dist/utils/prompt.service.js +33 -1
- package/oclif.manifest.json +479 -215
- package/package.json +7 -8
- package/dist/commands/files/download.d.ts +0 -15
- package/dist/commands/files/download.js +0 -63
- package/dist/commands/files/upload.d.ts +0 -18
- package/dist/commands/files/upload.js +0 -83
- package/dist/commands/storage/base.d.ts +0 -13
- package/dist/commands/storage/base.js +0 -125
- package/dist/commands/storage/create.d.ts +0 -11
- package/dist/commands/storage/create.js +0 -53
- package/dist/commands/storage/select.d.ts +0 -9
- package/dist/commands/storage/select.js +0 -38
- package/dist/commands/storage/show.d.ts +0 -17
- package/dist/commands/storage/show.js +0 -34
- package/dist/commands/storage/update.d.ts +0 -14
- package/dist/commands/storage/update.js +0 -204
- package/dist/commands/workflows/extend-lease.d.ts +0 -17
- package/dist/commands/workflows/extend-lease.js +0 -102
- package/dist/hooks/finally/shutdown-blockchain.d.ts +0 -3
- package/dist/hooks/finally/shutdown-blockchain.js +0 -8
- package/dist/middlewares/auth-middleware.d.ts +0 -9
- package/dist/middlewares/auth-middleware.js +0 -91
- package/dist/middlewares/cookies-middleware.d.ts +0 -8
- package/dist/middlewares/cookies-middleware.js +0 -80
- package/dist/services/storage.service.d.ts +0 -73
- package/dist/services/storage.service.js +0 -378
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { confirm, isCancel, select } from '@clack/prompts';
|
|
4
|
-
import { ux } from '@oclif/core';
|
|
5
3
|
import { Value } from 'typebox/value';
|
|
6
4
|
import { cliConfigSchema } from '../config/config.schema.js';
|
|
7
|
-
import { DEFAULT_USER_CONFIG_FILE, DEFAULT_USER_CONFIG_NAME, DIR_ACCESS_PERMS, FILE_ACCESS_PERMS, } from '../constants.js';
|
|
8
|
-
import { getConfigName } from '../utils/helper.js';
|
|
5
|
+
import { DEFAULT_USER_CONFIG_FILE, DEFAULT_USER_CONFIG_NAME, DIR_ACCESS_PERMS, FILE_ACCESS_PERMS, SWARM_URL, } from '../constants.js';
|
|
6
|
+
import { getConfigName, getConfigNameFromCredentials } from '../utils/helper.js';
|
|
9
7
|
export class ConfigFileManager {
|
|
10
8
|
logger;
|
|
11
9
|
static CONFIG_FILE = 'config.json';
|
|
@@ -16,112 +14,141 @@ export class ConfigFileManager {
|
|
|
16
14
|
currentConfig: undefined,
|
|
17
15
|
};
|
|
18
16
|
configsDir;
|
|
19
|
-
confirmPrompt;
|
|
20
17
|
runtimeConfigFile;
|
|
21
|
-
selectPrompt;
|
|
22
|
-
ux;
|
|
23
18
|
constructor(configDir, logger, options = {}) {
|
|
24
19
|
this.logger = logger;
|
|
25
20
|
this.configDir = configDir;
|
|
26
21
|
this.configFilePath = path.join(this.configDir, ConfigFileManager.CONFIG_FILE);
|
|
27
22
|
this.configsDir = path.join(this.configDir, ConfigFileManager.CONFIGS_DIR);
|
|
28
|
-
this.confirmPrompt = options.confirmPrompt ?? confirm;
|
|
29
|
-
this.selectPrompt = options.selectPrompt ?? select;
|
|
30
|
-
this.ux = options.ux ?? ux;
|
|
31
23
|
this.runtimeConfigFile = options.runtimeConfigFile;
|
|
32
24
|
}
|
|
33
|
-
async createConfig(configFileName, name, url, account) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
async createConfig(configFileName, name, url, account, authUrl) {
|
|
26
|
+
const normalizedName = name?.trim();
|
|
27
|
+
const resolvedFileName = normalizedName ? getConfigName(normalizedName) : configFileName;
|
|
28
|
+
const configPath = path.join(this.configsDir, resolvedFileName);
|
|
29
|
+
if (await this.pathExists(configPath)) {
|
|
30
|
+
throw new Error(`Account file already exists: ${resolvedFileName}`);
|
|
31
|
+
}
|
|
32
|
+
const config = {};
|
|
33
|
+
if (normalizedName) {
|
|
34
|
+
config.name = normalizedName;
|
|
37
35
|
}
|
|
38
|
-
const config = { name };
|
|
39
36
|
if (url) {
|
|
40
|
-
config.
|
|
37
|
+
config.swarmUrl = url;
|
|
41
38
|
}
|
|
42
39
|
if (account) {
|
|
43
40
|
config.account = account;
|
|
44
41
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
});
|
|
50
|
-
|
|
42
|
+
if (authUrl) {
|
|
43
|
+
config.authUrl = authUrl;
|
|
44
|
+
}
|
|
45
|
+
await this.writeJsonFile(configPath, config);
|
|
46
|
+
this.logger.info({ configFileName: resolvedFileName, name: normalizedName, url }, 'Created new configuration');
|
|
47
|
+
return resolvedFileName;
|
|
48
|
+
}
|
|
49
|
+
async updateConfigName(configFileName, name) {
|
|
50
|
+
const configPath = path.join(this.configsDir, configFileName);
|
|
51
|
+
const normalizedName = name.trim();
|
|
52
|
+
if (!normalizedName) {
|
|
53
|
+
throw new Error('Configuration name cannot be empty');
|
|
54
|
+
}
|
|
55
|
+
if (!(await this.pathExists(configPath))) {
|
|
56
|
+
throw new Error(`Account file not found: ${configFileName}`);
|
|
57
|
+
}
|
|
58
|
+
const config = await this.readJsonFileOrThrow(configPath, { configFileName }, 'Failed to read config data', `Failed to read configuration: ${configFileName}`);
|
|
59
|
+
const targetFileName = getConfigName(normalizedName);
|
|
60
|
+
const nameChanged = config.name !== normalizedName;
|
|
61
|
+
const fileChanged = targetFileName !== configFileName;
|
|
62
|
+
if (!nameChanged && !fileChanged) {
|
|
63
|
+
return { updated: false, fileName: configFileName };
|
|
64
|
+
}
|
|
65
|
+
if (fileChanged) {
|
|
66
|
+
const targetPath = path.join(this.configsDir, targetFileName);
|
|
67
|
+
if (await this.pathExists(targetPath)) {
|
|
68
|
+
throw new Error(`Account file already exists: ${targetFileName}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
config.name = normalizedName;
|
|
72
|
+
if (fileChanged) {
|
|
73
|
+
const targetPath = path.join(this.configsDir, targetFileName);
|
|
74
|
+
await this.writeJsonFile(targetPath, config);
|
|
75
|
+
await fs.unlink(configPath);
|
|
76
|
+
if (this.configs.currentConfig === configFileName) {
|
|
77
|
+
this.configs.currentConfig = targetFileName;
|
|
78
|
+
await this.save();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
await this.writeJsonFile(configPath, config);
|
|
83
|
+
}
|
|
84
|
+
this.logger.info({ configFileName, renamedTo: fileChanged ? targetFileName : undefined, name: normalizedName }, 'Updated configuration name');
|
|
85
|
+
return { updated: nameChanged, fileName: fileChanged ? targetFileName : configFileName };
|
|
86
|
+
}
|
|
87
|
+
async updateConfigAuthUrl(configFileName, authUrl) {
|
|
88
|
+
const configPath = path.join(this.configsDir, configFileName);
|
|
89
|
+
const normalizedUrl = authUrl.trim();
|
|
90
|
+
if (!normalizedUrl) {
|
|
91
|
+
throw new Error('Auth URL cannot be empty');
|
|
92
|
+
}
|
|
93
|
+
if (!(await this.pathExists(configPath))) {
|
|
94
|
+
throw new Error(`Account file not found: ${configFileName}`);
|
|
95
|
+
}
|
|
96
|
+
const config = await this.readJsonFileOrThrow(configPath, { configFileName }, 'Failed to read config data', `Failed to read configuration: ${configFileName}`);
|
|
97
|
+
if (config.authUrl === normalizedUrl) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
config.authUrl = normalizedUrl;
|
|
101
|
+
await this.writeJsonFile(configPath, config);
|
|
102
|
+
this.logger.info({ configFileName, authUrl: normalizedUrl }, 'Updated auth URL');
|
|
103
|
+
return true;
|
|
51
104
|
}
|
|
52
105
|
async deleteConfig(configName) {
|
|
53
106
|
const configPath = path.join(this.configsDir, configName);
|
|
54
|
-
if (!
|
|
55
|
-
throw new Error(`
|
|
107
|
+
if (!(await this.pathExists(configPath))) {
|
|
108
|
+
throw new Error(`Account file not found: ${configName}`);
|
|
56
109
|
}
|
|
57
|
-
|
|
58
|
-
|
|
110
|
+
if (this.configs.currentConfig === configName) {
|
|
111
|
+
const configs = await this.listConfigFiles();
|
|
112
|
+
const remainingConfigs = configs.filter((c) => c !== configName);
|
|
113
|
+
this.configs.currentConfig = remainingConfigs.length > 0 ? remainingConfigs[0] : undefined;
|
|
114
|
+
await this.save();
|
|
115
|
+
}
|
|
116
|
+
await fs.unlink(configPath);
|
|
59
117
|
this.logger.info({ configName }, 'Deleted configuration file');
|
|
60
118
|
}
|
|
61
|
-
async deleteConfigByName(name
|
|
62
|
-
const configs = this.getConfigsWithNames();
|
|
119
|
+
async deleteConfigByName(name) {
|
|
120
|
+
const configs = await this.getConfigsWithNames();
|
|
63
121
|
if (configs.length === 0) {
|
|
64
|
-
|
|
65
|
-
return;
|
|
122
|
+
throw new Error('No configurations found');
|
|
66
123
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const config = configs.find((c) => c.name === name);
|
|
70
|
-
if (!config) {
|
|
71
|
-
this.ux.error(`Configuration not found: ${name}`);
|
|
72
|
-
}
|
|
73
|
-
configToDelete = config.file;
|
|
124
|
+
if (!name) {
|
|
125
|
+
throw new Error('Configuration name is required');
|
|
74
126
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
options: configs.map((config) => ({
|
|
79
|
-
label: config.name,
|
|
80
|
-
value: config.file,
|
|
81
|
-
})),
|
|
82
|
-
});
|
|
83
|
-
if (isCancel(selection)) {
|
|
84
|
-
this.ux.stdout('Deletion cancelled');
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
configToDelete = selection;
|
|
88
|
-
}
|
|
89
|
-
const configToDeleteData = configs.find((c) => c.file === configToDelete);
|
|
90
|
-
const displayName = configToDeleteData?.name || configToDelete;
|
|
91
|
-
if (!force) {
|
|
92
|
-
const confirmed = await this.confirmPrompt({
|
|
93
|
-
initialValue: false,
|
|
94
|
-
message: `Are you sure you want to delete configuration "${displayName}"?`,
|
|
95
|
-
});
|
|
96
|
-
if (isCancel(confirmed) || !confirmed) {
|
|
97
|
-
this.ux.stdout('Deletion cancelled');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
127
|
+
const configToDeleteData = configs.find((c) => c.name === name || c.file === name);
|
|
128
|
+
if (!configToDeleteData) {
|
|
129
|
+
throw new Error(`Configuration not found: ${name}`);
|
|
100
130
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
this.ux.stdout('No configurations remaining');
|
|
111
|
-
this.ux.stdout('Create a new account with: sp account login --name "Account Name" or sp auth login');
|
|
112
|
-
}
|
|
131
|
+
const configToDelete = configToDeleteData.file;
|
|
132
|
+
const displayName = configToDeleteData.name || configToDelete;
|
|
133
|
+
await this.deleteConfig(configToDelete);
|
|
134
|
+
this.logger.info({ displayName, configToDelete }, 'Successfully deleted configuration');
|
|
135
|
+
const newCurrent = this.getCurrentConfigFile();
|
|
136
|
+
if (newCurrent) {
|
|
137
|
+
const newCurrentConfig = configs.find((c) => c.file === newCurrent);
|
|
138
|
+
this.logger.info({ currentConfig: newCurrentConfig?.name || newCurrent }, 'Current configuration is now');
|
|
113
139
|
}
|
|
114
|
-
|
|
115
|
-
this.
|
|
140
|
+
else {
|
|
141
|
+
this.logger.info('No configurations remaining');
|
|
142
|
+
this.logger.info('Create a new account with: sp account login --name "Account Name" or sp account login');
|
|
116
143
|
}
|
|
117
144
|
}
|
|
118
|
-
getConfigData(configName) {
|
|
145
|
+
async getConfigData(configName) {
|
|
119
146
|
const configPath = path.join(this.configsDir, configName);
|
|
120
|
-
if (!
|
|
147
|
+
if (!(await this.pathExists(configPath))) {
|
|
121
148
|
return undefined;
|
|
122
149
|
}
|
|
123
150
|
try {
|
|
124
|
-
const raw = fs.
|
|
151
|
+
const raw = await fs.readFile(configPath, 'utf8');
|
|
125
152
|
return JSON.parse(raw);
|
|
126
153
|
}
|
|
127
154
|
catch (error) {
|
|
@@ -132,36 +159,49 @@ export class ConfigFileManager {
|
|
|
132
159
|
getConfigDir() {
|
|
133
160
|
return this.configsDir;
|
|
134
161
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
162
|
+
async configExists(configName) {
|
|
163
|
+
return this.pathExists(path.join(this.configsDir, configName));
|
|
164
|
+
}
|
|
165
|
+
async listConfigFiles() {
|
|
166
|
+
try {
|
|
167
|
+
const files = await fs.readdir(this.configsDir);
|
|
168
|
+
return files.filter((file) => file.endsWith('.json') || file.endsWith('.config.json'));
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (this.isNotFoundError(error)) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
getConfigBaseName(file) {
|
|
178
|
+
if (file.endsWith('.config.json')) {
|
|
179
|
+
return file.slice(0, -'.config.json'.length);
|
|
138
180
|
}
|
|
139
|
-
return
|
|
181
|
+
return path.basename(file, path.extname(file));
|
|
182
|
+
}
|
|
183
|
+
async getConfigWithName(name) {
|
|
184
|
+
const configs = await this.getConfigsWithNames();
|
|
185
|
+
const config = configs.find((config) => config.name === name ||
|
|
186
|
+
config.file === name ||
|
|
187
|
+
this.getConfigBaseName(config.file) === name);
|
|
188
|
+
if (!config) {
|
|
189
|
+
throw new Error(`Account with name ${name} not found`);
|
|
190
|
+
}
|
|
191
|
+
return config;
|
|
140
192
|
}
|
|
141
|
-
getConfigsWithNames() {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
const configFiles = fs
|
|
146
|
-
.readdirSync(this.configsDir)
|
|
147
|
-
.filter((file) => file.endsWith('.config.json'));
|
|
148
|
-
return configFiles.map((configFile) => {
|
|
193
|
+
async getConfigsWithNames() {
|
|
194
|
+
const configFiles = await this.listConfigFiles();
|
|
195
|
+
return Promise.all(configFiles.map(async (configFile) => {
|
|
149
196
|
const configPath = path.join(this.configsDir, configFile);
|
|
150
|
-
let name = configFile;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (config.name) {
|
|
156
|
-
name = config.name;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
this.logger.warn({ configFile, err: error }, 'Failed to read config name');
|
|
197
|
+
let name = this.getConfigBaseName(configFile);
|
|
198
|
+
const config = await this.tryReadJsonFile(configPath, { configFile }, 'Failed to read config name');
|
|
199
|
+
const normalizedName = config?.name?.trim();
|
|
200
|
+
if (normalizedName) {
|
|
201
|
+
name = normalizedName;
|
|
162
202
|
}
|
|
163
203
|
return { file: configFile, name };
|
|
164
|
-
});
|
|
204
|
+
}));
|
|
165
205
|
}
|
|
166
206
|
getCurrentConfigFile() {
|
|
167
207
|
if (this.runtimeConfigFile) {
|
|
@@ -174,14 +214,18 @@ export class ConfigFileManager {
|
|
|
174
214
|
return this.runtimeConfigFile;
|
|
175
215
|
}
|
|
176
216
|
if (!this.configs.currentConfig) {
|
|
217
|
+
let defaultConfigFile = DEFAULT_USER_CONFIG_FILE;
|
|
177
218
|
try {
|
|
178
|
-
await this.createConfig(DEFAULT_USER_CONFIG_FILE, DEFAULT_USER_CONFIG_NAME);
|
|
219
|
+
defaultConfigFile = await this.createConfig(DEFAULT_USER_CONFIG_FILE, DEFAULT_USER_CONFIG_NAME);
|
|
179
220
|
}
|
|
180
|
-
catch {
|
|
181
|
-
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (!this.isAlreadyExistsError(error)) {
|
|
223
|
+
this.logger.error({ err: error }, 'Failed to create default config');
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
182
226
|
}
|
|
183
227
|
try {
|
|
184
|
-
await this.setCurrentConfig(
|
|
228
|
+
await this.setCurrentConfig(defaultConfigFile);
|
|
185
229
|
}
|
|
186
230
|
catch (error) {
|
|
187
231
|
this.logger.error({ err: error }, 'Failed to set default config');
|
|
@@ -191,12 +235,12 @@ export class ConfigFileManager {
|
|
|
191
235
|
return path.join(this.configsDir, this.configs.currentConfig || DEFAULT_USER_CONFIG_FILE);
|
|
192
236
|
}
|
|
193
237
|
async importConfig(sourcePath, targetName) {
|
|
194
|
-
if (!
|
|
238
|
+
if (!(await this.pathExists(sourcePath))) {
|
|
195
239
|
throw new Error(`Source file not found: ${sourcePath}`);
|
|
196
240
|
}
|
|
197
241
|
let sourceConfig;
|
|
198
242
|
try {
|
|
199
|
-
const raw = fs.
|
|
243
|
+
const raw = await fs.readFile(sourcePath, 'utf8');
|
|
200
244
|
sourceConfig = JSON.parse(raw);
|
|
201
245
|
if (!Value.Check(cliConfigSchema, sourceConfig)) {
|
|
202
246
|
throw new Error("Configuration doesn't match required schema");
|
|
@@ -205,23 +249,15 @@ export class ConfigFileManager {
|
|
|
205
249
|
catch (error) {
|
|
206
250
|
throw new Error(`Invalid configuration file: ${error instanceof Error ? error.message : String(error)}`);
|
|
207
251
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
else if (sourceConfig.name) {
|
|
213
|
-
targetFileName = getConfigName(sourceConfig.name);
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
const basename = path.basename(sourcePath, path.extname(sourcePath));
|
|
217
|
-
targetFileName = getConfigName(basename);
|
|
218
|
-
}
|
|
252
|
+
const targetFileName = targetName
|
|
253
|
+
? getConfigName(targetName)
|
|
254
|
+
: this.resolveConfigFileName(sourceConfig, path.basename(sourcePath, path.extname(sourcePath)));
|
|
219
255
|
const targetPath = path.join(this.configsDir, targetFileName);
|
|
220
|
-
if (
|
|
256
|
+
if (await this.pathExists(targetPath)) {
|
|
221
257
|
throw new Error(`Configuration file already exists: ${targetFileName}`);
|
|
222
258
|
}
|
|
223
|
-
fs.
|
|
224
|
-
fs.
|
|
259
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
260
|
+
await fs.chmod(targetPath, FILE_ACCESS_PERMS);
|
|
225
261
|
this.logger.info({ sourcePath, targetFileName }, 'Imported configuration');
|
|
226
262
|
return targetFileName;
|
|
227
263
|
}
|
|
@@ -230,20 +266,13 @@ export class ConfigFileManager {
|
|
|
230
266
|
this.logger.debug({ runtimeConfigFile: this.runtimeConfigFile }, 'Runtime config provided, skipping config file initialization');
|
|
231
267
|
return;
|
|
232
268
|
}
|
|
233
|
-
this.ensureDirectories();
|
|
269
|
+
await this.ensureDirectories();
|
|
234
270
|
await this.load();
|
|
235
|
-
|
|
236
|
-
async removeConfig(configName) {
|
|
237
|
-
if (this.configs.currentConfig === configName) {
|
|
238
|
-
const configs = this.getConfigs();
|
|
239
|
-
const remainingConfigs = configs.filter((c) => c !== configName);
|
|
240
|
-
this.configs.currentConfig = remainingConfigs.length > 0 ? remainingConfigs[0] : undefined;
|
|
241
|
-
await this.save();
|
|
242
|
-
}
|
|
271
|
+
await this.normalizeConfigFiles();
|
|
243
272
|
}
|
|
244
273
|
async setCurrentConfig(configName) {
|
|
245
274
|
const configPath = path.join(this.configsDir, configName);
|
|
246
|
-
if (!
|
|
275
|
+
if (!(await this.pathExists(configPath))) {
|
|
247
276
|
throw new Error(`Configuration file not found: ${configName}`);
|
|
248
277
|
}
|
|
249
278
|
this.configs.currentConfig = configName;
|
|
@@ -252,47 +281,142 @@ export class ConfigFileManager {
|
|
|
252
281
|
setRuntimeConfigFile(runtimeConfigFile) {
|
|
253
282
|
this.runtimeConfigFile = runtimeConfigFile;
|
|
254
283
|
}
|
|
255
|
-
ensureDirectories() {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
284
|
+
async ensureDirectories() {
|
|
285
|
+
await fs.mkdir(this.configDir, {
|
|
286
|
+
mode: DIR_ACCESS_PERMS,
|
|
287
|
+
recursive: true,
|
|
288
|
+
});
|
|
289
|
+
await fs.mkdir(this.configsDir, {
|
|
290
|
+
mode: DIR_ACCESS_PERMS,
|
|
291
|
+
recursive: true,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
async normalizeConfigFiles() {
|
|
295
|
+
const configFiles = await this.listConfigFiles();
|
|
296
|
+
if (configFiles.length === 0) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
let updatedCurrent = false;
|
|
300
|
+
for (const configFile of configFiles) {
|
|
301
|
+
const configPath = path.join(this.configsDir, configFile);
|
|
302
|
+
const config = await this.tryReadJsonFile(configPath, { configFile }, 'Failed to read config for normalization');
|
|
303
|
+
if (!config) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const desiredFileName = this.resolveConfigFileName(config, this.getConfigBaseName(configFile));
|
|
307
|
+
if (desiredFileName === configFile) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const targetPath = path.join(this.configsDir, desiredFileName);
|
|
311
|
+
if (await this.pathExists(targetPath)) {
|
|
312
|
+
this.logger.warn({ configFile, desiredFileName }, 'Config file name collision; skipping rename');
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
await fs.rename(configPath, targetPath);
|
|
316
|
+
if (this.configs.currentConfig === configFile) {
|
|
317
|
+
this.configs.currentConfig = desiredFileName;
|
|
318
|
+
updatedCurrent = true;
|
|
319
|
+
}
|
|
320
|
+
this.logger.info({ from: configFile, to: desiredFileName }, 'Renamed configuration file');
|
|
321
|
+
}
|
|
322
|
+
if (updatedCurrent) {
|
|
323
|
+
await this.save();
|
|
267
324
|
}
|
|
268
325
|
}
|
|
269
326
|
async load() {
|
|
270
327
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
328
|
+
const raw = await fs.readFile(this.configFilePath, 'utf8');
|
|
329
|
+
this.configs = JSON.parse(raw);
|
|
330
|
+
this.logger.debug({ configs: this.configs }, 'Configs loaded');
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (this.isNotFoundError(error)) {
|
|
334
|
+
this.logger.debug('Config manager file not found, using defaults');
|
|
335
|
+
await this.save();
|
|
275
336
|
}
|
|
276
337
|
else {
|
|
277
|
-
|
|
338
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
339
|
+
const backupPath = `${this.configFilePath}.corrupt-${timestamp}`;
|
|
340
|
+
this.logger.warn({ err: error, backupPath }, 'Config manager file appears corrupted; backing up and recreating');
|
|
341
|
+
try {
|
|
342
|
+
await fs.rename(this.configFilePath, backupPath);
|
|
343
|
+
}
|
|
344
|
+
catch (backupError) {
|
|
345
|
+
this.logger.error({ err: backupError, path: this.configFilePath, backupPath }, 'Failed to back up corrupted config manager file');
|
|
346
|
+
throw backupError;
|
|
347
|
+
}
|
|
348
|
+
this.configs = {
|
|
349
|
+
currentConfig: undefined,
|
|
350
|
+
};
|
|
278
351
|
await this.save();
|
|
279
352
|
}
|
|
280
353
|
}
|
|
281
|
-
catch (error) {
|
|
282
|
-
this.logger.error({ err: error }, 'Failed to load config manager file, using defaults');
|
|
283
|
-
this.configs = {
|
|
284
|
-
currentConfig: undefined,
|
|
285
|
-
};
|
|
286
|
-
await this.save();
|
|
287
|
-
}
|
|
288
354
|
}
|
|
289
355
|
async save() {
|
|
290
|
-
this.ensureDirectories();
|
|
356
|
+
await this.ensureDirectories();
|
|
291
357
|
this.logger.debug({ configs: this.configs, path: this.configFilePath }, 'Saving config manager file');
|
|
292
|
-
|
|
358
|
+
await this.writeJsonFile(this.configFilePath, this.configs);
|
|
359
|
+
}
|
|
360
|
+
async pathExists(targetPath) {
|
|
361
|
+
try {
|
|
362
|
+
await fs.access(targetPath);
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async readJsonFileOrThrow(filePath, context, warnMessage, errorMessage) {
|
|
370
|
+
try {
|
|
371
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
372
|
+
return JSON.parse(raw);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
this.logger.warn({ ...context, err: error }, warnMessage);
|
|
376
|
+
throw new Error(errorMessage);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async tryReadJsonFile(filePath, context, warnMessage) {
|
|
380
|
+
try {
|
|
381
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
382
|
+
return JSON.parse(raw);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
this.logger.warn({ ...context, err: error }, warnMessage);
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async writeJsonFile(filePath, data) {
|
|
390
|
+
await fs.writeFile(filePath, JSON.stringify(data, undefined, 2), {
|
|
293
391
|
encoding: 'utf8',
|
|
294
392
|
flag: 'w',
|
|
295
393
|
mode: FILE_ACCESS_PERMS,
|
|
296
394
|
});
|
|
297
395
|
}
|
|
396
|
+
resolveConfigFileName(config, fallbackName) {
|
|
397
|
+
const normalizedName = config.name?.trim();
|
|
398
|
+
if (normalizedName) {
|
|
399
|
+
return getConfigName(normalizedName);
|
|
400
|
+
}
|
|
401
|
+
if (config.account?.privateKey) {
|
|
402
|
+
const swarmUrl = (config.swarmUrl ?? SWARM_URL).trim();
|
|
403
|
+
return getConfigNameFromCredentials(config.account.privateKey, swarmUrl);
|
|
404
|
+
}
|
|
405
|
+
return getConfigName(fallbackName);
|
|
406
|
+
}
|
|
407
|
+
isNotFoundError(error) {
|
|
408
|
+
return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
|
|
409
|
+
}
|
|
410
|
+
isAlreadyExistsError(error) {
|
|
411
|
+
if (!error || typeof error !== 'object') {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
if ('code' in error && error.code === 'EEXIST') {
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
if (error instanceof Error) {
|
|
418
|
+
return /already exists/i.test(error.message);
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
298
422
|
}
|
|
@@ -7,18 +7,18 @@ export declare class ConfigManager implements IConfigManager {
|
|
|
7
7
|
private config;
|
|
8
8
|
private readonly configDir;
|
|
9
9
|
constructor(configFile: string, logger: pino.BaseLogger);
|
|
10
|
-
get<K extends keyof CliConfig, V extends CliConfig[K]>(key: K):
|
|
10
|
+
get<K extends keyof CliConfig, V extends CliConfig[K]>(key: K): undefined | V;
|
|
11
11
|
private getConfig;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
accessKey: string;
|
|
15
|
-
} | undefined>;
|
|
12
|
+
getAuthUrl(): string;
|
|
13
|
+
getSwarmUrl(): string;
|
|
16
14
|
init(): Promise<void>;
|
|
17
15
|
load(): Promise<CliConfig>;
|
|
18
16
|
private makeBackup;
|
|
19
17
|
save(): Promise<void>;
|
|
20
18
|
set<K extends keyof CliConfig, V extends CliConfig[K]>(key: K, value: V): Promise<void>;
|
|
21
|
-
|
|
19
|
+
getCredentials(): {
|
|
20
|
+
accessKey: string;
|
|
21
|
+
} | undefined;
|
|
22
22
|
setCredentials(auth: Auth): Promise<void>;
|
|
23
23
|
private getConfigPath;
|
|
24
24
|
private hasConfig;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ux } from '@oclif/core';
|
|
4
|
-
import { DIR_ACCESS_PERMS, FILE_ACCESS_PERMS } from '../constants.js';
|
|
4
|
+
import { DIR_ACCESS_PERMS, FILE_ACCESS_PERMS, SWARM_URL } from '../constants.js';
|
|
5
5
|
import { ConfigurationNotFoundError } from '../errors.js';
|
|
6
6
|
export class ConfigManager {
|
|
7
7
|
configFile;
|
|
@@ -13,7 +13,7 @@ export class ConfigManager {
|
|
|
13
13
|
this.logger = logger;
|
|
14
14
|
this.configDir = path.dirname(configFile);
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
get(key) {
|
|
17
17
|
return this.config[key];
|
|
18
18
|
}
|
|
19
19
|
getConfig() {
|
|
@@ -24,11 +24,11 @@ export class ConfigManager {
|
|
|
24
24
|
const raw = fs.readFileSync(configPath, 'utf8');
|
|
25
25
|
return JSON.parse(raw);
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
return this.
|
|
27
|
+
getAuthUrl() {
|
|
28
|
+
return this.config.authUrl || this.getSwarmUrl();
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
return this.
|
|
30
|
+
getSwarmUrl() {
|
|
31
|
+
return this.config.swarmUrl || SWARM_URL;
|
|
32
32
|
}
|
|
33
33
|
async init() {
|
|
34
34
|
try {
|
|
@@ -78,8 +78,8 @@ export class ConfigManager {
|
|
|
78
78
|
this.logger.debug({ key }, 'Saving parameters to file');
|
|
79
79
|
await this.save();
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
getCredentials() {
|
|
82
|
+
return this.get('auth');
|
|
83
83
|
}
|
|
84
84
|
async setCredentials(auth) {
|
|
85
85
|
await this.set('auth', auth);
|