hereya-cli 0.55.0 → 0.57.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.
Files changed (35) hide show
  1. package/README.md +94 -36
  2. package/dist/backend/cloud/cloud-backend.d.ts +5 -1
  3. package/dist/backend/cloud/cloud-backend.js +208 -0
  4. package/dist/backend/common.d.ts +64 -2
  5. package/dist/backend/file.d.ts +5 -1
  6. package/dist/backend/file.js +24 -0
  7. package/dist/commands/add/index.js +2 -1
  8. package/dist/commands/deploy/index.js +50 -24
  9. package/dist/commands/doc/index.d.ts +17 -0
  10. package/dist/commands/doc/index.js +154 -0
  11. package/dist/commands/down/index.js +40 -11
  12. package/dist/commands/import/index.js +2 -1
  13. package/dist/commands/publish/index.d.ts +32 -0
  14. package/dist/commands/publish/index.js +239 -0
  15. package/dist/commands/remove/index.js +16 -4
  16. package/dist/commands/undeploy/index.js +45 -17
  17. package/dist/commands/up/index.js +36 -11
  18. package/dist/commands/workspace/install/index.js +1 -1
  19. package/dist/commands/workspace/uninstall/index.js +18 -5
  20. package/dist/executor/interface.d.ts +5 -0
  21. package/dist/executor/local.js +3 -3
  22. package/dist/infrastructure/index.d.ts +2 -0
  23. package/dist/infrastructure/index.js +8 -8
  24. package/dist/lib/config/common.d.ts +1 -0
  25. package/dist/lib/config/simple.js +2 -2
  26. package/dist/lib/package/cloud.d.ts +19 -0
  27. package/dist/lib/package/cloud.js +226 -0
  28. package/dist/lib/package/common.d.ts +1 -0
  29. package/dist/lib/package/github.d.ts +1 -1
  30. package/dist/lib/package/github.js +33 -6
  31. package/dist/lib/package/index.d.ts +9 -4
  32. package/dist/lib/package/index.js +178 -35
  33. package/dist/lib/package/local.js +1 -0
  34. package/oclif.manifest.json +92 -1
  35. package/package.json +8 -1
@@ -135,8 +135,9 @@ export default class Import extends Command {
135
135
  const configManager = getConfigManager();
136
136
  await configManager.addPackage({
137
137
  metadata: ctx.importOutput.metadata,
138
- package: ctx.package,
138
+ package: ctx.importOutput.pkgName,
139
139
  projectRootDir,
140
+ version: ctx.importOutput.version,
140
141
  });
141
142
  await delay(500);
142
143
  },
