hereya-cli 0.56.0 → 0.57.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.
@@ -0,0 +1,226 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileExists, isNotEmpty } from '../filesystem.js';
6
+ export class CloudPackageManager {
7
+ backend;
8
+ logger;
9
+ constructor(backend, logger) {
10
+ this.backend = backend;
11
+ this.logger = logger;
12
+ }
13
+ async downloadAndValidatePackage(registryPackage, destPath) {
14
+ if (!registryPackage.repository) {
15
+ throw new Error(`No repository URL for package ${registryPackage.name}`);
16
+ }
17
+ if (await isNotEmpty(destPath)) {
18
+ return destPath;
19
+ }
20
+ await fs.mkdir(destPath, { recursive: true });
21
+ const tmpFolder = path.join(os.homedir(), '.hereya', 'downloads', randomUUID());
22
+ try {
23
+ // Use simple-git to clone the repository at specific commit
24
+ const { simpleGit } = await import('simple-git');
25
+ const git = simpleGit();
26
+ if (registryPackage.commit) {
27
+ // Clone repository and checkout specific commit
28
+ await git.clone(registryPackage.repository, tmpFolder, ['--no-checkout']);
29
+ const repoGit = simpleGit(tmpFolder);
30
+ await repoGit.checkout(registryPackage.commit);
31
+ }
32
+ else {
33
+ // Clone repository at default branch
34
+ await git.clone(registryPackage.repository, tmpFolder, ['--depth=1']);
35
+ }
36
+ // If checksum is provided, validate it
37
+ if (registryPackage.sha256) {
38
+ // Create tar archive from the cloned repository for checksum validation
39
+ const repoGit = simpleGit(tmpFolder);
40
+ const archiveBuffer = await repoGit.raw(['archive', '--format=tar', registryPackage.commit || 'HEAD']);
41
+ const hash = createHash('sha256');
42
+ hash.update(archiveBuffer);
43
+ const actualChecksum = hash.digest('hex');
44
+ if (actualChecksum !== registryPackage.sha256) {
45
+ throw new Error(`Checksum mismatch for package ${registryPackage.name}@${registryPackage.version}. Expected: ${registryPackage.sha256}, Got: ${actualChecksum}`);
46
+ }
47
+ }
48
+ // Move files from temp folder to destination
49
+ const files = await fs.readdir(tmpFolder);
50
+ await Promise.all(files.map(async (file) => {
51
+ if (file !== '.git') {
52
+ await fs.rename(path.join(tmpFolder, file), path.join(destPath, file));
53
+ }
54
+ }));
55
+ // Clean up temp folder
56
+ await fs.rm(tmpFolder, { force: true, recursive: true });
57
+ }
58
+ catch (error) {
59
+ // Clean up on error
60
+ try {
61
+ await fs.rm(tmpFolder, { force: true, recursive: true });
62
+ }
63
+ catch {
64
+ // Ignore cleanup errors
65
+ }
66
+ throw error;
67
+ }
68
+ return destPath;
69
+ }
70
+ async downloadPackage(pkgUrl, destPath) {
71
+ // Parse the package URL to extract repository and commit
72
+ // Format: <repository_url>#<commit_sha>
73
+ const [repoUrl, commitSha] = pkgUrl.split('#');
74
+ // For downloading packages during provision/deploy, we use downloadAndValidatePackage
75
+ // but we don't have SHA256 here. The validation happens in getRepoContent instead
76
+ // where we have access to the full registry metadata
77
+ const registryPackage = {
78
+ commit: commitSha,
79
+ description: '',
80
+ id: '',
81
+ name: '',
82
+ repository: repoUrl,
83
+ version: '',
84
+ // SHA256 not available here - validation happens in getRepoContent
85
+ };
86
+ return this.downloadAndValidatePackage(registryPackage, destPath);
87
+ }
88
+ async getFileFromRepository(repoUrl, commit, filePath, sha256) {
89
+ this.logger?.debug(`getFileFromRepository: trying to get ${filePath} from ${repoUrl}${commit ? `@${commit}` : ''}`);
90
+ const tmpFolder = path.join(os.homedir(), '.hereya', 'downloads', randomUUID());
91
+ try {
92
+ const { simpleGit } = await import('simple-git');
93
+ const git = simpleGit();
94
+ this.logger?.debug(`getFileFromRepository: cloning ${repoUrl}...`);
95
+ if (commit) {
96
+ // Clone repository and checkout specific commit
97
+ await git.clone(repoUrl, tmpFolder, ['--no-checkout']);
98
+ const repoGit = simpleGit(tmpFolder);
99
+ await repoGit.checkout(commit);
100
+ // Validate checksum if provided (for cloud packages)
101
+ if (sha256) {
102
+ this.logger?.debug('Validating package checksum...');
103
+ const archiveBuffer = await repoGit.raw(['archive', '--format=tar', commit]);
104
+ const hash = createHash('sha256');
105
+ hash.update(archiveBuffer);
106
+ const actualChecksum = hash.digest('hex');
107
+ if (actualChecksum !== sha256) {
108
+ await fs.rm(tmpFolder, { recursive: true });
109
+ return {
110
+ found: false,
111
+ reason: `Checksum validation failed for repository ${repoUrl}@${commit}. Expected: ${sha256}, Got: ${actualChecksum}. This may indicate the package has been tampered with or corrupted.`,
112
+ };
113
+ }
114
+ this.logger?.debug('Checksum validation successful');
115
+ }
116
+ }
117
+ else {
118
+ // Clone repository at default branch
119
+ await git.clone(repoUrl, tmpFolder, ['--depth=1']);
120
+ }
121
+ const fullFilePath = path.join(tmpFolder, filePath);
122
+ this.logger?.debug(`getFileFromRepository: checking if file exists at ${fullFilePath}`);
123
+ if (await fileExists(fullFilePath)) {
124
+ this.logger?.debug(`getFileFromRepository: file found, reading content...`);
125
+ const content = await fs.readFile(fullFilePath, 'utf8');
126
+ await fs.rm(tmpFolder, { recursive: true });
127
+ return {
128
+ content,
129
+ found: true,
130
+ pkgUrl: commit ? `${repoUrl}#${commit}` : repoUrl,
131
+ };
132
+ }
133
+ // List files to debug what's actually in the package
134
+ const files = await fs.readdir(tmpFolder);
135
+ this.logger?.debug(`getFileFromRepository: file ${filePath} not found. Available files: ${files.join(', ')}`);
136
+ await fs.rm(tmpFolder, { recursive: true });
137
+ return {
138
+ found: false,
139
+ reason: `File '${filePath}' not found in repository '${repoUrl}'. Available files: ${files.join(', ')}. This package may not be compatible with hereya or may be missing required metadata files.`,
140
+ };
141
+ }
142
+ catch (error) {
143
+ // Clean up on error
144
+ try {
145
+ await fs.rm(tmpFolder, { recursive: true });
146
+ }
147
+ catch {
148
+ // Ignore cleanup errors
149
+ }
150
+ return {
151
+ found: false,
152
+ reason: `Failed to download repository '${repoUrl}': ${error.message}. This could be due to network issues, repository access problems, or authentication issues.`,
153
+ };
154
+ }
155
+ }
156
+ async getRepoContent({ owner, path: filePath, repo, version }) {
157
+ // For cloud packages, owner/repo represent the package name in the registry
158
+ // The actual repository URL comes from the registry metadata
159
+ // Reconstruct package name from owner/repo
160
+ // - If owner is empty → package name is just 'repo' (simple name)
161
+ // - Otherwise → package name is 'owner/repo' (org/name format)
162
+ const packageName = owner ? `${owner}/${repo}` : repo;
163
+ // Include version if provided
164
+ const packageSpec = version ? `${packageName}@${version}` : packageName;
165
+ this.logger?.debug(`getRepoContent: trying to get ${filePath} from registry package '${packageSpec}'`);
166
+ // Resolve package from registry to get repository URL
167
+ const packageInfo = await this.resolvePackage(packageSpec);
168
+ if (!packageInfo) {
169
+ this.logger?.debug(`getRepoContent: package '${packageName}' not found in registry`);
170
+ return {
171
+ found: false,
172
+ reason: `Package '${packageName}' not found in registry`,
173
+ };
174
+ }
175
+ // Now we have the actual repository URL and commit from registry
176
+ const repoUrl = packageInfo.registryPackage.repository;
177
+ const { commit } = packageInfo.registryPackage;
178
+ if (!repoUrl) {
179
+ return {
180
+ found: false,
181
+ reason: `Package '${packageName}' in registry has no repository URL`,
182
+ };
183
+ }
184
+ this.logger?.debug(`getRepoContent: downloading from ${repoUrl}${commit ? `@${commit}` : ''}`);
185
+ // Use the getFileFromRepository method to download from the actual repository
186
+ // Pass SHA256 for checksum validation if available
187
+ return this.getFileFromRepository(repoUrl, commit, filePath, packageInfo.registryPackage.sha256);
188
+ }
189
+ async resolvePackage(packageSpec) {
190
+ // Parse package specification: name, org/name, name@version, or org/name@version
191
+ const { packageName, version } = this.parsePackageSpec(packageSpec);
192
+ try {
193
+ // Try to get package from registry
194
+ // Registry API accepts both 'name' and 'org/name' formats
195
+ const result = version
196
+ ? await this.backend.getPackageByVersion(packageName, version)
197
+ : await this.backend.getPackageLatest(packageName);
198
+ if (result.success && result.package) {
199
+ return {
200
+ name: packageName,
201
+ registryPackage: result.package,
202
+ version: result.package.version,
203
+ };
204
+ }
205
+ // Package not found in registry or API error
206
+ if (!result.success) {
207
+ this.logger?.debug(`Registry API returned error for ${packageName}: ${result.reason}`);
208
+ }
209
+ }
210
+ catch (error) {
211
+ // Network error or other exception
212
+ this.logger?.debug(`Registry API exception for ${packageName}: ${error.message}`);
213
+ }
214
+ return null;
215
+ }
216
+ parsePackageSpec(spec) {
217
+ // Parse package specification
218
+ // Formats: name, org/name, name@version, org/name@version
219
+ const match = spec.match(/^([^@]+)(?:@(.+))?$/);
220
+ if (!match) {
221
+ return { packageName: spec };
222
+ }
223
+ const [, packageName, version] = match;
224
+ return { packageName, version };
225
+ }
226
+ }
@@ -7,6 +7,7 @@ export type GetRepoContentInput = {
7
7
  path: string;
8
8
  projectRootDir?: string;
9
9
  repo: string;
10
+ version?: string;
10
11
  };
