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.
- package/README.md +38 -38
- package/dist/backend/common.d.ts +2 -2
- package/dist/commands/add/index.js +2 -1
- package/dist/commands/deploy/index.js +50 -24
- package/dist/commands/down/index.js +40 -11
- package/dist/commands/import/index.js +2 -1
- package/dist/commands/remove/index.js +16 -4
- package/dist/commands/undeploy/index.js +45 -17
- package/dist/commands/up/index.js +36 -11
- package/dist/commands/workspace/install/index.js +1 -1
- package/dist/commands/workspace/uninstall/index.js +18 -5
- package/dist/executor/interface.d.ts +5 -0
- package/dist/executor/local.js +3 -3
- package/dist/infrastructure/index.d.ts +2 -0
- package/dist/infrastructure/index.js +8 -8
- package/dist/lib/config/common.d.ts +1 -0
- package/dist/lib/config/simple.js +2 -2
- package/dist/lib/package/cloud.d.ts +19 -0
- package/dist/lib/package/cloud.js +226 -0
- package/dist/lib/package/common.d.ts +1 -0
- package/dist/lib/package/github.d.ts +1 -1
- package/dist/lib/package/github.js +33 -6
- package/dist/lib/package/index.d.ts +9 -4
- package/dist/lib/package/index.js +178 -35
- package/dist/lib/package/local.js +1 -0
- package/oclif.manifest.json +1 -1
- package/package.json +5 -2
|
@@ -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
|
+
}
|
|
@@ -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
|
-
//
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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 (
|
|
121
|
+
if (protocol === 'local') {
|
|
25
122
|
owner = 'local';
|
|
26
|
-
repo =
|
|
123
|
+
repo = packageName.replace('local/', '');
|
|
27
124
|
}
|
|
28
125
|
else {
|
|
29
|
-
const pkgParts =
|
|
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(
|
|
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
|
|
148
|
+
return resolvePackage({ ...input, package: metadata.onDeploy.pkg });
|
|
52
149
|
}
|
|
53
150
|
return {
|
|
54
|
-
canonicalName: getPackageCanonicalName(
|
|
151
|
+
canonicalName: getPackageCanonicalName(packageName),
|
|
55
152
|
found: true,
|
|
56
153
|
metadata,
|
|
57
154
|
packageUri: metadataContent$.pkgUrl,
|
|
58
|
-
pkgName:
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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);
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hereya-cli",
|
|
3
3
|
"description": "Infrastructure as Package",
|
|
4
|
-
"version": "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": "^
|
|
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
|
},
|