@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.
Files changed (95) hide show
  1. package/README.md +200 -167
  2. package/dist/commands/account/base.d.ts +3 -4
  3. package/dist/commands/account/base.js +12 -9
  4. package/dist/commands/account/forget.d.ts +1 -1
  5. package/dist/commands/account/forget.js +66 -17
  6. package/dist/commands/account/get-sppi.js +7 -11
  7. package/dist/commands/account/info.d.ts +1 -13
  8. package/dist/commands/account/info.js +25 -45
  9. package/dist/commands/account/list.js +6 -6
  10. package/dist/commands/account/login.d.ts +10 -3
  11. package/dist/commands/account/login.js +143 -87
  12. package/dist/commands/account/switch.d.ts +3 -0
  13. package/dist/commands/account/switch.js +31 -11
  14. package/dist/commands/assets/base.d.ts +39 -0
  15. package/dist/commands/assets/base.js +217 -0
  16. package/dist/commands/assets/create.d.ts +41 -0
  17. package/dist/commands/assets/create.js +277 -0
  18. package/dist/commands/assets/delete.d.ts +14 -0
  19. package/dist/commands/assets/delete.js +69 -0
  20. package/dist/commands/assets/get.d.ts +14 -0
  21. package/dist/commands/assets/get.js +79 -0
  22. package/dist/commands/assets/list.d.ts +7 -0
  23. package/dist/commands/assets/list.js +33 -0
  24. package/dist/commands/assets/update.d.ts +44 -0
  25. package/dist/commands/assets/update.js +321 -0
  26. package/dist/commands/base.d.ts +1 -0
  27. package/dist/commands/base.js +6 -0
  28. package/dist/config/config.schema.d.ts +2 -11
  29. package/dist/config/config.schema.js +2 -7
  30. package/dist/constants.d.ts +3 -3
  31. package/dist/constants.js +3 -3
  32. package/dist/errors.d.ts +0 -2
  33. package/dist/errors.js +0 -2
  34. package/dist/hooks/prerun/auth.js +5 -9
  35. package/dist/interfaces/config-manager.interface.d.ts +3 -1
  36. package/dist/lib/container.d.ts +4 -12
  37. package/dist/lib/container.js +28 -113
  38. package/dist/lib/swarm-client/fetch-api.d.ts +7 -0
  39. package/dist/lib/swarm-client/fetch-api.js +41 -0
  40. package/dist/lib/swarm-client/fetch-timeout.client.d.ts +1 -0
  41. package/dist/lib/swarm-client/fetch-timeout.client.js +32 -0
  42. package/dist/lib/swarm-client/index.d.ts +6 -0
  43. package/dist/lib/swarm-client/index.js +31 -0
  44. package/dist/lib/swarm-client/middlewares/authorization.middleware.d.ts +2 -0
  45. package/dist/lib/swarm-client/middlewares/authorization.middleware.js +12 -0
  46. package/dist/lib/swarm-client/middlewares/index.d.ts +6 -0
  47. package/dist/lib/swarm-client/middlewares/index.js +5 -0
  48. package/dist/lib/swarm-client/middlewares/logger.middleware.d.ts +2 -0
  49. package/dist/lib/swarm-client/middlewares/logger.middleware.js +30 -0
  50. package/dist/lib/swarm-client/middlewares/request-id.middleware.d.ts +2 -0
  51. package/dist/lib/swarm-client/middlewares/request-id.middleware.js +13 -0
  52. package/dist/lib/swarm-client/types.d.ts +23 -0
  53. package/dist/lib/swarm-client/types.js +1 -0
  54. package/dist/managers/account-manager.d.ts +1 -0
  55. package/dist/managers/account-manager.js +13 -18
  56. package/dist/managers/config-file-manager.d.ts +24 -17
  57. package/dist/managers/config-file-manager.js +285 -161
  58. package/dist/managers/config-manager.d.ts +6 -6
  59. package/dist/managers/config-manager.js +8 -8
  60. package/dist/services/account.service.d.ts +42 -0
  61. package/dist/services/account.service.js +140 -0
  62. package/dist/services/asset.service.d.ts +35 -0
  63. package/dist/services/asset.service.js +120 -0
  64. package/dist/services/auth.service.d.ts +4 -6
  65. package/dist/services/auth.service.js +108 -118
  66. package/dist/utils/helper.js +2 -2
  67. package/dist/utils/progress.js +1 -0
  68. package/dist/utils/prompt.service.d.ts +8 -1
  69. package/dist/utils/prompt.service.js +33 -1
  70. package/oclif.manifest.json +479 -215
  71. package/package.json +7 -8
  72. package/dist/commands/files/download.d.ts +0 -15
  73. package/dist/commands/files/download.js +0 -63
  74. package/dist/commands/files/upload.d.ts +0 -18
  75. package/dist/commands/files/upload.js +0 -83
  76. package/dist/commands/storage/base.d.ts +0 -13
  77. package/dist/commands/storage/base.js +0 -125
  78. package/dist/commands/storage/create.d.ts +0 -11
  79. package/dist/commands/storage/create.js +0 -53
  80. package/dist/commands/storage/select.d.ts +0 -9
  81. package/dist/commands/storage/select.js +0 -38
  82. package/dist/commands/storage/show.d.ts +0 -17
  83. package/dist/commands/storage/show.js +0 -34
  84. package/dist/commands/storage/update.d.ts +0 -14
  85. package/dist/commands/storage/update.js +0 -204
  86. package/dist/commands/workflows/extend-lease.d.ts +0 -17
  87. package/dist/commands/workflows/extend-lease.js +0 -102
  88. package/dist/hooks/finally/shutdown-blockchain.d.ts +0 -3
  89. package/dist/hooks/finally/shutdown-blockchain.js +0 -8
  90. package/dist/middlewares/auth-middleware.d.ts +0 -9
  91. package/dist/middlewares/auth-middleware.js +0 -91
  92. package/dist/middlewares/cookies-middleware.d.ts +0 -8
  93. package/dist/middlewares/cookies-middleware.js +0 -80
  94. package/dist/services/storage.service.d.ts +0 -73
  95. package/dist/services/storage.service.js +0 -378