@@ -0,0 +1,32 @@
1
+ import { Command } from '@oclif/core';
2
+ interface HereyarcConfig {
3
+ description: string;
4
+ iac: string;
5
+ infra: string;
6
+ name: string;
7
+ onDeploy?: {
8
+ pkg: string;
9
+ version: string;
10
+ };
11
+ version: string;
12
+ visibility?: 'PRIVATE' | 'private' | 'PUBLIC' | 'public';
13
+ }
14
+ export default class Publish extends Command {
15
+ static description: string;
16
+ static examples: string[];
17
+ static flags: {
18
+ chdir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ };
20
+ calculateGitArchiveSha256(packageDir: string): Promise<string>;
21
+ displayPublishError(reason: string, config: HereyarcConfig): void;
22
+ getGitInfo(packageDir: string): Promise<{
23
+ commit: string;
24
+ repository: string;
25
+ sha256: string;
26
+ }>;
27
+ loadConfig(packageDir: string): Promise<HereyarcConfig>;
28
+ loadReadme(packageDir: string): string | undefined;
29
+ run(): Promise<void>;
30
+ validateConfig(config: HereyarcConfig): void;
31
+ }
32
+ export {};
@@ -0,0 +1,239 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { simpleGit } from 'simple-git';
5
+ import * as yaml from 'yaml';
6
+ import { getBackend } from '../../backend/index.js';
7
+ export default class Publish extends Command {
8
+ static description = 'Publish a package to the Hereya registry';
9
+ static examples = [
10
+ '$ hereya publish',
11
+ '$ hereya publish --chdir=/path/to/package',
12
+ ];
13
+ static flags = {
14
+ chdir: Flags.string({
15
+ description: `
16
+ Directory where the command will be executed.
17
+ If not specified, it defaults to the current working directory.
18
+ `,
19
+ required: false,
20
+ }),
21
+ };
22
+ async calculateGitArchiveSha256(packageDir) {
23
+ const { exec } = await import('node:child_process');
24
+ const { promisify } = await import('node:util');
25
+ const execAsync = promisify(exec);
26
+ try {
27
+ // Create a tar archive of HEAD and calculate its SHA256
28
+ const { stdout } = await execAsync('git archive --format=tar HEAD | shasum -a 256', {
29
+ cwd: packageDir,
30
+ encoding: 'utf8',
31
+ maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large repos
32
+ });
33
+ // The shasum output is "hash -" so we take the first part
34
+ const sha256 = stdout.trim().split(' ')[0];
35
+ if (!sha256 || sha256.length !== 64) {
36
+ this.error('Failed to calculate SHA256 hash of the repository');
37
+ }
38
+ return sha256;
39
+ }
40
+ catch (error) {
41
+ this.error(`Failed to calculate SHA256: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ }
44
+ displayPublishError(reason, config) {
45
+ this.log('\n❌ Failed to publish package\n');
46
+ // Handle specific error cases with helpful messages
47
+ if (reason.toLowerCase().includes('already exists')) {
48
+ this.log(`Package ${config.name}@${config.version} already exists in the registry.`);
49
+ this.log('\nPossible solutions:');
50
+ this.log(' • Increment the version number in hereyarc.yaml');
51
+ this.log(' • Use a different package name');
52
+ this.log(' • If you own this package, delete the existing version first');
53
+ }
54
+ else if (reason.toLowerCase().includes('permission')) {
55
+ this.log('You do not have permission to publish this package.');
56
+ this.log('\nPossible reasons:');
57
+ this.log(' • The package namespace is owned by another user/organization');
58
+ this.log(' • Your account does not have publishing privileges');
59
+ this.log(' • Contact the namespace owner for access');
60
+ }
61
+ else if (reason.toLowerCase().includes('authentication')) {
62
+ this.log('Authentication failed.');
63
+ this.log('\nPlease run: hereya login');
64
+ }
65
+ else if (reason.includes('\n')) {
66
+ // Multiple validation errors
67
+ this.log('Validation errors:');
68
+ const errors = reason.split('\n');
69
+ for (const error of errors) {
70
+ this.log(` • ${error}`);
71
+ }
72
+ }
73
+ else {
74
+ // Single error message
75
+ this.log(`Error: ${reason}`);
76
+ }
77
+ this.log(''); // Empty line for spacing
78
+ this.error('Package publication failed', { exit: 1 });
79
+ }
80
+ async getGitInfo(packageDir) {
81
+ const git = simpleGit(packageDir);
82
+ // Check if it's a git repository
83
+ const isRepo = await git.checkIsRepo();
84
+ if (!isRepo) {
85
+ this.error('The current directory is not a git repository');
86
+ }
87
+ // Get current commit SHA
88
+ const commit = await git.revparse(['HEAD']);
89
+ if (!commit) {
90
+ this.error('Could not get current commit SHA');
91
+ }
92
+ // Get repository URL
93
+ const remotes = await git.getRemotes(true);
94
+ if (remotes.length === 0) {
95
+ this.error('No git remotes found');
96
+ }
97
+ // Use origin remote by default, or the first one available
98
+ const origin = remotes.find((r) => r.name === 'origin') || remotes[0];
99
+ if (!origin || !origin.refs.fetch) {
100
+ this.error('Could not find repository URL');
101
+ }
102
+ let repository = origin.refs.fetch;
103
+ // Convert SSH URLs to HTTPS
104
+ if (repository.startsWith('git@github.com:')) {
105
+ repository = repository.replace('git@github.com:', 'https://github.com/');
106
+ }
107
+ // Remove .git suffix if present
108
+ if (repository.endsWith('.git')) {
109
+ repository = repository.slice(0, -4);
110
+ }
111
+ // Calculate SHA256 of the git archive
112
+ const sha256 = await this.calculateGitArchiveSha256(packageDir);
113
+ return { commit: commit.trim(), repository, sha256 };
114
+ }
115
+ async loadConfig(packageDir) {
116
+ // Look for hereyarc.yaml or hereyarc.yml
117
+ let configPath = join(packageDir, 'hereyarc.yaml');
118
+ if (!existsSync(configPath)) {
119
+ configPath = join(packageDir, 'hereyarc.yml');
120
+ if (!existsSync(configPath)) {
121
+ this.error('No hereyarc.yaml or hereyarc.yml file found in the current directory');
122
+ }
123
+ }
124
+ // Parse the configuration file
125
+ const configContent = readFileSync(configPath, 'utf8');
126
+ const config = yaml.parse(configContent);
127
+ // Validate required fields
128
+ this.validateConfig(config);
129
+ return config;
130
+ }
131
+ loadReadme(packageDir) {
132
+ // Check for README files in various formats
133
+ const readmeVariants = [
134
+ 'README.md',
135
+ 'readme.md',
136
+ 'Readme.md',
137
+ 'README.MD',
138
+ 'README',
139
+ 'readme',
140
+ 'README.txt',
141
+ 'readme.txt',
142
+ ];
143
+ for (const variant of readmeVariants) {
144
+ const readmePath = join(packageDir, variant);
145
+ if (existsSync(readmePath)) {
146
+ try {
147
+ const content = readFileSync(readmePath, 'utf8');
148
+ if (content.trim()) {
149
+ this.log(` README: Found ${variant}`);
150
+ return content;
151
+ }
152
+ }
153
+ catch {
154
+ // Ignore read errors and try next variant
155
+ continue;
156
+ }
157
+ }
158
+ }
159
+ this.log(' README: Not found (optional)');
160
+ return undefined;
161
+ }
162
+ async run() {
163
+ const { flags } = await this.parse(Publish);
164
+ const packageDir = flags.chdir || process.cwd();
165
+ try {
166
+ // Load and validate configuration
167
+ const config = await this.loadConfig(packageDir);
168
+ // Display package info
169
+ this.log(`\n📦 Preparing package ${config.name}@${config.version}`);
170
+ this.log(` Description: ${config.description}`);
171
+ this.log(` Infrastructure: ${config.infra} (${config.iac})`);
172
+ if (config.visibility) {
173
+ this.log(` Visibility: ${config.visibility}`);
174
+ }
175
+ // Get git information (including SHA256 of git archive)
176
+ this.log('\n🔍 Analyzing repository...');
177
+ const gitInfo = await this.getGitInfo(packageDir);
178
+ const { commit, repository, sha256 } = gitInfo;
179
+ this.log(` Repository: ${repository}`);
180
+ this.log(` Commit: ${commit.slice(0, 8)}`);
181
+ this.log(` SHA256: ${sha256.slice(0, 16)}...`);
182
+ // Load README content if available
183
+ const readmeContent = this.loadReadme(packageDir);
184
+ // Load backend configuration and check if it's cloud backend
185
+ this.log('\n🔐 Checking authentication...');
186
+ const backend = await getBackend();
187
+ if (!backend.publishPackage) {
188
+ this.log('\n❌ Authentication failed\n');
189
+ this.error('Publishing packages is only supported with the cloud backend. Please run `hereya login` first.');
190
+ }
191
+ this.log(' ✓ Authenticated with Hereya Cloud');
192
+ // Prepare the publish input (normalize visibility to uppercase)
193
+ const publishInput = {
194
+ commit: commit.trim(),
195
+ description: config.description,
196
+ doc: readmeContent,
197
+ iac: config.iac,
198
+ infra: config.infra,
199
+ name: config.name,
200
+ onDeployPkg: config.onDeploy?.pkg,
201
+ onDeployVersion: config.onDeploy?.version,
202
+ repository,
203
+ sha256,
204
+ version: config.version,
205
+ visibility: config.visibility?.toUpperCase(),
206
+ };
207
+ this.log('\n📤 Publishing to registry...');
208
+ // Publish the package
209
+ const result = await backend.publishPackage(publishInput);
210
+ if (!result.success) {
211
+ this.displayPublishError(result.reason, config);
212
+ return;
213
+ }
214
+ this.log(`\n✅ Successfully published ${result.package.name}@${result.package.version}`);
215
+ this.log(` Package ID: ${result.package.id}`);
216
+ this.log('');
217
+ }
218
+ catch (error) {
219
+ this.error(error instanceof Error ? error.message : String(error));
220
+ }
221
+ }
222
+ validateConfig(config) {
223
+ if (!config.name) {
224
+ this.error('Missing required field "name" in hereyarc.yaml');
225
+ }
226
+ if (!config.version) {
227
+ this.error('Missing required field "version" in hereyarc.yaml');
228
+ }
229
+ if (!config.description) {
230
+ this.error('Missing required field "description" in hereyarc.yaml');
231
+ }
232
+ if (!config.iac) {
233
+ this.error('Missing required field "iac" in hereyarc.yaml');
234
+ }
235
+ if (!config.infra) {
236
+ this.error('Missing required field "infra" in hereyarc.yaml');
237
+ }
238
+ }
239
+ }
@@ -68,9 +68,21 @@ export default class Remove extends Command {
68
68
  throw new Error(validation.message);
69
69
  }
70
70
  const { config } = loadConfigOutput;
71
- if (!(ctx.package in (config.packages ?? {})) && !(ctx.package in (config.deploy ?? {}))) {
72
- throw new Error(`Package ${ctx.package} not found in the project.`);
71
+ // Parse package name to extract name and version
72
+ const [packageNameWithoutVersion, userSpecifiedVersion] = ctx.package.split('@');
73
+ // Check if package exists in config (using clean name without version)
74
+ const packageInfo = config.packages?.[packageNameWithoutVersion] || config.deploy?.[packageNameWithoutVersion];
75
+ if (!packageInfo) {
76
+ throw new Error(`Package ${packageNameWithoutVersion} not found in the project.`);
73
77
  }
78
+ // Get the installed version from config
79
+ const installedVersion = packageInfo.version || '';
80
+ // If user specified a version, validate it matches the installed version
81
+ if (userSpecifiedVersion && userSpecifiedVersion !== installedVersion) {
82
+ throw new Error(`Package ${packageNameWithoutVersion} version mismatch: installed version is ${installedVersion || 'unspecified'}, but you specified ${userSpecifiedVersion}. ` +
83
+ `Please use 'hereya remove ${packageNameWithoutVersion}' to remove the installed version.`);
84
+ }
85
+ ctx.packageWithInstalledVersion = installedVersion ? `${packageNameWithoutVersion}@${installedVersion}` : packageNameWithoutVersion;
74
86
  await delay(500);
75
87
  },
76
88
  title: 'Loading project config',
@@ -102,7 +114,7 @@ export default class Remove extends Command {
102
114
  const { executor } = executor$;
103
115
  const destroyOutput = await executor.destroy({
104
116
  logger: getLogger(task),
105
- package: ctx.package,
117
+ package: ctx.packageWithInstalledVersion,
106
118
  parameters: ctx.parametersOutput.parameters,
107
119
  project: ctx.configOutput.config.project,
108
120
  projectRootDir,
@@ -134,7 +146,7 @@ export default class Remove extends Command {
134
146
  const configManager = getConfigManager();
135
147
  await configManager.removePackage({
136
148
  metadata: ctx.destroyOutput.metadata,
137
- package: ctx.package,
149
+ package: ctx.destroyOutput.pkgName,
138
150
  projectRootDir,
139
151
  });
140
152
  await delay(500);
@@ -98,14 +98,42 @@ export default class Undeploy extends Command {
98
98
  },
99
99
  {
100
100
  async task(ctx) {
101
- const deployPackages = Object.keys(ctx.configOutput.config.deploy ?? {});
102
- const savedDeployPackages = Object.keys(ctx.savedStateOutput?.config.deploy ?? {});
103
- const removedDeployPackages = savedDeployPackages.filter((packageName) => !deployPackages.includes(packageName));
104
- ctx.deployPackages = [...removedDeployPackages, ...deployPackages];
105
- const packages = Object.keys(ctx.configOutput.config.packages ?? {});
106
- const savedPackages = Object.keys(ctx.savedStateOutput?.config.packages ?? {});
107
- const removedPackages = savedPackages.filter((packageName) => !packages.includes(packageName));
108
- ctx.packages = [...removedPackages, ...packages];
101
+ // Get deploy packages with versions from config
102
+ const configDeployPackages = ctx.configOutput.config.deploy ?? {};
103
+ const deployPackagesWithVersions = Object.entries(configDeployPackages).map(([name, info]) => ({
104
+ name,
105
+ packageSpec: info.version ? `${name}@${info.version}` : name,
106
+ version: info.version || ''
107
+ }));
108
+ // Get removed deploy packages with versions from saved state
109
+ const savedDeployPackages = ctx.savedStateOutput?.config.deploy ?? {};
110
+ const currentDeployPackageNames = Object.keys(configDeployPackages);
111
+ const removedDeployPackages = Object.entries(savedDeployPackages)
112
+ .filter(([name]) => !currentDeployPackageNames.includes(name))
113
+ .map(([name, info]) => ({
114
+ name,
115
+ packageSpec: info.version ? `${name}@${info.version}` : name,
116
+ version: info.version || ''
117
+ }));
118
+ ctx.deployPackages = [...removedDeployPackages, ...deployPackagesWithVersions];
119
+ // Get regular packages with versions from config
120
+ const configPackages = ctx.configOutput.config.packages ?? {};
121
+ const packagesWithVersions = Object.entries(configPackages).map(([name, info]) => ({
122
+ name,
123
+ packageSpec: info.version ? `${name}@${info.version}` : name,
124
+ version: info.version || ''
125
+ }));
126
+ // Get removed packages with versions from saved state
127
+ const savedPackages = ctx.savedStateOutput?.config.packages ?? {};
128
+ const currentPackageNames = Object.keys(configPackages);
129
+ const removedPackages = Object.entries(savedPackages)
130
+ .filter(([name]) => !currentPackageNames.includes(name))
131
+ .map(([name, info]) => ({
132
+ name,
133
+ packageSpec: info.version ? `${name}@${info.version}` : name,
134
+ version: info.version || ''
135
+ }));
136
+ ctx.packages = [...removedPackages, ...packagesWithVersions];
109
137
  await delay(500);
110
138
  },
111
139
  title: 'Identifying removed packages',
@@ -113,7 +141,7 @@ export default class Undeploy extends Command {
113
141
  {
114
142
  skip: (ctx) => ctx.deployPackages.length === 0,
115
143
  async task(ctx, task) {
116
- return task.newListr(ctx.deployPackages.map((packageName) => ({
144
+ return task.newListr(ctx.deployPackages.map((pkg) => ({
117
145
  rendererOptions: {
118
146
  persistentOutput: isDebug(),
119
147
  },
@@ -122,7 +150,7 @@ export default class Undeploy extends Command {
122
150
  const backend = await getBackend();
123
151
  const profile = await getProfileFromWorkspace(backend, ctx.workspace, ctx.configOutput.config.project);
124
152
  const { parameters } = await parameterManager.getPackageParameters({
125
- package: packageName,
153
+ package: pkg.name,
126
154
  profile,
127
155
  projectRootDir,
128
156
  });
@@ -134,7 +162,7 @@ export default class Undeploy extends Command {
134
162
  const destroyOutput = await executor.destroy({
135
163
  isDeploying: true,
136
164
  logger: getLogger(task),
137
- package: packageName,
165
+ package: pkg.packageSpec,
138
166
  parameters,
139
167
  project: ctx.configOutput.config.project,
140
168
  projectEnv: ctx.projectEnv,
@@ -145,7 +173,7 @@ export default class Undeploy extends Command {
145
173
  throw new Error(destroyOutput.reason);
146
174
  }
147
175
  },
148
- title: `Destroying package ${packageName}`,
176
+ title: `Destroying package ${pkg.name}`,
149
177
  })), { concurrent: false, rendererOptions: { collapseSubtasks: !isDebug() } });
150
178
  },
151
179
  title: 'Destroying deployment packages',
@@ -153,7 +181,7 @@ export default class Undeploy extends Command {
153
181
  {
154
182
  skip: (ctx) => !ctx.packages || ctx.packages.length === 0,
155
183
  async task(ctx, task) {
156
- return task.newListr(ctx.packages.map((packageName) => ({
184
+ return task.newListr(ctx.packages.map((pkg) => ({
157
185
  rendererOptions: {
158
186
  persistentOutput: isDebug(),
159
187
  },
@@ -162,7 +190,7 @@ export default class Undeploy extends Command {
162
190
  const backend = await getBackend();
163
191
  const profile = await getProfileFromWorkspace(backend, ctx.workspace, ctx.configOutput.config.project);
164
192
  const { parameters } = await parameterManager.getPackageParameters({
165
- package: packageName,
193
+ package: pkg.name,
166
194
  profile,
167
195
  projectRootDir,
168
196
  });
@@ -174,7 +202,7 @@ export default class Undeploy extends Command {
174
202
  const destroyOutput = await executor.destroy({
175
203
  isDeploying: true,
176
204
  logger: getLogger(task),
177
- package: packageName,
205
+ package: pkg.packageSpec,
178
206
  parameters,
179
207
  project: ctx.configOutput.config.project,
180
208
  projectRootDir,
@@ -185,10 +213,10 @@ export default class Undeploy extends Command {
185
213
  }
186
214
  const { env, metadata } = destroyOutput;
187
215
  const output = ctx.destroyed || [];
188
- output.push({ env, metadata, packageName });
216
+ output.push({ env, metadata, packageName: pkg.name });
189
217
  ctx.destroyed = output;
190
218
  },
191
- title: `Destroying ${packageName}`,
219
+ title: `Destroying ${pkg.name}`,
192
220
  })), { concurrent: true, rendererOptions: { collapseSubtasks: !isDebug() } });
193
221
  },
194
222
  title: `Destroying packages`,
@@ -85,14 +85,39 @@ export default class Up extends Command {
85
85
  },
86
86
  {
87
87
  async task(ctx) {
88
- const packages = Object.keys(ctx.configOutput.config.packages ?? {});
88
+ // Get packages with their versions from config
89
+ const configPackages = ctx.configOutput.config.packages ?? {};
90
+ const packagesWithVersions = Object.entries(configPackages).map(([name, info]) => ({
91
+ name,
92
+ packageSpec: info.version ? `${name}@${info.version}` : name,
93
+ version: info.version || ''
94
+ }));
89
95
  const savedPackages = Object.keys(ctx.savedStateOutput?.config?.packages ?? {});
90
- const removedPackages = savedPackages.filter((packageName) => !packages.includes(packageName));
96
+ const removedPackages = savedPackages.filter((packageName) => !configPackages[packageName]);
91
97
  ctx.removedPackages = removedPackages;
92
- ctx.packages =
93
- flags.select.length > 0
94
- ? packages.filter((packageName) => flags.select.includes(packageName))
95
- : packages;
98
+ // Handle --select flag with version validation
99
+ if (flags.select.length > 0) {
100
+ const selectedPackages = [];
101
+ for (const selectedPkg of flags.select) {
102
+ // Parse the selected package to extract name and version
103
+ const [selectedName, selectedVersion] = selectedPkg.split('@');
104
+ // Find the package in the config
105
+ const configPkg = packagesWithVersions.find(pkg => pkg.name === selectedName);
106
+ if (!configPkg) {
107
+ throw new Error(`Package ${selectedName} not found in project`);
108
+ }
109
+ // If user specified a version, validate it matches the installed version
110
+ if (selectedVersion && selectedVersion !== configPkg.version) {
111
+ throw new Error(`Package ${selectedName} version mismatch: installed version is ${configPkg.version || 'unspecified'}, but you specified ${selectedVersion}. ` +
112
+ `Please use 'hereya up -s ${selectedName}' to use the installed version, or update the package to version ${selectedVersion} first.`);
113
+ }
114
+ selectedPackages.push(configPkg);
115
+ }
116
+ ctx.packages = selectedPackages;
117
+ }
118
+ else {
119
+ ctx.packages = packagesWithVersions;
120
+ }
96
121
  await delay(500);
97
122
  },
98
123
  title: 'Searching for removed packages',
@@ -150,7 +175,7 @@ export default class Up extends Command {
150
175
  if (!ctx.packages || ctx.packages.length === 0) {
151
176
  return;
152
177
  }
153
- return task.newListr(ctx.packages.map((packageName) => ({
178
+ return task.newListr(ctx.packages.map((pkg) => ({
154
179
  rendererOptions: {
155
180
  persistentOutput: isDebug(),
156
181
  },
@@ -159,7 +184,7 @@ export default class Up extends Command {
159
184
  const backend = await getBackend();
160
185
  const profile = await getProfileFromWorkspace(backend, ctx.workspace, ctx.configOutput.config.project);
161
186
  const { parameters } = await parameterManager.getPackageParameters({
162
- package: packageName,
187
+ package: pkg.name,
163
188
  profile,
164
189
  projectRootDir,
165
190
  });
@@ -171,7 +196,7 @@ export default class Up extends Command {
171
196
  const provisionOutput = await executor.provision({
172
197
  isDeploying: flags.deploy,
173
198
  logger: getLogger(task),
174
- package: packageName,
199
+ package: pkg.packageSpec,
175
200
  parameters,
176
201
  project: ctx.configOutput.config.project,
177
202
  projectRootDir,
@@ -182,10 +207,10 @@ export default class Up extends Command {
182
207
  }
183
208
  const { env, metadata } = provisionOutput;
184
209
  const output = ctx.added || [];
185
- output.push({ env, metadata, packageName });
210
+ output.push({ env, metadata, packageName: pkg.name });
186
211
  ctx.added = output;
187
212
  },
188
- title: `Provisioning ${packageName}`,
213
+ title: `Provisioning ${pkg.name}`,
189
214
  })), { concurrent: true, rendererOptions: { collapseSubtasks: !isDebug() } });
190
215
  },
191
216
  title: `Provisioning packages`,
@@ -106,7 +106,7 @@ export default class WorkspaceInstall extends Command {
106
106
  const output = await backend.addPackageToWorkspace({
107
107
  env,
108
108
  infra: metadata.infra,
109
- package: args.package,
109
+ package: ctx.provisionOutput.pkgName,
110
110
  parameters: ctx.parameters,
111
111
  workspace: flags.workspace,
112
112
  });
@@ -54,9 +54,20 @@ export default class WorkspaceUninstall extends Command {
54
54
  if (loadWorkspaceOutput.workspace.mirrorOf) {
55
55
  throw new Error(`Workspace ${flags.workspace} is a mirror of ${loadWorkspaceOutput.workspace.mirrorOf}`);
56
56
  }
57
- if (!(args.package in (loadWorkspaceOutput.workspace.packages ?? {}))) {
58
- throw new Error(`Package ${args.package} not found in workspace ${flags.workspace}`);
57
+ // Parse package name to extract name and version
58
+ const [packageNameWithoutVersion, userSpecifiedVersion] = args.package.split('@');
59
+ const packageInfo = loadWorkspaceOutput.workspace.packages?.[packageNameWithoutVersion];
60
+ if (!packageInfo) {
61
+ throw new Error(`Package ${packageNameWithoutVersion} not found in workspace ${flags.workspace}`);
59
62
  }
63
+ // Get the installed version from workspace
64
+ const installedVersion = packageInfo.version || '';
65
+ // If user specified a version, validate it matches the installed version
66
+ if (userSpecifiedVersion && userSpecifiedVersion !== installedVersion) {
67
+ throw new Error(`Package ${packageNameWithoutVersion} version mismatch: installed version is ${installedVersion || 'unspecified'}, but you specified ${userSpecifiedVersion}. ` +
68
+ `Please use 'hereya workspace uninstall ${packageNameWithoutVersion} -w ${flags.workspace}' to uninstall the installed version.`);
69
+ }
70
+ ctx.packageWithInstalledVersion = installedVersion ? `${packageNameWithoutVersion}@${installedVersion}` : packageNameWithoutVersion;
60
71
  ctx.workspace = loadWorkspaceOutput;
61
72
  await delay(500);
62
73
  },
@@ -73,8 +84,10 @@ export default class WorkspaceUninstall extends Command {
73
84
  }
74
85
  parametersFromFile = data;
75
86
  }
87
+ // Use the clean package name to get stored parameters
88
+ const packageNameWithoutVersion = args.package.split('@')[0];
76
89
  const parameters = {
77
- ...ctx.workspace.workspace.packages?.[args.package].parameters,
90
+ ...ctx.workspace.workspace.packages?.[packageNameWithoutVersion].parameters,
78
91
  ...parametersFromFile,
79
92
  ...parametersInCmdline,
80
93
  };
@@ -95,7 +108,7 @@ export default class WorkspaceUninstall extends Command {
95
108
  const { executor } = executor$;
96
109
  const destroyOutput = await executor.destroy({
97
110
  logger: getLogger(task),
98
- package: args.package,
111
+ package: ctx.packageWithInstalledVersion,
99
112
  parameters: ctx.parameters,
100
113
  workspace: flags.workspace,
101
114
  });
@@ -113,7 +126,7 @@ export default class WorkspaceUninstall extends Command {
113
126
  const output = await backend.removePackageFromWorkspace({
114
127
  env,
115
128
  infra: metadata.infra,
116
- package: args.package,
129
+ package: ctx.destroyOutput.pkgName,
117
130
  workspace: flags.workspace,
118
131
  });
119
132
  if (!output.success) {
@@ -21,7 +21,9 @@ export type ExecutorProvisionOutput = {
21
21
  [key: string]: string;
22
22
  };
23
23
  metadata: IPackageMetadata;
24
+ pkgName: string;
24
25
  success: true;
26
+ version?: string;
25
27
  } | {
26
28
  reason: string;
27
29
  success: false;
@@ -61,6 +63,7 @@ export type ExecutorUnsetEnvVarInput = {
61
63
  workspace: string;
62
64
  };
63
65
  export type ExecutorImportInput = {
66
+ logger?: Logger;
64
67
  package: string;
65
68
  project: string;
66
69
  projectRootDir?: string;
@@ -69,7 +72,9 @@ export type ExecutorImportInput = {
69
72
  };
70
73
  export type ExecutorImportOutput = {
71
74
  metadata: IPackageMetadata;
75
+ pkgName: string;
72
76
  success: true;
77
+ version?: string;
73
78
  } | {
74
79
  reason: string;
75
80
  success: false;