@git.zone/cli 2.2.2 → 2.6.1

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 (41) hide show
  1. package/assets/templates/npmextra/npmextra.json +4 -2
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.gitzoneconfig.js +10 -4
  4. package/dist_ts/gitzone.cli.js +8 -1
  5. package/dist_ts/mod_commit/index.js +109 -7
  6. package/dist_ts/mod_commit/mod.ui.d.ts +14 -0
  7. package/dist_ts/mod_commit/mod.ui.js +56 -2
  8. package/dist_ts/mod_config/classes.releaseconfig.d.ts +60 -0
  9. package/dist_ts/mod_config/classes.releaseconfig.js +131 -0
  10. package/dist_ts/mod_config/index.d.ts +3 -0
  11. package/dist_ts/mod_config/index.js +253 -0
  12. package/dist_ts/mod_config/mod.plugins.d.ts +2 -0
  13. package/dist_ts/mod_config/mod.plugins.js +4 -0
  14. package/dist_ts/mod_format/format.npmextra.js +33 -1
  15. package/dist_ts/mod_format/index.d.ts +6 -0
  16. package/dist_ts/mod_format/index.js +34 -1
  17. package/dist_ts/mod_standard/index.d.ts +1 -1
  18. package/dist_ts/mod_standard/index.js +75 -27
  19. package/npmextra.json +12 -5
  20. package/package.json +33 -23
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/classes.gitzoneconfig.ts +13 -5
  23. package/ts/gitzone.cli.ts +8 -0
  24. package/ts/mod_commit/index.ts +112 -8
  25. package/ts/mod_commit/mod.ui.ts +69 -1
  26. package/ts/mod_config/classes.releaseconfig.ts +166 -0
  27. package/ts/mod_config/index.ts +282 -0
  28. package/ts/mod_config/mod.plugins.ts +3 -0
  29. package/ts/mod_format/format.npmextra.ts +39 -0
  30. package/ts/mod_format/index.ts +42 -0
  31. package/ts/mod_standard/index.ts +78 -31
  32. package/dist_ts/gitzone.config.d.ts +0 -28
  33. package/dist_ts/gitzone.config.js +0 -21
  34. package/dist_ts/gitzone.monitor.d.ts +0 -1
  35. package/dist_ts/gitzone.monitor.js +0 -2
  36. package/dist_ts/gitzone.paths.d.ts +0 -4
  37. package/dist_ts/gitzone.paths.js +0 -6
  38. package/dist_ts/gitzone.plugins.d.ts +0 -10
  39. package/dist_ts/gitzone.plugins.js +0 -11
  40. package/dist_ts/mod_format/format.classes.project.d.ts +0 -8
  41. package/dist_ts/mod_format/format.classes.project.js +0 -20
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@git.zone/cli",
3
3
  "private": false,
4
- "version": "2.2.2",
4
+ "version": "2.6.1",
5
5
  "description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
6
6
  "main": "dist_ts/index.ts",
7
7
  "typings": "dist_ts/index.d.ts",
@@ -10,6 +10,24 @@
10
10
  "gitzone": "./cli.js",
11
11
  "gzone": "./cli.js"
12
12
  },