11
12
  export type GetRepoContentOutput = {
12
13
  content: string;
@@ -3,5 +3,5 @@ export declare class GitHubPackageManager implements PackageManager {
3
3
  private readonly registryUrl;
4
4
  constructor(registryUrl?: string);
5
5
  downloadPackage(pkgUrl: string, destPath: string): Promise<string>;
6
- getRepoContent({ owner, path: filePath, repo }: GetRepoContentInput): Promise<GetRepoContentOutput>;
6
+ getRepoContent({ owner, path: filePath, repo, version }: GetRepoContentInput): Promise<GetRepoContentOutput>;
7
7
  }
@@ -16,30 +16,57 @@ export class GitHubPackageManager {
16
16
  await fs.mkdir(destPath, { recursive: true });
17
17
  // Initialize simple-git
18
18
  const git = simpleGit();
19
- // Clone repository into temp directory
20
- await git.clone(pkgUrl, destPath, ['--depth=1']);
19
+ // Parse version from URL if present (format: url#version)
20
+ const [repoUrl, version] = pkgUrl.split('#');
21
+ if (version) {
22
+ // Clone repository and checkout specific version tag
23
+ await git.clone(repoUrl, destPath, ['--no-checkout']);
24
+ const repoGit = simpleGit(destPath);
25
+ try {
26
+ // Try to checkout the version as a tag (e.g., v1.2.3 or 1.2.3)
27
+ await repoGit.checkout(`v${version}`);
28
+ }
29
+ catch {
30
+ try {
31
+ // If v-prefixed tag doesn't exist, try without prefix
32
+ await repoGit.checkout(version);
33
+ }
34
+ catch {
35
+ // If neither tag exists, fail with clear error
36
+ throw new Error(`Version '${version}' not found in repository ${repoUrl}. Available tags can be found at ${repoUrl}/tags`);
37
+ }
38
+ }
39
+ }
40
+ else {
41
+ // Clone repository at default branch
42
+ await git.clone(pkgUrl, destPath, ['--depth=1']);
43
+ }
21
44
  return destPath;
22
45
  }
23
- async getRepoContent({ owner, path: filePath, repo }) {
46
+ async getRepoContent({ owner, path: filePath, repo, version }) {
24
47
  const pkgUrl = `${this.registryUrl}/${owner}/${repo}`;
25
48
  const tmpFolder = path.join(os.homedir(), '.hereya', 'downloads', randomUUID());
26
49
  try {
27
- const destPath = await this.downloadPackage(pkgUrl, tmpFolder);
50
+ // Build URL with version if provided
51
+ const versionedPkgUrl = version ? `${pkgUrl}#${version}` : pkgUrl;
52
+ const destPath = await this.downloadPackage(versionedPkgUrl, tmpFolder);
28
53
  if (await fileExists(path.join(destPath, filePath))) {
29
54
  const content = await fs.readFile(path.join(destPath, filePath), 'utf8');
30
55
  // remove the tmp folder
31
56
  await fs.rm(destPath, { recursive: true });
57
+ // Include version in the pkgUrl if provided
58
+ const finalPkgUrl = version ? `${pkgUrl}#${version}` : pkgUrl;
32
59
  return {
33
60
  content,
34
61
  found: true,
35
- pkgUrl,
62
+ pkgUrl: finalPkgUrl,
36
63
  };
37
64
  }
38
65
  // remove the tmp folder
39
66
  await fs.rm(destPath, { recursive: true });
40
67
  return {
41
68
  found: false,
42
- reason: `File ${filePath} not found in ${pkgUrl}`,
69
+ reason: `File ${filePath} not found in ${pkgUrl}${version ? ` at version ${version}` : ''}`,
43
70
  };
44
71
  }
45
72
  catch (error) {
@@ -1,16 +1,20 @@
1
1
  import { z } from 'zod';
2
+ import { Backend } from '../../backend/common.js';
2
3
  import { IacType } from '../../iac/common.js';
3
4
  import { InfrastructureType } from '../../infrastructure/common.js';
5
+ import { Logger } from '../log.js';
4
6
  import { PackageManager } from './common.js';
5
7
  import { LocalPackageManager } from './local.js';
6
- export declare const packageManager: PackageManager;
8
+ export declare const githubPackageManager: PackageManager;
7
9
  export declare const localPackageManager: LocalPackageManager;
8
- export declare function getPackageManager(protocol: string): PackageManager;
10
+ export declare function getPackageManager(protocol: string, logger?: Logger): Promise<PackageManager>;
9
11
  export declare function resolvePackage(input: ResolvePackageInput): Promise<ResolvePackageOutput>;
10
12
  export declare function getPackageCanonicalName(packageName: string): string;
11
13
  export declare function downloadPackage(pkgUrl: string, destPath: string): Promise<string>;
12
14
  export type ResolvePackageInput = {
15
+ backend?: Backend;
13
16
  isDeploying?: boolean;
17
+ logger?: Logger;
14
18
  package: string;
15
19
  projectRootDir?: string;
16
20
  };
@@ -20,6 +24,7 @@ export type ResolvePackageOutput = {
20
24
  metadata: z.infer<typeof PackageMetadata>;
21
25
  packageUri: string;
22
26
  pkgName: string;
27
+ version?: string;
23
28
  } | {
24
29
  found: false;
25
30
  reason: string;
@@ -41,8 +46,8 @@ export declare const PackageMetadata: z.ZodObject<{
41
46
  }>>;
42
47
  originalInfra: z.ZodOptional<z.ZodNativeEnum<typeof InfrastructureType>>;
43
48
  }, "strip", z.ZodTypeAny, {
44
- iac: IacType;
45
49
  infra: InfrastructureType;
50
+ iac: IacType;
46
51
  dependencies?: Record<string, string> | undefined;
47
52
  deploy?: boolean | undefined;
48
53
  onDeploy?: {
@@ -51,8 +56,8 @@ export declare const PackageMetadata: z.ZodObject<{
51
56
  } | undefined;
52
57
  originalInfra?: InfrastructureType | undefined;
53
58
  }, {
54
- iac: IacType;
55
59
  infra: InfrastructureType;
60
+ iac: IacType;
56
61
  dependencies?: Record<string, string> | undefined;
57
62
  deploy?: boolean | undefined;
58
63
  onDeploy?: {
@@ -1,42 +1,139 @@
1
1
  import * as yaml from 'yaml';
2
2
  import { z } from 'zod';
3
+ import { getCurrentBackendType } from '../../backend/config.js';
4
+ import { BackendType, getBackend } from '../../backend/index.js';
3
5
  import { IacType } from '../../iac/common.js';
4
6
  import { InfrastructureType } from '../../infrastructure/common.js';
7
+ import { CloudPackageManager } from './cloud.js';
5
8
  import { GitHubPackageManager } from './github.js';
6
9
  import { LocalPackageManager } from './local.js';
7
- export const packageManager = new GitHubPackageManager();
10
+ export const githubPackageManager = new GitHubPackageManager();
8
11
  export const localPackageManager = new LocalPackageManager();
9
- export function getPackageManager(protocol) {
12
+ export async function getPackageManager(protocol, logger) {
10
13
  if (protocol === 'local') {
11
14
  return localPackageManager;
12
15
  }
13
- return packageManager;
16
+ if (protocol === 'cloud') {
17
+ const backend = await getBackend();
18
+ return new CloudPackageManager(backend, logger);
19
+ }
20
+ // Default to GitHub
21
+ return githubPackageManager;
14
22
  }
15
23
  export async function resolvePackage(input) {
16
- if (input.package.includes('.')) {
24
+ // Parse package spec to extract name and version
25
+ const { packageName, version } = parsePackageSpec(input.package);
26
+ // Validate package name format
27
+ if (packageName.includes('.')) {
17
28
  return { found: false, reason: 'Invalid package format. Package name cannot contain dots (.) nor double dashes (--)' };
18
29
  }
19
- if (input.package.includes('--')) {
30
+ if (packageName.includes('--')) {
20
31
  return { found: false, reason: 'Invalid package format. Package name cannot contain dots (.) nor double dashes (--)' };
21
32
  }
22
- const isLocal = input.package.startsWith('local/');
33
+ const isLocal = packageName.startsWith('local/');
34
+ // Try cloud first if not local and backend is cloud
35
+ if (!isLocal) {
36
+ const backendType = await getCurrentBackendType();
37
+ if (backendType === BackendType.Cloud) {
38
+ const result = await tryCloudResolution(input);
39
+ if (result.found) {
40
+ return result; // Success
41
+ }
42
+ // Failed - check if we should fallback to GitHub or return the error
43
+ if (!packageName.includes('/')) {
44
+ // Simple package names can't fallback to GitHub, return the detailed error
45
+ return result;
46
+ }
47
+ // For owner/repo format, we can fallback to GitHub
48
+ input.logger?.debug(`Package ${packageName} not found in registry, trying GitHub...`);
49
+ input.logger?.debug(`Registry error: ${result.reason}`);
50
+ }
51
+ }
52
+ // Determine protocol for standard resolution
53
+ const protocol = isLocal ? 'local' : '';
54
+ // Standard resolution (GitHub or local)
55
+ return resolveWithStandardManager(protocol, input, packageName, version);
56
+ }
57
+ export function getPackageCanonicalName(packageName) {
58
+ return packageName.replaceAll('/', '--');
59
+ }
60
+ export async function downloadPackage(pkgUrl, destPath) {
61
+ // Determine protocol based on URL characteristics
62
+ let protocol = '';
63
+ if (pkgUrl.startsWith('local/')) {
64
+ protocol = 'local';
65
+ }
66
+ else if (pkgUrl.includes('#')) {
67
+ // Registry package URLs contain commit SHA
68
+ const backendType = await getCurrentBackendType();
69
+ if (backendType === BackendType.Cloud) {
70
+ protocol = 'cloud';
71
+ }
72
+ }
73
+ // Otherwise defaults to GitHub
74
+ const packageManager = await getPackageManager(protocol);
75
+ return packageManager.downloadPackage(pkgUrl, destPath);
76
+ }
77
+ export const PackageMetadata = z.object({
78
+ dependencies: z.record(z.string()).optional(),
79
+ deploy: z.boolean().optional(),
80
+ iac: z.nativeEnum(IacType),
81
+ infra: z.nativeEnum(InfrastructureType),
82
+ onDeploy: z
83
+ .object({
84
+ pkg: z.string(),
85
+ version: z.string(),
86
+ })
87
+ .optional(),
88
+ originalInfra: z.nativeEnum(InfrastructureType).optional(),
89
+ });
90
+ // Helper function to parse package spec into name and version
91
+ function parsePackageSpec(spec) {
92
+ const match = spec.match(/^([^@]+)(?:@(.+))?$/);
93
+ if (!match) {
94
+ return { packageName: spec };
95
+ }
96
+ const [, packageName, version] = match;
97
+ return { packageName, version };
98
+ }
99
+ // Helper function to try cloud resolution
100
+ async function tryCloudResolution(input) {
101
+ const cloudManager = await getPackageManager('cloud', input.logger);
102
+ const cloudPackageInfo = await cloudManager.resolvePackage(input.package);
103
+ if (cloudPackageInfo) {
104
+ // Successfully found in registry, now get metadata
105
+ input.logger?.debug(`Found package in registry: ${cloudPackageInfo.name}@${cloudPackageInfo.version}`);
106
+ const result = await resolveCloudPackage(cloudPackageInfo, cloudManager, input);
107
+ input.logger?.debug(`Metadata resolution result: ${result.found ? 'success' : result.reason}`);
108
+ return result; // Return success or failure with detailed reason
109
+ }
110
+ // Package not found in registry at all
111
+ return {
112
+ found: false,
113
+ reason: `Package '${input.package}' not found in cloud registry. Make sure the package name is correct and you have access to it.`
114
+ };
115
+ }
116
+ // Helper function for standard (GitHub/local) resolution
117
+ async function resolveWithStandardManager(protocol, input, packageName, version) {
118
+ // Standard managers don't support logger yet
119
+ // Parse owner/repo from package name
23
120
  let [owner, repo] = ['', ''];
24
- if (isLocal) {
121
+ if (protocol === 'local') {
25
122
  owner = 'local';
26
- repo = input.package.replace('local/', '');
123
+ repo = packageName.replace('local/', '');
27
124
  }
28
125
  else {
29
- const pkgParts = input.package.split('/');
126
+ const pkgParts = packageName.split('/');
30
127
  if (pkgParts.length !== 2) {
31
- return { found: false, reason: 'Invalid package format. Use owner/repository' };
128
+ return { found: false, reason: 'Invalid package format. Use owner/repository or org/name' };
32
129
  }
33
130
  ;
34
131
  [owner, repo] = pkgParts;
35
132
  }
36
- const packageManager = getPackageManager(isLocal ? 'local' : '');
133
+ const packageManager = await getPackageManager(protocol);
37
134
  const metadataContentCandidates = (await Promise.all([
38
- packageManager.getRepoContent({ owner, path: 'hereyarc.yaml', projectRootDir: input.projectRootDir, repo }),
39
- packageManager.getRepoContent({ owner, path: 'hereyarc.yml', projectRootDir: input.projectRootDir, repo }),
135
+ packageManager.getRepoContent({ owner, path: 'hereyarc.yaml', projectRootDir: input.projectRootDir, repo, version }),
136
+ packageManager.getRepoContent({ owner, path: 'hereyarc.yml', projectRootDir: input.projectRootDir, repo, version }),
40
137
  ])).filter((content$) => content$.found);
41
138
  if (metadataContentCandidates.length === 0) {
42
139
  return { found: false, reason: `No hereya metadata file found in ${input.package}` };
@@ -48,37 +145,83 @@ export async function resolvePackage(input) {
48
145
  return { found: false, reason: 'Package has dependencies but is not a deploy package' };
49
146
  }
50
147
  if (input.isDeploying && metadata.onDeploy) {
51
- return resolvePackage({ package: metadata.onDeploy.pkg, projectRootDir: input.projectRootDir });
148
+ return resolvePackage({ ...input, package: metadata.onDeploy.pkg });
52
149
  }
53
150
  return {
54
- canonicalName: getPackageCanonicalName(input.package),
151
+ canonicalName: getPackageCanonicalName(packageName),
55
152
  found: true,
56
153
  metadata,
57
154
  packageUri: metadataContent$.pkgUrl,
58
- pkgName: input.package,
155
+ pkgName: packageName,
156
+ version: version || '',
59
157
  };
60
158
  }
61
159
  catch (error) {
62
160
  return { found: false, reason: error.message };
63
161
  }
64
162
  }
65
- export function getPackageCanonicalName(packageName) {
66
- return packageName.replaceAll('/', '--');
67
- }
68
- export async function downloadPackage(pkgUrl, destPath) {
69
- const packageManager = getPackageManager(pkgUrl.startsWith('local/') ? 'local' : '');
70
- return packageManager.downloadPackage(pkgUrl, destPath);
163
+ // Helper function to resolve cloud package metadata
164
+ async function resolveCloudPackage(packageInfo, cloudManager, input) {
165
+ // Parse package name to determine owner/repo for getRepoContent interface
166
+ // For simple names like 'mongo', owner will be empty
167
+ // For org/name format like 'hereya/mongo', split into owner and repo
168
+ const parts = packageInfo.name.split('/');
169
+ const owner = parts.length === 2 ? parts[0] : '';
170
+ const repo = parts.length === 2 ? parts[1] : packageInfo.name;
171
+ // Get metadata using the standard getRepoContent interface
172
+ // CloudPackageManager will reconstruct the package name and look it up in registry
173
+ const metadataContentCandidates = (await Promise.all([
174
+ cloudManager.getRepoContent({
175
+ owner,
176
+ path: 'hereyarc.yaml',
177
+ projectRootDir: input.projectRootDir,
178
+ repo,
179
+ version: packageInfo.version,
180
+ }),
181
+ cloudManager.getRepoContent({
182
+ owner,
183
+ path: 'hereyarc.yml',
184
+ projectRootDir: input.projectRootDir,
185
+ repo,
186
+ version: packageInfo.version,
187
+ }),
188
+ ])).filter((content$) => content$.found);
189
+ if (metadataContentCandidates.length === 0) {
190
+ // Try to get more detailed error from the last attempt
191
+ const lastAttempt = await cloudManager.getRepoContent({
192
+ owner,
193
+ path: 'hereyarc.yaml',
194
+ projectRootDir: input.projectRootDir,
195
+ repo,
196
+ });
197
+ const detailedReason = !lastAttempt.found && lastAttempt.reason
198
+ ? lastAttempt.reason
199
+ : `No hereya metadata file (hereyarc.yaml or hereyarc.yml) found for package '${packageInfo.name}'`;
200
+ return { found: false, reason: detailedReason };
201
+ }
202
+ const metadataContent$ = metadataContentCandidates[0];
203
+ try {
204
+ const metadata = PackageMetadata.parse(yaml.parse(metadataContent$.content));
205
+ if (!metadata.deploy && metadata.dependencies) {
206
+ return { found: false, reason: 'Package has dependencies but is not a deploy package' };
207
+ }
208
+ if (input.isDeploying && metadata.onDeploy) {
209
+ return resolvePackage({ ...input, package: metadata.onDeploy.pkg });
210
+ }
211
+ // Build packageUri with repository and commit
212
+ const packageUri = packageInfo.registryPackage.commit
213
+ ? `${packageInfo.registryPackage.repository}#${packageInfo.registryPackage.commit}`
214
+ : packageInfo.registryPackage.repository || metadataContent$.pkgUrl;
215
+ return {
216
+ canonicalName: getPackageCanonicalName(packageInfo.name),
217
+ found: true,
218
+ metadata,
219
+ packageUri,
220
+ pkgName: packageInfo.name,
221
+ version: packageInfo.version,
222
+ };
223
+ }
224
+ catch (error) {
225
+ return { found: false, reason: error.message };
226
+ }
71
227
  }
72
- export const PackageMetadata = z.object({
73
- dependencies: z.record(z.string()).optional(),
74
- deploy: z.boolean().optional(),
75
- iac: z.nativeEnum(IacType),
76
- infra: z.nativeEnum(InfrastructureType),
77
- onDeploy: z
78
- .object({
79
- pkg: z.string(),
80
- version: z.string(),
81
- })
82
- .optional(),
83
- originalInfra: z.nativeEnum(InfrastructureType).optional(),
84
- });
@@ -7,6 +7,7 @@ export class LocalPackageManager {
7
7
  return destPath;
8
8
  }
9
9
  async getRepoContent({ path: filePath, projectRootDir, repo }) {
10
+ // Local packages don't have versions, version parameter is ignored
10
11
  try {
11
12
  const pkgRootDir = path.join(projectRootDir ?? process.cwd(), repo);
12
13
  const resolvedPath = path.join(pkgRootDir, filePath);
@@ -1877,5 +1877,5 @@
1877
1877
  ]
1878
1878
  }
1879
1879
  },
1880
- "version": "0.56.0"
1880
+ "version": "0.57.1"
1881
1881
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hereya-cli",
3
3
  "description": "Infrastructure as Package",
4
- "version": "0.56.0",
4
+ "version": "0.57.1",
5
5
  "author": "Hereya Developers",
6
6
  "bin": {
7
7
  "hereya": "./bin/run.js"
@@ -24,7 +24,7 @@
24
24
  "ignore": "^7.0.3",
25
25
  "keytar": "^7.9.0",
26
26
  "listr2": "^8.2.5",
27
- "marked": "^16.1.2",
27
+ "marked": "^15.0.12",
28
28
  "marked-terminal": "7.3.0",
29
29
  "node-machine-id": "^1.1.12",
30
30
  "open": "^10.1.1",
@@ -43,6 +43,8 @@
43
43
  "@types/mock-fs": "^4.13.4",
44
44
  "@types/node": "^22",
45
45
  "@types/sinon": "^17.0.3",
46
+ "@types/sinon-chai": "^4.0.0",
47
+ "@types/tar": "^6.1.13",
46
48
  "@types/unzip-stream": "^0.3.4",
47
49
  "chai": "^5.1.2",
48
50
  "eslint": "^9.20.1",
@@ -56,6 +58,7 @@
56
58
  "oclif": "^4.17.27",
57
59
  "shx": "^0.3.4",
58
60
  "sinon": "^19.0.2",
61
+ "sinon-chai": "^4.0.0",
59
62
  "tsx": "^4.20.3",
60
63
  "typescript": "^5.7.3"
61
64
  },