@@ -1,11 +1,9 @@
1
- import * as fs from 'node:fs';
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 configPath = path.join(this.configsDir, configFileName);
35
- if (fs.existsSync(configPath)) {
36
- throw new Error(`Configuration file already exists: ${configFileName}`);
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.providerUrl = url;
37
+ config.swarmUrl = url;
41
38
  }
42
39
  if (account) {
43
40
  config.account = account;
44
41
  }
45
- fs.writeFileSync(configPath, JSON.stringify(config, undefined, 2), {
46
- encoding: 'utf8',
47
- flag: 'w',
48
- mode: FILE_ACCESS_PERMS,
49
- });
50
- this.logger.info({ configFileName, name, url }, 'Created new configuration');
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 (!fs.existsSync(configPath)) {
55
- throw new Error(`Configuration file not found: ${configName}`);
107
+ if (!(await this.pathExists(configPath))) {
108
+ throw new Error(`Account file not found: ${configName}`);
56
109
  }
57
- await this.removeConfig(configName);
58
- fs.unlinkSync(configPath);
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, force) {
62
- const configs = this.getConfigsWithNames();
119
+ async deleteConfigByName(name) {
120
+ const configs = await this.getConfigsWithNames();
63
121
  if (configs.length === 0) {
64
- this.ux.stdout('No configurations found');
65
- return;
122
+ throw new Error('No configurations found');
66
123
  }
67
- let configToDelete;
68
- if (name) {
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
- else {
76
- const selection = await this.selectPrompt({
77
- message: 'Select configuration to delete:',
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
- try {
102
- await this.deleteConfig(configToDelete);
103
- this.ux.stdout(`Successfully deleted configuration: ${displayName}`);
104
- const newCurrent = this.getCurrentConfigFile();
105
- if (newCurrent) {
106
- const newCurrentConfig = configs.find((c) => c.file === newCurrent);
107
- this.ux.stdout(`Current configuration is now: ${newCurrentConfig?.name || newCurrent}`);
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
- catch (error) {
115
- this.ux.error(`Failed to delete configuration: ${error instanceof Error ? error.message : String(error)}`);
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 (!fs.existsSync(configPath)) {
147
+ if (!(await this.pathExists(configPath))) {
121
148
  return undefined;
122
149
  }
123
150
  try {
124
- const raw = fs.readFileSync(configPath, 'utf8');
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
- getConfigs() {
136
- if (!fs.existsSync(this.configsDir)) {
137
- return [];
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 fs.readdirSync(this.configsDir).filter((file) => file.endsWith('.config.json'));
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
- if (!fs.existsSync(this.configsDir)) {
143
- return [];
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
- try {
152
- if (fs.existsSync(configPath)) {
153
- const raw = fs.readFileSync(configPath, 'utf8');
154
- const config = JSON.parse(raw);
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(DEFAULT_USER_CONFIG_FILE);
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 (!fs.existsSync(sourcePath)) {
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.readFileSync(sourcePath, 'utf8');
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
- let targetFileName;
209
- if (targetName) {
210
- targetFileName = getConfigName(targetName);
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 (fs.existsSync(targetPath)) {
256
+ if (await this.pathExists(targetPath)) {
221
257
  throw new Error(`Configuration file already exists: ${targetFileName}`);
222
258
  }
223
- fs.copyFileSync(sourcePath, targetPath);
224
- fs.chmodSync(targetPath, FILE_ACCESS_PERMS);
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 (!fs.existsSync(configPath)) {
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
- if (!fs.existsSync(this.configDir)) {
257
- fs.mkdirSync(this.configDir, {
258
- mode: DIR_ACCESS_PERMS,
259
- recursive: true,
260
- });
261
- }
262
- if (!fs.existsSync(this.configsDir)) {
263
- fs.mkdirSync(this.configsDir, {
264
- mode: DIR_ACCESS_PERMS,
265
- recursive: true,
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
- if (fs.existsSync(this.configFilePath)) {
272
- const raw = fs.readFileSync(this.configFilePath, 'utf8');
273
- this.configs = JSON.parse(raw);
274
- this.logger.debug({ configs: this.configs }, 'Configs loaded');
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
- this.logger.debug('Config manager file not found, using defaults');
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
- fs.writeFileSync(this.configFilePath, JSON.stringify(this.configs, undefined, 2), {
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): Promise<undefined | V>;
10
+ get<K extends keyof CliConfig, V extends CliConfig[K]>(key: K): undefined | V;
11
11
  private getConfig;
12
- getCookies(): Promise<unknown>;
13
- getCredentials(): Promise<{
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
- setCookies(cookies: unknown): Promise<void>;
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
- async get(key) {
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
- getCookies() {
28
- return this.get('cookies');
27
+ getAuthUrl() {
28
+ return this.config.authUrl || this.getSwarmUrl();
29
29
  }
30
- getCredentials() {
31
- return this.get('auth');
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
- async setCookies(cookies) {
82
- await this.set('cookies', cookies);
81
+ getCredentials() {
82
+ return this.get('auth');
83
83
  }
84
84
  async setCredentials(auth) {
85
85
  await this.set('auth', auth);