13
+ "scripts": {
14
+ "test": "(npm run clean && npm run prepareTest && npm run testCli && npm run testFormat && npm run testCommit && npm run testDeprecate && npm run testVersion && npm run testReadme && npm run testUpdate && npm run testTemplateNpm && npm run testTemplateLit) && rm -rf test",
15
+ "build": "tsbuild tsfolders",
16
+ "clean": "(rm -rf test/)",
17
+ "prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)",
18
+ "testBuild": "npm run build && rm -r dist/",
19
+ "testCli": "(cd test && node ../cli.ts.js)",
20
+ "testCommit": "(cd test && node ../cli.ts.js commit)",
21
+ "testDeprecate": "(cd test && node ../cli.ts.js deprecate)",
22
+ "testOpen": "(cd test && node ../cli.ts.js open ci)",
23
+ "testReadme": "(cd test && node ../cli.ts.js readme)",
24
+ "testFormat": "(cd test && node ../cli.ts.js format)",
25
+ "testTemplateNpm": "(rm -rf test/testtemplate_npm/ && mkdir test/testtemplate_npm && cd test/testtemplate_npm && node ../../cli.ts.js template npm)",
26
+ "testTemplateLit": "(rm -rf test/testtemplate_lit/ && mkdir test/testtemplate_lit && cd test/testtemplate_lit && node ../../cli.ts.js template lit)",
27
+ "testUpdate": "(cd test && node ../cli.ts.js update)",
28
+ "testVersion": "(cd test && node ../cli.ts.js -v)",
29
+ "buildDocs": "tsdoc"
30
+ },
13
31
  "repository": {
14
32
  "type": "git",
15
33
  "url": "https://gitlab.com/gitzone/private/gitzone.git"
@@ -39,14 +57,14 @@
39
57
  },
40
58
  "homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
41
59
  "devDependencies": {
42
- "@git.zone/tsbuild": "^4.0.1",
60
+ "@git.zone/tsbuild": "^4.0.2",
43
61
  "@git.zone/tsrun": "^2.0.1",
44
62
  "@git.zone/tstest": "^3.1.3",
45
63
  "@push.rocks/smartdelay": "^3.0.5",
46
64
  "@push.rocks/smartinteract": "^2.0.16",
47
65
  "@push.rocks/smartnetwork": "^4.4.0",
48
66
  "@push.rocks/smartshell": "^3.3.0",
49
- "@types/node": "^24.10.3"
67
+ "@types/node": "^25.0.2"
50
68
  },
51
69
  "dependencies": {
52
70
  "@git.zone/tsdoc": "^1.10.2",
@@ -62,7 +80,7 @@
62
80
  "@push.rocks/smartfile": "^13.1.2",
63
81
  "@push.rocks/smartfs": "^1.2.0",
64
82
  "@push.rocks/smartgulp": "^3.0.4",
65
- "@push.rocks/smartjson": "^5.2.0",
83
+ "@push.rocks/smartjson": "^6.0.0",
66
84
  "@push.rocks/smartlegal": "^1.0.27",
67
85
  "@push.rocks/smartlog": "^3.1.10",
68
86
  "@push.rocks/smartlog-destination-local": "^9.0.2",
@@ -95,22 +113,14 @@
95
113
  "browserslist": [
96
114
  "last 1 chrome versions"
97
115
  ],
98
- "scripts": {
99
- "test": "(npm run clean && npm run prepareTest && npm run testCli && npm run testFormat && npm run testCommit && npm run testDeprecate && npm run testVersion && npm run testReadme && npm run testUpdate && npm run testTemplateNpm && npm run testTemplateLit) && rm -rf test",
100
- "build": "tsbuild tsfolders",
101
- "clean": "(rm -rf test/)",
102
- "prepareTest": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)",
103
- "testBuild": "npm run build && rm -r dist/",
104
- "testCli": "(cd test && node ../cli.ts.js)",
105
- "testCommit": "(cd test && node ../cli.ts.js commit)",
106
- "testDeprecate": "(cd test && node ../cli.ts.js deprecate)",
107
- "testOpen": "(cd test && node ../cli.ts.js open ci)",
108
- "testReadme": "(cd test && node ../cli.ts.js readme)",
109
- "testFormat": "(cd test && node ../cli.ts.js format)",
110
- "testTemplateNpm": "(rm -rf test/testtemplate_npm/ && mkdir test/testtemplate_npm && cd test/testtemplate_npm && node ../../cli.ts.js template npm)",
111
- "testTemplateLit": "(rm -rf test/testtemplate_lit/ && mkdir test/testtemplate_lit && cd test/testtemplate_lit && node ../../cli.ts.js template lit)",
112
- "testUpdate": "(cd test && node ../cli.ts.js update)",
113
- "testVersion": "(cd test && node ../cli.ts.js -v)",
114
- "buildDocs": "tsdoc"
115
- }
116
- }
116
+ "pnpm": {
117
+ "overrides": {},
118
+ "onlyBuiltDependencies": [
119
+ "esbuild",
120
+ "mongodb-memory-server",
121
+ "puppeteer",
122
+ "sharp"
123
+ ]
124
+ },
125
+ "packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
126
+ }
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/cli',
6
- version: '2.2.1',
6
+ version: '2.6.1',
7
7
  description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
