hereya-cli 0.83.1 → 0.84.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.
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { simpleGit } from 'simple-git';
5
5
  import * as yaml from 'yaml';
6
+ import { CloudBackend } from '../../backend/cloud/cloud-backend.js';
6
7
  import { getBackend } from '../../backend/index.js';
7
8
  export default class Publish extends Command {
8
9
  static description = 'Publish a package to the Hereya registry';
@@ -165,6 +166,11 @@ export default class Publish extends Command {
165
166
  try {
166
167
  // Load and validate configuration
167
168
  const config = await this.loadConfig(packageDir);
169
+ const isApp = config.kind === 'app';
170
+ if (isApp) {
171
+ await this.publishApp(packageDir, config);
172
+ return;
173
+ }
168
174
  // Display package info
169
175
  this.log(`\nšŸ“¦ Preparing package ${config.name}@${config.version}`);
170
176
  this.log(` Description: ${config.description}`);
@@ -226,6 +232,10 @@ export default class Publish extends Command {
226
232
  if (!config.version) {
227
233
  this.error('Missing required field "version" in hereyarc.yaml');
228
234
  }
235
+ if (config.kind === 'app') {
236
+ // Apps don't require iac/infra; description optional but recommended.
237
+ return;
238
+ }
229
239
  if (!config.description) {
230
240
  this.error('Missing required field "description" in hereyarc.yaml');
231
241
  }
@@ -236,4 +246,69 @@ export default class Publish extends Command {
236
246
  this.error('Missing required field "infra" in hereyarc.yaml');
237
247
  }
238
248
  }
249
+ async publishApp(packageDir, config) {
250
+ this.log(`\nšŸ“¦ Preparing app ${config.name}@${config.version}`);
251
+ if (config.description) {
252
+ this.log(` Description: ${config.description}`);
253
+ }
254
+ if (config.visibility) {
255
+ this.log(` Visibility: ${config.visibility}`);
256
+ }
257
+ // Validate hereya.yaml shape: must exist; must NOT contain `project:` or `workspace:`.
258
+ const hereyaYamlPath = ['hereya.yaml', 'hereya.yml']
259
+ .map((name) => join(packageDir, name))
260
+ .find((p) => existsSync(p));
261
+ if (!hereyaYamlPath) {
262
+ this.error('Apps must have a hereya.yaml file in the package directory');
263
+ }
264
+ const hereyaYamlContent = readFileSync(hereyaYamlPath, 'utf8');
265
+ let parsedHereyaYaml = null;
266
+ try {
267
+ parsedHereyaYaml = (yaml.parse(hereyaYamlContent) ?? {});
268
+ }
269
+ catch (error) {
270
+ this.error(`Failed to parse hereya.yaml: ${error instanceof Error ? error.message : String(error)}`);
271
+ }
272
+ if (parsedHereyaYaml && typeof parsedHereyaYaml === 'object') {
273
+ if ('project' in parsedHereyaYaml) {
274
+ this.error('App hereya.yaml must not contain a "project:" key (it is set at deploy time)');
275
+ }
276
+ if ('workspace' in parsedHereyaYaml) {
277
+ this.error('App hereya.yaml must not contain a "workspace:" key (it is set at deploy time)');
278
+ }
279
+ }
280
+ this.log('\nšŸ” Analyzing repository...');
281
+ const gitInfo = await this.getGitInfo(packageDir);
282
+ const { commit, repository, sha256 } = gitInfo;
283
+ this.log(` Repository: ${repository}`);
284
+ this.log(` Commit: ${commit.slice(0, 8)}`);
285
+ this.log(` SHA256: ${sha256.slice(0, 16)}...`);
286
+ this.log('\nšŸ” Checking authentication...');
287
+ const backend = await getBackend();
288
+ if (!(backend instanceof CloudBackend) || typeof backend.publishAppVersion !== 'function') {
289
+ this.log('\nāŒ Authentication failed\n');
290
+ this.error('Publishing apps is only supported with the cloud backend. Please run `hereya login` first.');
291
+ }
292
+ this.log(' āœ“ Authenticated with Hereya Cloud');
293
+ this.log('\nšŸ“¤ Publishing app version to registry...');
294
+ const result = await backend.publishAppVersion({
295
+ commit: commit.trim(),
296
+ description: config.description,
297
+ hereyaYaml: hereyaYamlContent,
298
+ name: config.name,
299
+ repository,
300
+ sha256,
301
+ version: config.version,
302
+ visibility: config.visibility?.toLowerCase(),
303
+ });
304
+ if (!result.success) {
305
+ this.displayPublishError(result.reason, config);
306
+ return;
307
+ }
308
+ this.log(`\nāœ… Successfully published app ${result.app.name}@${result.app.version}`);
309
+ if (result.app.id) {
310
+ this.log(` App Version ID: ${result.app.id}`);
311
+ }
312
+ this.log('');
313
+ }
239
314
  }
@@ -0,0 +1,21 @@
1
+ import { Logger } from './log.js';
2
+ export interface DownloadAppSourceInput {
3
+ commit?: string;
4
+ logger?: Logger;
5
+ repository: string;
6
+ sha256?: string;
7
+ }
8
+ export interface DownloadAppSourceOutput {
9
+ cleanup: () => Promise<void>;
10
+ rootDir: string;
11
+ }
12
+ /**
13
+ * Clone an app's source repository (by repo URL + commit SHA) into a fresh
14
+ * temp dir under ~/.hereya/downloads/<id>. Returns the rootDir path along
15
+ * with a cleanup() helper.
16
+ *
17
+ * Validates the git archive sha256 matches when provided.
18
+ */
19
+ export declare const appSource: {
20
+ download(input: DownloadAppSourceInput): Promise<DownloadAppSourceOutput>;
21
+ };
@@ -0,0 +1,55 @@
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
+ /**
6
+ * Clone an app's source repository (by repo URL + commit SHA) into a fresh
7
+ * temp dir under ~/.hereya/downloads/<id>. Returns the rootDir path along
8
+ * with a cleanup() helper.
9
+ *
10
+ * Validates the git archive sha256 matches when provided.
11
+ */
12
+ export const appSource = {
13
+ async download(input) {
14
+ const downloadId = randomUUID();
15
+ const downloadsRoot = path.join(os.homedir(), '.hereya', 'downloads');
16
+ await fs.mkdir(downloadsRoot, { recursive: true });
17
+ const rootDir = path.join(downloadsRoot, downloadId);
18
+ const cleanup = async () => {
19
+ try {
20
+ await fs.rm(rootDir, { force: true, recursive: true });
21
+ }
22
+ catch {
23
+ // best-effort cleanup
24
+ }
25
+ };
26
+ try {
27
+ const { simpleGit } = await import('simple-git');
28
+ const git = simpleGit();
29
+ input.logger?.debug?.(`Cloning app source from ${input.repository}${input.commit ? `@${input.commit}` : ''} into ${rootDir}`);
30
+ if (input.commit) {
31
+ await git.clone(input.repository, rootDir, ['--no-checkout']);
32
+ const repoGit = simpleGit(rootDir);
33
+ await repoGit.checkout(input.commit);
34
+ }
35
+ else {
36
+ await git.clone(input.repository, rootDir, ['--depth=1']);
37
+ }
38
+ if (input.sha256) {
39
+ const repoGit = simpleGit(rootDir);
40
+ const archiveBuffer = await repoGit.raw(['archive', '--format=tar', input.commit ?? 'HEAD']);
41
+ const hash = createHash('sha256');
42
+ hash.update(archiveBuffer);
43
+ const actualChecksum = hash.digest('hex');
44
+ if (actualChecksum !== input.sha256) {
45
+ throw new Error(`Checksum mismatch for app source ${input.repository}@${input.commit}. Expected: ${input.sha256}, Got: ${actualChecksum}`);
46
+ }
47
+ }
48
+ return { cleanup, rootDir };
49
+ }
50
+ catch (error) {
51
+ await cleanup();
52
+ throw error;
53
+ }
54
+ },
55
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Write per-deployment hereyavars overrides into <appRootDir>/hereyaconfig/hereyavars/.
3
+ *
4
+ * `hereyaVarsYaml` is a YAML string mapping filename -> YAML body (string).
5
+ * Each filename is validated against a safe pattern (no path traversal, must
6
+ * end in `.yaml`/`.yml`). Each body must be a string.
7
+ */
8
+ export declare function applyHereyaVars(appRootDir: string, hereyaVarsYaml: string | undefined): Promise<{
9
+ filesWritten: string[];
10
+ }>;
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import * as yaml from 'yaml';
4
+ const HEREYAVARS_FILENAME_PATTERN = /^[\w.-]+\.ya?ml$/;
5
+ /**
6
+ * Write per-deployment hereyavars overrides into <appRootDir>/hereyaconfig/hereyavars/.
7
+ *
8
+ * `hereyaVarsYaml` is a YAML string mapping filename -> YAML body (string).
9
+ * Each filename is validated against a safe pattern (no path traversal, must
10
+ * end in `.yaml`/`.yml`). Each body must be a string.
11
+ */
12
+ export async function applyHereyaVars(appRootDir, hereyaVarsYaml) {
13
+ if (!hereyaVarsYaml || hereyaVarsYaml.trim() === '') {
14
+ return { filesWritten: [] };
15
+ }
16
+ let parsed;
17
+ try {
18
+ parsed = yaml.parse(hereyaVarsYaml);
19
+ }
20
+ catch (error) {
21
+ throw new Error(`Failed to parse hereyaVarsYaml: ${error.message}`);
22
+ }
23
+ if (parsed === null || parsed === undefined) {
24
+ return { filesWritten: [] };
25
+ }
26
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
27
+ throw new TypeError('hereyaVarsYaml must be a YAML mapping of filename -> body string');
28
+ }
29
+ const targetDir = path.join(appRootDir, 'hereyaconfig', 'hereyavars');
30
+ await fs.mkdir(targetDir, { recursive: true });
31
+ const filesWritten = [];
32
+ for (const [filename, body] of Object.entries(parsed)) {
33
+ if (!HEREYAVARS_FILENAME_PATTERN.test(filename)) {
34
+ throw new Error(`Invalid hereyavars filename '${filename}'. Expected a name matching ${HEREYAVARS_FILENAME_PATTERN.source}`);
35
+ }
36
+ if (typeof body !== 'string') {
37
+ throw new TypeError(`hereyavars value for '${filename}' must be a string (YAML body)`);
38
+ }
39
+ const filePath = path.join(targetDir, filename);
40
+ // eslint-disable-next-line no-await-in-loop
41
+ await fs.writeFile(filePath, body, { encoding: 'utf8' });
42
+ filesWritten.push(filePath);
43
+ }
44
+ return { filesWritten };
45
+ }
@@ -29,6 +29,8 @@ export type ResolvePackageOutput = {
29
29
  found: false;
30
30
  reason: string;
31
31
  };
32
+ export declare const PackageKind: z.ZodDefault<z.ZodEnum<["app", "package"]>>;
33
+ export type PackageKindType = z.infer<typeof PackageKind>;
32
34
  export declare const PackageMetadata: z.ZodObject<{
33
35
  dependencies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
34
36
  deploy: z.ZodOptional<z.ZodBoolean>;
@@ -36,6 +38,7 @@ export declare const PackageMetadata: z.ZodObject<{
36
38
  iac: z.ZodNativeEnum<typeof IacType>;
37
39
  infra: z.ZodNativeEnum<typeof InfrastructureType>;
38
40
  inputs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{}, "passthrough", z.ZodTypeAny, z.objectOutputType<{}, z.ZodTypeAny, "passthrough">, z.objectInputType<{}, z.ZodTypeAny, "passthrough">>>>;
41
+ kind: z.ZodOptional<z.ZodDefault<z.ZodEnum<["app", "package"]>>>;
39
42
  onDeploy: z.ZodOptional<z.ZodObject<{
40
43
  pkg: z.ZodString;
41
44
  version: z.ZodString;
@@ -53,6 +56,7 @@ export declare const PackageMetadata: z.ZodObject<{
53
56
  infra: InfrastructureType;
54
57
  iac: IacType;
55
58
  dependencies?: Record<string, string> | undefined;
59
+ kind?: "package" | "app" | undefined;
56
60
  deploy?: boolean | undefined;
57
61
  devDeploy?: boolean | undefined;
58
62
  inputs?: Record<string, z.objectOutputType<{}, z.ZodTypeAny, "passthrough">> | undefined;
@@ -67,6 +71,7 @@ export declare const PackageMetadata: z.ZodObject<{
67
71
  infra: InfrastructureType;
68
72
  iac: IacType;
69
73
  dependencies?: Record<string, string> | undefined;
74
+ kind?: "package" | "app" | undefined;
70
75
  deploy?: boolean | undefined;
71
76
  devDeploy?: boolean | undefined;
72
77
  inputs?: Record<string, z.objectInputType<{}, z.ZodTypeAny, "passthrough">> | undefined;
@@ -74,6 +74,7 @@ export async function downloadPackage(pkgUrl, destPath) {
74
74
  const packageManager = await getPackageManager(protocol);
75
75
  return packageManager.downloadPackage(pkgUrl, destPath);
76
76
  }
77
+ export const PackageKind = z.enum(['app', 'package']).default('package');
77
78
  export const PackageMetadata = z.object({
78
79
  dependencies: z.record(z.string()).optional(),
79
80
  deploy: z.boolean().optional(),
@@ -81,6 +82,7 @@ export const PackageMetadata = z.object({
81
82
  iac: z.nativeEnum(IacType),
82
83
  infra: z.nativeEnum(InfrastructureType),
83
84
  inputs: z.record(z.object({}).passthrough()).optional(),
85
+ kind: PackageKind.optional(),
84
86
  onDeploy: z
85
87
  .object({
86
88
  pkg: z.string(),