spectrum-cli 2.1.1 → 3.0.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/src/token.js CHANGED
@@ -123,11 +123,11 @@ function parseTargets(urls, kind) {
123
123
  return { ok: true, targets };
124
124
  }
125
125
 
126
- async function promptPrivateToken() {
126
+ async function promptHidden(message) {
127
127
  return new Promise((resolve) => {
128
128
  const stdin = process.stdin;
129
129
  const stdout = process.stdout;
130
- stdout.write('🔑 Введите GITLAB_PRIVATE_TOKEN: ');
130
+ stdout.write(message);
131
131
 
132
132
  if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
133
133
  const rl = readline.createInterface({ input: stdin, output: stdout });
@@ -165,9 +165,9 @@ async function promptPrivateToken() {
165
165
  });
166
166
  }
167
167
 
168
- async function gitlabRequest(origin, privateToken, method, apiPath, body) {
168
+ async function gitlabRequest(origin, accessToken, method, apiPath, body) {
169
169
  const url = `${origin}/api/v4${apiPath}`;
170
- const headers = { 'PRIVATE-TOKEN': privateToken };
170
+ const headers = { 'PRIVATE-TOKEN': accessToken };
171
171
  const options = { method, headers };
172
172
  if (body !== undefined) {
173
173
  headers['Content-Type'] = 'application/json';
@@ -193,13 +193,13 @@ async function gitlabRequest(origin, privateToken, method, apiPath, body) {
193
193
  };
194
194
  }
195
195
 
196
- async function gitlabGetAll(origin, privateToken, apiPath) {
196
+ async function gitlabGetAll(origin, accessToken, apiPath) {
197
197
  const items = [];
198
198
  let page = 1;
199
199
 
200
200
  while (true) {
201
201
  const separator = apiPath.includes('?') ? '&' : '?';
202
- const result = await gitlabRequest(origin, privateToken, 'GET', `${apiPath}${separator}per_page=100&page=${page}`);
202
+ const result = await gitlabRequest(origin, accessToken, 'GET', `${apiPath}${separator}per_page=100&page=${page}`);
203
203
  if (!result.ok) {
204
204
  return result;
205
205
  }
@@ -272,10 +272,17 @@ function loadOrCreateConfig() {
272
272
  };
273
273
  }
274
274
 
275
- async function askPrivateToken(ctx) {
276
- logSuccess('🔑', 'Запрашиваю %s. Значение останется только в памяти.', TOKEN_NAME);
277
- const privateToken = await module.exports.promptPrivateToken();
278
- if (!privateToken) {
275
+ async function askTokens(ctx) {
276
+ logSuccess('🔑', 'Запрашиваю owner PAT для работы с CI variables. Значение останется только в памяти.');
277
+ const ownerToken = await module.exports.promptHidden('🔑 Введите owner PAT: ');
278
+ if (!ownerToken) {
279
+ return { ok: false, reason: 'Owner PAT не указан.' };
280
+ }
281
+ logSuccess('🔑', 'Owner PAT принят в память.');
282
+
283
+ logSuccess('🔑', 'Запрашиваю %s для записи в CI variables. Значение останется только в памяти.', TOKEN_NAME);
284
+ const ciToken = await module.exports.promptHidden(`🔑 Введите ${TOKEN_NAME}: `);
285
+ if (!ciToken) {
279
286
  return { ok: false, reason: `${TOKEN_NAME} не указан.` };
280
287
  }
281
288
 
@@ -284,7 +291,8 @@ async function askPrivateToken(ctx) {
284
291
  return {
285
292
  ok: true,
286
293
  data: {
287
- privateToken,
294
+ ownerToken,
295
+ ciToken,
288
296
  expiresAt,
289
297
  variableDescription: buildDescription(ctx.bot, expiresAt)
290
298
  }
@@ -297,7 +305,7 @@ async function checkTargetsAccess(ctx) {
297
305
  const target = ctx.targets[index];
298
306
  const endpoint = target.kind === 'группы' ? 'groups' : 'projects';
299
307
  logSuccess('🔍', '[%s/%s] Проверяю доступ к %s %s', String(index + 1), String(total), target.kind, target.url);
300
- const result = await gitlabRequest(ctx.origin, ctx.privateToken, 'GET', `/${endpoint}/${target.encodedPath}`);
308
+ const result = await gitlabRequest(ctx.origin, ctx.ownerToken, 'GET', `/${endpoint}/${target.encodedPath}`);
301
309
  if (!result.ok) {
302
310
  return { ok: false, reason: `${target.kind === 'группы' ? 'Группа' : 'Проект'} недоступен: ${target.url} (HTTP ${result.status}).` };
303
311
  }
@@ -311,7 +319,7 @@ async function updateTargetVariable(ctx, target, index, total) {
311
319
  const label = target.kind === 'группы' ? 'группе' : 'проекте';
312
320
  logSuccess('📦', '[%s/%s] Обновляю CI variable %s в %s %s', String(index + 1), String(total), TOKEN_NAME, label, target.path);
313
321
 
314
- const listed = await gitlabGetAll(ctx.origin, ctx.privateToken, `/${endpoint}/${target.encodedPath}/variables`);
322
+ const listed = await gitlabGetAll(ctx.origin, ctx.ownerToken, `/${endpoint}/${target.encodedPath}/variables`);
315
323
  if (!listed.ok) {
316
324
  logError('❌', 'Не удалось получить CI variables для %s (HTTP %s).', target.path, String(listed.status));
317
325
  return false;
@@ -320,7 +328,7 @@ async function updateTargetVariable(ctx, target, index, total) {
320
328
  const exists = (listed.data || []).some((variable) => variable.key === TOKEN_NAME);
321
329
  if (exists) {
322
330
  logSuccess('🗑', '[%s/%s] Удаляю старую CI variable %s в %s %s', String(index + 1), String(total), TOKEN_NAME, label, target.path);
323
- const deleted = await gitlabRequest(ctx.origin, ctx.privateToken, 'DELETE', `/${endpoint}/${target.encodedPath}/variables/${TOKEN_NAME}`);
331
+ const deleted = await gitlabRequest(ctx.origin, ctx.ownerToken, 'DELETE', `/${endpoint}/${target.encodedPath}/variables/${TOKEN_NAME}`);
324
332
  if (!deleted.ok && deleted.status !== 204) {
325
333
  logError('❌', 'Не удалось удалить CI variable в %s (HTTP %s).', target.path, String(deleted.status));
326
334
  return false;
@@ -331,9 +339,9 @@ async function updateTargetVariable(ctx, target, index, total) {
331
339
  }
332
340
 
333
341
  logSuccess('📝', '[%s/%s] Создаю CI variable %s в %s %s', String(index + 1), String(total), TOKEN_NAME, label, target.path);
334
- const created = await gitlabRequest(ctx.origin, ctx.privateToken, 'POST', `/${endpoint}/${target.encodedPath}/variables`, {
342
+ const created = await gitlabRequest(ctx.origin, ctx.ownerToken, 'POST', `/${endpoint}/${target.encodedPath}/variables`, {
335
343
  key: TOKEN_NAME,
336
- value: ctx.privateToken,
344
+ value: ctx.ciToken,
337
345
  masked_and_hidden: true,
338
346
  description: ctx.variableDescription
339
347
  });
@@ -364,7 +372,7 @@ function tokenRotate() {
364
372
  name: 'token rotate',
365
373
  checks: [
366
374
  { name: 'load-config', run: loadOrCreateConfig },
367
- { name: 'ask-private-token', run: askPrivateToken },
375
+ { name: 'ask-tokens', run: askTokens },
368
376
  { name: 'check-access', run: checkTargetsAccess }
369
377
  ],
370
378
  steps: [
@@ -382,9 +390,9 @@ module.exports = {
382
390
  addMonths,
383
391
  buildDescription,
384
392
  validateConfig,
385
- promptPrivateToken,
393
+ promptHidden,
386
394
  loadOrCreateConfig,
387
- askPrivateToken,
395
+ askTokens,
388
396
  checkTargetsAccess,
389
397
  updateCiVariables,
390
398
  tokenRotate
package/src/utils.js CHANGED
@@ -83,15 +83,6 @@ function getPackageManager() {
83
83
  return null;
84
84
  }
85
85
 
86
- function getVersion() {
87
- try {
88
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
89
- return packageJson.version;
90
- } catch (error) {
91
- return null;
92
- }
93
- }
94
-
95
86
  function getRemoteUrl() {
96
87
  const raw = execSilent('git remote get-url origin');
97
88
  if (!raw) return null;
@@ -125,7 +116,6 @@ module.exports = {
125
116
  getMainBranch,
126
117
  getDevelopBranch,
127
118
  getPackageManager,
128
- getVersion,
129
119
  getRemoteUrl,
130
120
  getMergeRequestUrl
131
121
  };
package/src/version.js CHANGED
@@ -1,12 +1,33 @@
1
1
  #!/usr/bin/env node
2
- const fs = require('fs');
3
- const { logSuccess, logError } = require('./utils');
4
- const { runCommand } = require('./command-executor');
5
- const { requireGitRepo, requireFileExists, requirePackageVersion } = require('./preflight');
2
+
3
+ const STABLE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
4
+
5
+ function parseStableVersion(version) {
6
+ const match = String(version || '').match(STABLE_VERSION_PATTERN);
7
+ if (!match) return null;
8
+ return match.slice(1).map(Number);
9
+ }
10
+
11
+ function compareVersions(left, right) {
12
+ const leftParts = parseStableVersion(left);
13
+ const rightParts = parseStableVersion(right);
14
+ if (!leftParts || !rightParts) {
15
+ throw new Error('Для сравнения требуются стабильные SemVer версии X.Y.Z.');
16
+ }
17
+
18
+ for (let index = 0; index < leftParts.length; index += 1) {
19
+ if (leftParts[index] !== rightParts[index]) {
20
+ return leftParts[index] - rightParts[index];
21
+ }
22
+ }
23
+ return 0;
24
+ }
6
25
 
7
26
  function upVersion(oldVersion, upType) {
8
- const [major, minor, patch] = oldVersion.split('.').map(Number);
9
-
27
+ const parts = parseStableVersion(oldVersion);
28
+ if (!parts) return null;
29
+ const [major, minor, patch] = parts;
30
+
10
31
  switch (upType) {
11
32
  case 'major':
12
33
  return `${major + 1}.0.0`;
@@ -18,39 +39,9 @@ function upVersion(oldVersion, upType) {
18
39
  }
19
40
  }
20
41
 
21
- function updateVersionFile(versionType) {
22
- try {
23
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
24
- const oldVersion = packageJson.version;
25
- const newVersion = upVersion(oldVersion, versionType);
26
-
27
- packageJson.version = newVersion;
28
- fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
29
- logSuccess('🔖', 'Версия обновлена с %s до %s.', oldVersion, newVersion);
30
- return true;
31
- } catch (error) {
32
- logError('❌', 'Ошибка при обновлении версии в package.json');
33
- return false;
34
- }
35
- }
36
-
37
- function setVersion(versionType) {
38
- return runCommand({
39
- name: `version up ${versionType}`,
40
- checks: [
41
- { name: 'git-repo', run: requireGitRepo },
42
- { name: 'package-json-exists', run: () => requireFileExists('package.json') },
43
- { name: 'package-version', run: requirePackageVersion },
44
- ],
45
- steps: [
46
- { name: 'update-package-version', run: () => updateVersionFile(versionType) }
47
- ]
48
- });
49
- }
50
-
51
-
52
42
  module.exports = {
53
- upVersion,
54
- updateVersionFile,
55
- setVersion
43
+ STABLE_VERSION_PATTERN,
44
+ parseStableVersion,
45
+ compareVersions,
46
+ upVersion
56
47
  };
package/src/UNRELEASED.md DELETED
@@ -1,33 +0,0 @@
1
- ## [Unreleased]
2
-
3
- ### 💥 Breaking change
4
-
5
- _Список обратно несовместимых изменений._
6
-
7
- ### 🆕 Added
8
-
9
- _Список новой функциональности._
10
-
11
- ### 🛠 Changed
12
-
13
- _Список изменившейся функциональности._
14
-
15
- ### 📜 Deprecated
16
-
17
- _Список устаревшей функциональности._
18
-
19
- ### 🗑 Removed
20
-
21
- _Список удаленной функциональности._
22
-
23
- ### 🪲 Fixed
24
-
25
- _Список исправлений багов._
26
-
27
- ### 🔐 Security
28
-
29
- _Список правок для обеспечения безопасности._
30
-
31
- ### 📦 Support
32
-
33
- _Список правок для обеспечения технической поддержки._