8
8
  }
@@ -40,11 +40,19 @@ export class GitzoneConfig {
40
40
  public async readConfigFromCwd() {
41
41
  const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd);
42
42
  this.data = npmextraInstance.dataFor<IGitzoneConfigData>('@git.zone/cli', {});
43
- this.data.npmciOptions = npmextraInstance.dataFor<
44
- IGitzoneConfigData['npmciOptions']
45
- >('@ship.zone/szci', {
46
- npmAccessLevel: 'public',
47
- });
43
+
44
+ // Read szci config for backward compatibility
45
+ const szciConfig = npmextraInstance.dataFor<any>('@ship.zone/szci', {});
46
+
47
+ // Prefer accessLevel from @git.zone/cli.release, fallback to @ship.zone/szci.npmAccessLevel
48
+ const accessLevel =
49
+ (this.data as any)?.release?.accessLevel ||
50
+ szciConfig?.npmAccessLevel ||
51
+ 'public';
52
+
53
+ this.data.npmciOptions = {
54
+ npmAccessLevel: accessLevel,
55
+ };
48
56
  }
49
57
 
50
58
  constructor() {}
package/ts/gitzone.cli.ts CHANGED
@@ -131,6 +131,14 @@ export let run = async () => {
131
131
  modHelpers.run(argvArg);
132
132
  });
133
133
 
134
+ /**
135
+ * manage release configuration
136
+ */
137
+ gitzoneSmartcli.addCommand('config').subscribe(async (argvArg) => {
138
+ const modConfig = await import('./mod_config/index.js');
139
+ await modConfig.run(argvArg);
140
+ });
141
+
134
142
  /**
135
143
  * manage development services (MongoDB, S3/MinIO)
136
144
  */
@@ -5,8 +5,35 @@ import * as paths from '../paths.js';
5
5
  import { logger } from '../gitzone.logging.js';
6
6
  import * as helpers from './mod.helpers.js';
7
7
  import * as ui from './mod.ui.js';
8
+ import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js';
8
9
 
9
10
  export const run = async (argvArg: any) => {
11
+ // Check if release flag is set and validate registries early
12
+ const wantsRelease = !!(argvArg.r || argvArg.release);
13
+ const wantsBuild = !!(argvArg.b || argvArg.build);
14
+ let releaseConfig: ReleaseConfig | null = null;
15
+
16
+ if (wantsRelease) {
17
+ releaseConfig = await ReleaseConfig.fromCwd();
18
+ if (!releaseConfig.hasRegistries()) {
19
+ logger.log('error', 'No release registries configured.');
20
+ console.log('');
21
+ console.log(' Run `gitzone config add <registry-url>` to add registries.');
22
+ console.log('');
23
+ process.exit(1);
24
+ }
25
+ }
26
+
27
+ // Print execution plan at the start
28
+ ui.printExecutionPlan({
29
+ autoAccept: !!(argvArg.y || argvArg.yes),
30
+ push: !!(argvArg.p || argvArg.push),
31
+ build: wantsBuild,
32
+ release: wantsRelease,
33
+ format: !!argvArg.format,
34
+ registries: releaseConfig?.getRegistries(),
35
+ });
36
+
10
37
  if (argvArg.format) {
11
38
  const formatMod = await import('../mod_format/index.js');
12
39
  await formatMod.run();
@@ -56,6 +83,10 @@ export const run = async (argvArg: any) => {
56
83
  name: 'pushToOrigin',
57
84
  value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
58
85
  });
86
+ answerBucket.addAnswer({
87
+ name: 'createRelease',
88
+ value: wantsRelease,
89
+ });
59
90
  } else {
60
91
  // Warn if --yes was provided but we're requiring confirmation due to breaking change
61
92
  if (isBreakingChange && (argvArg.y || argvArg.yes)) {
@@ -89,6 +120,12 @@ export const run = async (argvArg: any) => {
89
120
  message: `Do you want to push this version now?`,
90
121
  default: true,
91
122
  },
123
+ {
124
+ type: 'confirm',
125
+ name: `createRelease`,
126
+ message: `Do you want to publish to npm registries?`,
127
+ default: wantsRelease,
128
+ },
92
129
  ]);
93
130
  answerBucket = await commitInteract.runQueue();
94
131
  }
@@ -111,8 +148,25 @@ export const run = async (argvArg: any) => {
111
148
  sourceFilePaths: [],
112
149
  });
113
150
 
114
- // Determine total steps (6 if pushing, 5 if not)
115
- const totalSteps = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true') ? 6 : 5;
151
+ // Load release config if user wants to release (interactively selected)
152
+ if (answerBucket.getAnswerFor('createRelease') && !releaseConfig) {
153
+ releaseConfig = await ReleaseConfig.fromCwd();
154
+ if (!releaseConfig.hasRegistries()) {
155
+ logger.log('error', 'No release registries configured.');
156
+ console.log('');
157
+ console.log(' Run `gitzone config add <registry-url>` to add registries.');
158
+ console.log('');
159
+ process.exit(1);
160
+ }
161
+ }
162
+
163
+ // Determine total steps based on options
164
+ const willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true');
165
+ const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries();
166
+ let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
167
+ if (wantsBuild) totalSteps += 2; // build step + verification step
168
+ if (willPush) totalSteps++;
169
+ if (willRelease) totalSteps++;
116
170
  let currentStep = 0;
117
171
 
118
172
  // Step 1: Baking commitinfo
@@ -173,18 +227,66 @@ export const run = async (argvArg: any) => {
173
227
  const projectType = await helpers.detectProjectType();
174
228
  const newVersion = await helpers.bumpProjectVersion(projectType, commitVersionType, currentStep, totalSteps);
175
229
 
176
- // Step 6: Push to remote (optional)
230
+ // Step 6: Run build (optional)
231
+ if (wantsBuild) {
232
+ currentStep++;
233
+ ui.printStep(currentStep, totalSteps, '🔨 Running build', 'in-progress');
234
+ const buildResult = await smartshellInstance.exec('pnpm build');
235
+ if (buildResult.exitCode !== 0) {
236
+ ui.printStep(currentStep, totalSteps, '🔨 Running build', 'error');
237
+ logger.log('error', 'Build failed. Aborting release.');
238
+ process.exit(1);
239
+ }
240
+ ui.printStep(currentStep, totalSteps, '🔨 Running build', 'done');
241
+
242
+ // Step 7: Verify no uncommitted changes
243
+ currentStep++;
244
+ ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'in-progress');
245
+ const statusResult = await smartshellInstance.exec('git status --porcelain');
246
+ if (statusResult.stdout.trim() !== '') {
247
+ ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'error');
248
+ logger.log('error', 'Build produced uncommitted changes. This usually means build output is not gitignored.');
249
+ logger.log('error', 'Uncommitted files:');
250
+ console.log(statusResult.stdout);
251
+ logger.log('error', 'Aborting release. Please ensure build artifacts are in .gitignore');
252
+ process.exit(1);
253
+ }
254
+ ui.printStep(currentStep, totalSteps, '🔍 Verifying clean working tree', 'done');
255
+ }
256
+
257
+ // Step: Push to remote (optional)
177
258
  const currentBranch = await helpers.detectCurrentBranch();
178
- if (
179
- answerBucket.getAnswerFor('pushToOrigin') &&
180
- !(process.env.CI === 'true')
181
- ) {
259
+ if (willPush) {
182
260
  currentStep++;
183
261
  ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress');
184
262
  await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`);
185
263
  ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'done');
186
264
  }
187
265
 
266
+ // Step 7: Publish to npm registries (optional)
267
+ let releasedRegistries: string[] = [];
268
+ if (willRelease && releaseConfig) {
269
+ currentStep++;
270
+ const registries = releaseConfig.getRegistries();
271
+ ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'in-progress');
272
+
273
+ const accessLevel = releaseConfig.getAccessLevel();
274
+ for (const registry of registries) {
275
+ try {
276
+ await smartshellInstance.exec(`npm publish --registry=${registry} --access=${accessLevel}`);
277
+ releasedRegistries.push(registry);
278
+ } catch (error) {
279
+ logger.log('error', `Failed to publish to ${registry}: ${error}`);
280
+ }
281
+ }
282
+
283
+ if (releasedRegistries.length === registries.length) {
284
+ ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'done');
285
+ } else {
286
+ ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'error');
287
+ }
288
+ }
289
+
188
290
  console.log(''); // Add spacing before summary
189
291
 
190
292
  // Get commit SHA for summary
@@ -200,7 +302,9 @@ export const run = async (argvArg: any) => {
200
302
  commitMessage: answerBucket.getAnswerFor('commitDescription'),
201
303
  newVersion: newVersion,
202
304
  commitSha: commitSha,
203
- pushed: answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true'),
305
+ pushed: willPush,
306
+ released: releasedRegistries.length > 0,
307
+ releasedRegistries: releasedRegistries.length > 0 ? releasedRegistries : undefined,
204
308
  });
205
309
  };
206
310
 
@@ -14,6 +14,17 @@ interface ICommitSummary {
14
14
  commitSha?: string;
15
15
  pushed: boolean;
16
16
  repoUrl?: string;
17
+ released?: boolean;
18
+ releasedRegistries?: string[];
19
+ }
20
+
21
+ interface IExecutionPlanOptions {
22
+ autoAccept: boolean;
23
+ push: boolean;
24
+ build: boolean;
25
+ release: boolean;
26
+ format: boolean;
27
+ registries?: string[];
17
28
  }
18
29
 
19
30
  interface IRecommendation {
@@ -39,6 +50,54 @@ export function printHeader(title: string): void {
39
50
  console.log('');
40
51
  }
41
52
 
53
+ /**
54
+ * Print execution plan at the start of commit
55
+ */
56
+ export function printExecutionPlan(options: IExecutionPlanOptions): void {
57
+ console.log('');
58
+ console.log('╭───────────────────────────────────────────────────────────────╮');
59
+ console.log('│ gitzone commit - Execution Plan │');
60
+ console.log('╰───────────────────────────────────────────────────────────────╯');
61
+ console.log('');
62
+
63
+ // Show active options
64
+ console.log(' Options:');
65
+ console.log(` Auto-accept ${options.autoAccept ? '✓ enabled (-y)' : '○ interactive mode'}`);
66
+ console.log(` Push to remote ${options.push ? '✓ enabled (-p)' : '○ disabled'}`);
67
+ console.log(` Build & verify ${options.build ? '✓ enabled (-b)' : '○ disabled'}`);
68
+ console.log(` Release to npm ${options.release ? '✓ enabled (-r)' : '○ disabled'}`);
69
+ if (options.format) {
70
+ console.log(` Format first ✓ enabled (--format)`);
71
+ }
72
+ console.log('');
73
+
74
+ // Show steps
75
+ console.log(' Steps:');
76
+ let stepNum = 1;
77
+ if (options.format) {
78
+ console.log(` ${stepNum++}. Format project files`);
79
+ }
80
+ console.log(` ${stepNum++}. Analyze repository changes`);
81
+ console.log(` ${stepNum++}. Bake commit info into code`);
82
+ console.log(` ${stepNum++}. Generate changelog.md`);
83
+ console.log(` ${stepNum++}. Stage and commit files`);
84
+ console.log(` ${stepNum++}. Bump version`);
85
+ if (options.build) {
86
+ console.log(` ${stepNum++}. Run build`);
87
+ console.log(` ${stepNum++}. Verify clean working tree`);
88
+ }
89
+ if (options.push) {
90
+ console.log(` ${stepNum++}. Push to remote`);
91
+ }
92
+ if (options.release) {
93
+ console.log(` ${stepNum++}. Publish to npm registries`);
94
+ if (options.registries && options.registries.length > 0) {
95
+ options.registries.forEach(r => console.log(` → ${r}`));
96
+ }
97
+ }
98
+ console.log('');
99
+ }
100
+
42
101
  /**
43
102
  * Print a section with a border
44
103
  */
@@ -146,6 +205,13 @@ export function printSummary(summary: ICommitSummary): void {
146
205
  lines.push(`Remote: ⊘ Not pushed (local only)`);
147
206
  }
148
207
 
208
+ if (summary.released && summary.releasedRegistries && summary.releasedRegistries.length > 0) {
209
+ lines.push(`Published: ✓ Released to ${summary.releasedRegistries.length} registr${summary.releasedRegistries.length === 1 ? 'y' : 'ies'}`);
210
+ summary.releasedRegistries.forEach((registry) => {
211
+ lines.push(` → ${registry}`);
212
+ });
213
+ }
214
+
149
215
  if (summary.repoUrl && summary.commitSha) {
150
216
  lines.push('');
151
217
  lines.push(`View at: ${summary.repoUrl}/commit/${summary.commitSha}`);
@@ -153,7 +219,9 @@ export function printSummary(summary: ICommitSummary): void {
153
219
 
154
220
  printSection('✅ Commit Summary', lines);
155
221
 
156
- if (summary.pushed) {
222
+ if (summary.released) {
223
+ console.log('🎉 All done! Your changes are committed, pushed, and released.\n');
224
+ } else if (summary.pushed) {
157
225
  console.log('🎉 All done! Your changes are committed and pushed.\n');
158
226
  } else {
159
227
  console.log('✓ Commit created successfully.\n');
@@ -0,0 +1,166 @@
1
+ import * as plugins from './mod.plugins.js';
2
+
3
+ export type TAccessLevel = 'public' | 'private';
4
+
5
+ export interface IReleaseConfig {
6
+ registries: string[];
7
+ accessLevel: TAccessLevel;
8
+ }
9
+
10
+ /**
11
+ * Manages release configuration stored in npmextra.json
12
+ * under @git.zone/cli.release namespace
13
+ */
14
+ export class ReleaseConfig {
15
+ private cwd: string;
16
+ private config: IReleaseConfig;
17
+
18
+ constructor(cwd: string = process.cwd()) {
19
+ this.cwd = cwd;
20
+ this.config = { registries: [], accessLevel: 'public' };
21
+ }
22
+
23
+ /**
24
+ * Create a ReleaseConfig instance from current working directory
25
+ */
26
+ public static async fromCwd(cwd: string = process.cwd()): Promise<ReleaseConfig> {
27
+ const instance = new ReleaseConfig(cwd);
28
+ await instance.load();
29
+ return instance;
30
+ }
31
+
32
+ /**
33
+ * Load configuration from npmextra.json
34
+ */
35
+ public async load(): Promise<void> {
36
+ const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
37
+ const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
38
+
39
+ // Also check szci for backward compatibility
40
+ const szciConfig = npmextraInstance.dataFor<any>('@ship.zone/szci', {});
41
+
42
+ this.config = {
43
+ registries: gitzoneConfig?.release?.registries || [],
44
+ accessLevel: gitzoneConfig?.release?.accessLevel || szciConfig?.npmAccessLevel || 'public',
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Save configuration to npmextra.json
50
+ */
51
+ public async save(): Promise<void> {
52
+ const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json');
53
+ let npmextraData: any = {};
54
+
55
+ // Read existing npmextra.json
56
+ if (await plugins.smartfs.file(npmextraPath).exists()) {
57
+ const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
58
+ npmextraData = JSON.parse(content as string);
59
+ }
60
+
61
+ // Ensure @git.zone/cli namespace exists
62
+ if (!npmextraData['@git.zone/cli']) {
63
+ npmextraData['@git.zone/cli'] = {};
64
+ }
65
+
66
+ // Ensure release object exists
67
+ if (!npmextraData['@git.zone/cli'].release) {
68
+ npmextraData['@git.zone/cli'].release = {};
69
+ }
70
+
71
+ // Update registries and accessLevel
72
+ npmextraData['@git.zone/cli'].release.registries = this.config.registries;
73
+ npmextraData['@git.zone/cli'].release.accessLevel = this.config.accessLevel;
74
+
75
+ // Write back to file
76
+ await plugins.smartfs
77
+ .file(npmextraPath)
78
+ .encoding('utf8')
79
+ .write(JSON.stringify(npmextraData, null, 2));
80
+ }
81
+
82
+ /**
83
+ * Get all configured registries
84
+ */
85
+ public getRegistries(): string[] {
86
+ return [...this.config.registries];
87
+ }
88
+
89
+ /**
90
+ * Check if any registries are configured
91
+ */
92
+ public hasRegistries(): boolean {
93
+ return this.config.registries.length > 0;
94
+ }
95
+
96
+ /**
97
+ * Add a registry URL
98
+ * @returns true if added, false if already exists
99
+ */
100
+ public addRegistry(url: string): boolean {
101
+ const normalizedUrl = this.normalizeUrl(url);
102
+
103
+ if (this.config.registries.includes(normalizedUrl)) {
104
+ return false;
105
+ }
106
+
107
+ this.config.registries.push(normalizedUrl);
108
+ return true;
109
+ }
110
+
111
+ /**
112
+ * Remove a registry URL
113
+ * @returns true if removed, false if not found
114
+ */
115
+ public removeRegistry(url: string): boolean {
116
+ const normalizedUrl = this.normalizeUrl(url);
117
+ const index = this.config.registries.indexOf(normalizedUrl);
118
+
119
+ if (index === -1) {
120
+ return false;
121
+ }
122
+
123
+ this.config.registries.splice(index, 1);
124
+ return true;
125
+ }
126
+
127
+ /**
128
+ * Clear all registries
129
+ */
130
+ public clearRegistries(): void {
131
+ this.config.registries = [];
132
+ }
133
+
134
+ /**
135
+ * Get the npm access level
136
+ */
137
+ public getAccessLevel(): TAccessLevel {
138
+ return this.config.accessLevel;
139
+ }
140
+
141
+ /**
142
+ * Set the npm access level
143
+ */
144
+ public setAccessLevel(level: TAccessLevel): void {
145
+ this.config.accessLevel = level;
146
+ }
147
+
148
+ /**
149
+ * Normalize a registry URL (ensure it has https:// prefix)
150
+ */
151
+ private normalizeUrl(url: string): string {
152
+ let normalized = url.trim();
153
+
154
+ // Add https:// if no protocol specified
155
+ if (!normalized.startsWith('http://') && !normalized.startsWith('https://')) {
156
+ normalized = `https://${normalized}`;
157
+ }
158
+
159
+ // Remove trailing slash
160
+ if (normalized.endsWith('/')) {
161
+ normalized = normalized.slice(0, -1);
162
+ }
163
+
164
+ return normalized;
165
+ }
166
+ }