hereya-cli 0.72.1 → 0.73.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.
@@ -9,6 +9,7 @@ export interface Backend {
9
9
  exportBackend(): Promise<ExportBackendOutput>;
10
10
  getPackageByVersion?(name: string, version: string): Promise<GetPackageOutput>;
11
11
  getPackageLatest?(name: string): Promise<GetPackageOutput>;
12
+ getProjectMetadata?(input: GetProjectMetadataInput): Promise<GetProjectMetadataOutput>;
12
13
  getProvisioningId(input: GetProvisioningIdInput): Promise<GetProvisioningIdOutput>;
13
14
  getState(input: GetStateInput): Promise<GetStateOutput>;
14
15
  getWorkspace(workspace: string): Promise<GetWorkspaceOutput>;
@@ -19,6 +20,7 @@ export interface Backend {
19
20
  listWorkspaces(input?: ListWorkspacesInput): Promise<string[]>;
20
21
  publishPackage?(input: PublishPackageInput): Promise<PublishPackageOutput>;
21
22
  removePackageFromWorkspace(input: RemovePackageFromWorkspaceInput): Promise<RemovePackageFromWorkspaceOutput>;
23
+ saveProjectMetadata?(input: SaveProjectMetadataInput): Promise<SaveProjectMetadataOutput>;
22
24
  saveState(config: Config, workspace?: string): Promise<void>;
23
25
  searchPackages?(input: SearchPackagesInput): Promise<SearchPackagesOutput>;
24
26
  setEnvVar(input: SetEnvVarInput): Promise<SetEnvVarOutput>;
@@ -311,3 +313,29 @@ export type SearchPackagesOutput = {
311
313
  reason: string;
312
314
  success: false;
313
315
  };
316
+ export type GetProjectMetadataInput = {
317
+ project: string;
318
+ };
319
+ export type ProjectMetadata = {
320
+ env?: {
321
+ [key: string]: string;
322
+ };
323
+ template: string;
324
+ };
325
+ export type GetProjectMetadataOutput = {
326
+ found: false;
327
+ reason?: string;
328
+ } | {
329
+ found: true;
330
+ metadata: ProjectMetadata;
331
+ };
332
+ export type SaveProjectMetadataInput = {
333
+ metadata: ProjectMetadata;
334
+ project: string;
335
+ };
336
+ export type SaveProjectMetadataOutput = {
337
+ reason: string;
338
+ success: false;
339
+ } | {
340
+ success: true;
341
+ };
@@ -1,5 +1,5 @@
1
1
  import { Config } from '../lib/config/common.js';
2
- import { AddPackageToWorkspaceInput, AddPackageToWorkspaceOutput, Backend, CreateWorkspaceInput, CreateWorkspaceOutput, DeleteStateInput, DeleteStateOutput, DeleteWorkspaceInput, DeleteWorkspaceOutput, ExportBackendOutput, GetPackageOutput, GetProvisioningIdInput, GetProvisioningIdOutput, GetStateInput, GetStateOutput, GetWorkspaceEnvInput, GetWorkspaceEnvOutput, GetWorkspaceOutput, ImportBackendInput, ImportBackendOutput, InitProjectInput, InitProjectOutput, ListPackageVersionsOutput, ListWorkspacesInput, PublishPackageInput, PublishPackageOutput, RemovePackageFromWorkspaceInput, RemovePackageFromWorkspaceOutput, SetEnvVarInput, SetEnvVarOutput, UnsetEnvVarInput, UnsetEnvVarOutput, UpdateWorkspaceInput, UpdateWorkspaceOutput } from './common.js';
2
+ import { AddPackageToWorkspaceInput, AddPackageToWorkspaceOutput, Backend, CreateWorkspaceInput, CreateWorkspaceOutput, DeleteStateInput, DeleteStateOutput, DeleteWorkspaceInput, DeleteWorkspaceOutput, ExportBackendOutput, GetPackageOutput, GetProjectMetadataInput, GetProjectMetadataOutput, GetProvisioningIdInput, GetProvisioningIdOutput, GetStateInput, GetStateOutput, GetWorkspaceEnvInput, GetWorkspaceEnvOutput, GetWorkspaceOutput, ImportBackendInput, ImportBackendOutput, InitProjectInput, InitProjectOutput, ListPackageVersionsOutput, ListWorkspacesInput, PublishPackageInput, PublishPackageOutput, RemovePackageFromWorkspaceInput, RemovePackageFromWorkspaceOutput, SaveProjectMetadataInput, SaveProjectMetadataOutput, SetEnvVarInput, SetEnvVarOutput, UnsetEnvVarInput, UnsetEnvVarOutput, UpdateWorkspaceInput, UpdateWorkspaceOutput } from './common.js';
3
3
  import { FileStorage } from './file-storage/common.js';
4
4
  export declare class FileBackend implements Backend {
5
5
  private readonly fileStorage;
@@ -11,6 +11,7 @@ export declare class FileBackend implements Backend {
11
11
  exportBackend(): Promise<ExportBackendOutput>;
12
12
  getPackageByVersion(_: string, __: string): Promise<GetPackageOutput>;
13
13
  getPackageLatest(_: string): Promise<GetPackageOutput>;
14
+ getProjectMetadata(_input: GetProjectMetadataInput): Promise<GetProjectMetadataOutput>;
14
15
  getProvisioningId(input: GetProvisioningIdInput): Promise<GetProvisioningIdOutput>;
15
16
  getState(input: GetStateInput): Promise<GetStateOutput>;
16
17
  getWorkspace(workspace: string): Promise<GetWorkspaceOutput>;
@@ -21,6 +22,7 @@ export declare class FileBackend implements Backend {
21
22
  listWorkspaces(_input?: ListWorkspacesInput): Promise<string[]>;
22
23
  publishPackage(_: PublishPackageInput): Promise<PublishPackageOutput>;
23
24
  removePackageFromWorkspace(input: RemovePackageFromWorkspaceInput): Promise<RemovePackageFromWorkspaceOutput>;
25
+ saveProjectMetadata(_input: SaveProjectMetadataInput): Promise<SaveProjectMetadataOutput>;
24
26
  saveState(config: Config, workspace?: string): Promise<void>;
25
27
  setEnvVar(input: SetEnvVarInput): Promise<SetEnvVarOutput>;
26
28
  unsetEnvVar(input: UnsetEnvVarInput): Promise<UnsetEnvVarOutput>;
@@ -192,6 +192,9 @@ export class FileBackend {
192
192
  success: false,
193
193
  };
194
194
  }
195
+ async getProjectMetadata(_input) {
196
+ return { found: false };
197
+ }
195
198
  async getProvisioningId(input) {
196
199
  const idPaths = [`provisioning/${input.logicalId}.yaml`, `provisioning/${input.logicalId}.yml`];
197
200
  const newId = `p-${randomUUID()}`;
@@ -402,6 +405,12 @@ export class FileBackend {
402
405
  };
403
406
  }
404
407
  }
408
+ async saveProjectMetadata(_input) {
409
+ return {
410
+ reason: 'Project metadata is only supported with the cloud backend. Please run `hereya login` first.',
411
+ success: false,
412
+ };
413
+ }
405
414
  async saveState(config, workspace) {
406
415
  const paths = [
407
416
  ['state', 'projects', workspace ?? config.workspace, `${config.project}.yaml`].join('/'),
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CredentialHelper extends Command {
3
+ static args: {
4
+ operation: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static hidden: boolean;
8
+ run(): Promise<void>;
9
+ private readCredentialInput;
10
+ }
@@ -0,0 +1,87 @@
1
+ import { Args, Command } from '@oclif/core';
2
+ import { createInterface } from 'node:readline';
3
+ import { getBackend } from '../../backend/index.js';
4
+ import { getExecutorForWorkspace } from '../../executor/context.js';
5
+ import { getConfigManager } from '../../lib/config/index.js';
6
+ export default class CredentialHelper extends Command {
7
+ static args = {
8
+ operation: Args.string({ description: 'Git credential operation (get, store, erase)', required: true }),
9
+ };
10
+ static description = 'Git credential helper for hereya-managed repositories';
11
+ static hidden = true;
12
+ async run() {
13
+ const { args } = await this.parse(CredentialHelper);
14
+ // Only handle 'get' - store and erase are no-ops
15
+ if (args.operation !== 'get') {
16
+ return;
17
+ }
18
+ // Read stdin input per git credential protocol (key=value pairs, blank line terminated)
19
+ await this.readCredentialInput();
20
+ // Strategy 1: Check environment variables (used during init clone)
21
+ const envUsername = process.env.hereyaGitUsername;
22
+ const envPassword = process.env.hereyaGitPassword;
23
+ if (envUsername || envPassword) {
24
+ if (envUsername)
25
+ process.stdout.write(`username=${envUsername}\n`);
26
+ if (envPassword)
27
+ process.stdout.write(`password=${envPassword}\n`);
28
+ process.stdout.write('\n');
29
+ return;
30
+ }
31
+ // Strategy 2: Load from project metadata via backend + resolve through executor
32
+ try {
33
+ const configManager = getConfigManager();
34
+ const config$ = await configManager.loadConfig({});
35
+ if (!config$.found)
36
+ return;
37
+ const backend = await getBackend();
38
+ if (!backend.getProjectMetadata)
39
+ return;
40
+ const metadata$ = await backend.getProjectMetadata({ project: config$.config.project });
41
+ if (!metadata$.found || !metadata$.metadata.env)
42
+ return;
43
+ const envToResolve = {};
44
+ if (metadata$.metadata.env.hereyaGitUsername)
45
+ envToResolve.hereyaGitUsername = metadata$.metadata.env.hereyaGitUsername;
46
+ if (metadata$.metadata.env.hereyaGitPassword)
47
+ envToResolve.hereyaGitPassword = metadata$.metadata.env.hereyaGitPassword;
48
+ if (Object.keys(envToResolve).length === 0)
49
+ return;
50
+ const executor$ = await getExecutorForWorkspace(config$.config.workspace, config$.config.project);
51
+ if (!executor$.success)
52
+ return;
53
+ const resolved = await executor$.executor.resolveEnvValues({ env: envToResolve });
54
+ if (resolved.hereyaGitUsername)
55
+ process.stdout.write(`username=${resolved.hereyaGitUsername}\n`);
56
+ if (resolved.hereyaGitPassword)
57
+ process.stdout.write(`password=${resolved.hereyaGitPassword}\n`);
58
+ process.stdout.write('\n');
59
+ }
60
+ catch {
61
+ // Silently fail - git will prompt for credentials
62
+ }
63
+ }
64
+ readCredentialInput() {
65
+ return new Promise((resolve) => {
66
+ const result = {};
67
+ const rl = createInterface({ input: process.stdin });
68
+ rl.on('line', (line) => {
69
+ if (line === '') {
70
+ rl.close();
71
+ return;
72
+ }
73
+ const eqIndex = line.indexOf('=');
74
+ if (eqIndex !== -1) {
75
+ result[line.slice(0, eqIndex)] = line.slice(eqIndex + 1);
76
+ }
77
+ });
78
+ rl.on('close', () => resolve(result));
79
+ // Timeout in case stdin never provides input
80
+ const timeoutMs = process.env.NODE_ENV === 'test' ? 10 : 5000;
81
+ setTimeout(() => {
82
+ rl.close();
83
+ resolve(result);
84
+ }, timeoutMs);
85
+ });
86
+ }
87
+ }
@@ -1,8 +1,8 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
2
  import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
3
- import { getCloudCredentials, loadBackendConfig } from '../../../backend/config.js';
4
3
  import { getBackend } from '../../../backend/index.js';
5
4
  import { getExecutorForWorkspace } from '../../../executor/context.js';
5
+ import { hereyaTokenUtils } from '../../../lib/hereya-token.js';
6
6
  import { getLogger, getLogPath, isDebug, setDebug } from '../../../lib/log.js';
7
7
  import { arrayOfStringToObject } from '../../../lib/object-utils.js';
8
8
  import { delay } from '../../../lib/shell.js';
@@ -78,7 +78,7 @@ export default class DevenvInstall extends Command {
78
78
  },
79
79
  {
80
80
  async task(ctx) {
81
- const token = await generateHereyaToken(flags.workspace);
81
+ const token = await hereyaTokenUtils.generateHereyaToken(`Dev environment: ${flags.workspace}`);
82
82
  if (token) {
83
83
  ctx.parameters = { ...ctx.parameters, hereyaCloudUrl: token.cloudUrl, hereyaToken: token.token };
84
84
  }
@@ -142,29 +142,3 @@ See ${getLogPath()} for more details`);
142
142
  }
143
143
  }
144
144
  }
145
- async function generateHereyaToken(workspace) {
146
- const backendConfig = await loadBackendConfig();
147
- if (!backendConfig.cloud) {
148
- return null;
149
- }
150
- const { clientId, url } = backendConfig.cloud;
151
- const credentials = await getCloudCredentials(clientId);
152
- if (!credentials) {
153
- return null;
154
- }
155
- const formData = new FormData();
156
- formData.append('description', `Dev environment: ${workspace}`);
157
- formData.append('expiresInDays', '365');
158
- const response = await fetch(`${url}/api/personal-tokens`, {
159
- body: formData,
160
- headers: {
161
- Authorization: `Bearer ${credentials.accessToken}`,
162
- },
163
- method: 'POST',
164
- });
165
- if (!response.ok) {
166
- return null;
167
- }
168
- const result = await response.json();
169
- return { cloudUrl: url, token: result.data.token };
170
- }
@@ -7,6 +7,8 @@ export default class DevenvProjectInit extends Command {
7
7
  static examples: string[];
8
8
  static flags: {
9
9
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ parameter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ template: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
12
  workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
13
  };
12
14
  run(): Promise<void>;
@@ -17,6 +17,8 @@ export default class DevenvProjectInit extends Command {
17
17
  static description = 'Initialize a project on a remote dev environment.';
18
18
  static examples = [
19
19
  '<%= config.bin %> <%= command.id %> my-app -w my-workspace',
20
+ '<%= config.bin %> <%= command.id %> my-app -w my-workspace -t acme/node-starter',
21
+ '<%= config.bin %> <%= command.id %> my-app -w my-workspace -t acme/node-starter -p region=us-east-1',
20
22
  ];
21
23
  static flags = {
22
24
  force: Flags.boolean({
@@ -24,6 +26,18 @@ export default class DevenvProjectInit extends Command {
24
26
  default: false,
25
27
  description: 'continue even if folder already exists',
26
28
  }),
29
+ parameter: Flags.string({
30
+ char: 'p',
31
+ default: [],
32
+ description: "parameter for the template, in the form of 'key=value'. Can be specified multiple times.",
33
+ multiple: true,
34
+ required: false,
35
+ }),
36
+ template: Flags.string({
37
+ char: 't',
38
+ description: 'template package to scaffold the project from',
39
+ required: false,
40
+ }),
27
41
  workspace: Flags.string({
28
42
  char: 'w',
29
43
  description: 'name of the workspace',
@@ -33,7 +47,7 @@ export default class DevenvProjectInit extends Command {
33
47
  async run() {
34
48
  const { args, flags } = await this.parse(DevenvProjectInit);
35
49
  const { project } = args;
36
- const { force, workspace } = flags;
50
+ const { force, parameter, template, workspace } = flags;
37
51
  const backend = await getBackend();
38
52
  const getWorkspaceEnvOutput = await backend.getWorkspaceEnv({ workspace });
39
53
  if (!getWorkspaceEnvOutput.success) {
@@ -58,9 +72,21 @@ export default class DevenvProjectInit extends Command {
58
72
  try {
59
73
  await fs.writeFile(tempKeyPath, sshPrivateKey);
60
74
  await setKeyFilePermissions(tempKeyPath);
61
- const remoteScript = force
62
- ? `mkdir -p ~/${project} && cd ~/${project} && hereya init ${project} -w ${workspace}`
63
- : `if [ -d ~/${project} ]; then echo "ERROR: Folder ~/${project} already exists. Use --force to continue." && exit 1; fi && mkdir -p ~/${project} && cd ~/${project} && hereya init ${project} -w ${workspace}`;
75
+ const templateFlag = template ? ` -t ${template}` : '';
76
+ const paramFlags = (parameter ?? []).map((p) => ` -p ${p}`).join('');
77
+ let remoteScript;
78
+ if (template) {
79
+ // With template: hereya init creates dir via git clone, no need for mkdir
80
+ remoteScript = force
81
+ ? `cd ~ && hereya init ${project} -w ${workspace}${templateFlag}${paramFlags}`
82
+ : `if [ -d ~/${project} ]; then echo "ERROR: Folder ~/${project} already exists. Use --force to continue." && exit 1; fi && cd ~ && hereya init ${project} -w ${workspace}${templateFlag}${paramFlags}`;
83
+ }
84
+ else {
85
+ // Without template: existing behavior (mkdir + cd + init)
86
+ remoteScript = force
87
+ ? `mkdir -p ~/${project} && cd ~/${project} && hereya init ${project} -w ${workspace}`
88
+ : `if [ -d ~/${project} ]; then echo "ERROR: Folder ~/${project} already exists. Use --force to continue." && exit 1; fi && mkdir -p ~/${project} && cd ~/${project} && hereya init ${project} -w ${workspace}`;
89
+ }
64
90
  const sshArgs = ['-i', tempKeyPath, '-o', 'StrictHostKeyChecking=no', `${sshUser}@${host}`, remoteScript];
65
91
  await this.spawnSsh(sshArgs);
66
92
  }
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class DevenvProjectUninit extends Command {
3
+ static args: {
4
+ project: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ protected spawnSsh(args: string[]): Promise<void>;
14
+ }
@@ -0,0 +1,95 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { spawn } from 'node:child_process';
3
+ import { randomUUID } from 'node:crypto';
4
+ import fs from 'node:fs/promises';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { getBackend } from '../../../../backend/index.js';
8
+ import { getExecutorForWorkspace } from '../../../../executor/context.js';
9
+ import { setKeyFilePermissions } from '../../../../lib/ssh-utils.js';
10
+ export default class DevenvProjectUninit extends Command {
11
+ static args = {
12
+ project: Args.string({
13
+ description: 'project name',
14
+ required: true,
15
+ }),
16
+ };
17
+ static description = 'Uninitialize a project on a remote dev environment.';
18
+ static examples = [
19
+ '<%= config.bin %> <%= command.id %> my-app -w my-workspace',
20
+ '<%= config.bin %> <%= command.id %> my-app -w my-workspace --force',
21
+ ];
22
+ static flags = {
23
+ force: Flags.boolean({
24
+ char: 'f',
25
+ default: false,
26
+ description: 'also remove the project directory after uninit',
27
+ }),
28
+ workspace: Flags.string({
29
+ char: 'w',
30
+ description: 'name of the workspace',
31
+ required: true,
32
+ }),
33
+ };
34
+ async run() {
35
+ const { args, flags } = await this.parse(DevenvProjectUninit);
36
+ const { project } = args;
37
+ const { force, workspace } = flags;
38
+ const backend = await getBackend();
39
+ const getWorkspaceEnvOutput = await backend.getWorkspaceEnv({ workspace });
40
+ if (!getWorkspaceEnvOutput.success) {
41
+ this.error(getWorkspaceEnvOutput.reason);
42
+ }
43
+ let { env } = getWorkspaceEnvOutput;
44
+ const executor$ = await getExecutorForWorkspace(workspace);
45
+ if (!executor$.success) {
46
+ this.error(executor$.reason);
47
+ }
48
+ const { executor } = executor$;
49
+ env = await executor.resolveEnvValues({ env });
50
+ const sshHost = env.devEnvSshHost;
51
+ const sshPrivateKey = env.devEnvSshPrivateKey;
52
+ const sshUser = env.devEnvSshUser;
53
+ const sshHostDns = env.devEnvSshHostDns;
54
+ if (!sshHost || !sshPrivateKey || !sshUser) {
55
+ this.error('devEnvSshHost, devEnvSshPrivateKey, and devEnvSshUser must be set in the workspace environment');
56
+ }
57
+ const host = sshHostDns || sshHost;
58
+ const tempKeyPath = path.join(os.tmpdir(), `hereya-ssh-${randomUUID()}`);
59
+ try {
60
+ await fs.writeFile(tempKeyPath, sshPrivateKey);
61
+ await setKeyFilePermissions(tempKeyPath);
62
+ const remoteScript = force
63
+ ? `cd ~ && hereya uninit ${project} -w ${workspace} && rm -rf ~/${project}`
64
+ : `cd ~ && hereya uninit ${project} -w ${workspace}`;
65
+ const sshArgs = ['-i', tempKeyPath, '-o', 'StrictHostKeyChecking=no', `${sshUser}@${host}`, remoteScript];
66
+ await this.spawnSsh(sshArgs);
67
+ }
68
+ finally {
69
+ try {
70
+ await fs.unlink(tempKeyPath);
71
+ }
72
+ catch {
73
+ // Ignore cleanup errors
74
+ }
75
+ }
76
+ }
77
+ spawnSsh(args) {
78
+ return new Promise((resolve, reject) => {
79
+ const child = spawn('ssh', args, {
80
+ stdio: 'inherit',
81
+ });
82
+ child.on('close', (code) => {
83
+ if (code === 0) {
84
+ resolve();
85
+ }
86
+ else {
87
+ reject(new Error(`SSH exited with code ${code}`));
88
+ }
89
+ });
90
+ child.on('error', (err) => {
91
+ reject(err);
92
+ });
93
+ });
94
+ }
95
+ }
@@ -7,6 +7,8 @@ export default class Init extends Command {
7
7
  static examples: string[];
8
8
  static flags: {
9
9
  chdir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ parameter: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
11
+ template: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
12
  workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
13
  };
12
14
  run(): Promise<void>;
@@ -1,6 +1,14 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
+ import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
2
5
  import { getBackend } from '../../backend/index.js';
6
+ import { getExecutorForWorkspace } from '../../executor/context.js';
3
7
  import { getConfigManager } from '../../lib/config/index.js';
8
+ import { gitUtils } from '../../lib/git-utils.js';
9
+ import { hereyaTokenUtils } from '../../lib/hereya-token.js';
10
+ import { getLogger, getLogPath } from '../../lib/log.js';
11
+ import { arrayOfStringToObject } from '../../lib/object-utils.js';
4
12
  export default class Init extends Command {
5
13
  static args = {
6
14
  project: Args.string({ description: 'project name', required: true }),
@@ -9,12 +17,25 @@ export default class Init extends Command {
9
17
  static examples = [
10
18
  '<%= config.bin %> <%= command.id %> myProject -w=defaultWorkspace',
11
19
  '<%= config.bin %> <%= command.id %> myProject -w=defaultWorkspace --chdir=./myProject',
20
+ '<%= config.bin %> <%= command.id %> myProject -w=dev -t=acme/node-starter',
21
+ '<%= config.bin %> <%= command.id %> myProject -w=dev -t=acme/node-starter -p region=us-east-1',
12
22
  ];
13
23
  static flags = {
14
24
  chdir: Flags.string({
15
25
  description: 'directory to run command in',
16
26
  required: false,
17
27
  }),
28
+ parameter: Flags.string({
29
+ char: 'p',
30
+ default: [],
31
+ description: "template parameter, in the form of 'key=value'. Can be specified multiple times.",
32
+ multiple: true,
33
+ }),
34
+ template: Flags.string({
35
+ char: 't',
36
+ description: 'template package name (e.g. owner/repo)',
37
+ required: false,
38
+ }),
18
39
  workspace: Flags.string({
19
40
  char: 'w',
20
41
  description: 'workspace to set as default',
@@ -24,6 +45,118 @@ export default class Init extends Command {
24
45
  async run() {
25
46
  const { args, flags } = await this.parse(Init);
26
47
  const projectRootDir = flags.chdir || process.env.HEREYA_PROJECT_ROOT_DIR;
48
+ // Template flow
49
+ if (flags.template) {
50
+ const targetDir = projectRootDir || path.join(process.cwd(), args.project);
51
+ if (fs.existsSync(targetDir)) {
52
+ this.error(`Directory ${targetDir} already exists.`);
53
+ }
54
+ const task = new Listr([
55
+ {
56
+ async task(ctx) {
57
+ const backend = await getBackend();
58
+ ctx.initOutput = await backend.init({ project: args.project, workspace: flags.workspace });
59
+ },
60
+ title: 'Initializing project',
61
+ },
62
+ {
63
+ async task(ctx, task) {
64
+ const executor$ = await getExecutorForWorkspace(flags.workspace);
65
+ if (!executor$.success)
66
+ throw new Error(executor$.reason);
67
+ const userParams = arrayOfStringToObject(flags.parameter);
68
+ const tokenResult = await hereyaTokenUtils.generateHereyaToken(`Template: ${args.project}`);
69
+ const provisionOutput = await executor$.executor.provision({
70
+ logger: getLogger(task),
71
+ package: flags.template,
72
+ parameters: {
73
+ ...userParams,
74
+ projectName: args.project,
75
+ workspace: flags.workspace,
76
+ ...(tokenResult ? { hereyaCloudUrl: tokenResult.cloudUrl, hereyaToken: tokenResult.token } : {}),
77
+ },
78
+ project: args.project,
79
+ workspace: flags.workspace,
80
+ });
81
+ if (!provisionOutput.success)
82
+ throw new Error(provisionOutput.reason);
83
+ ctx.provisionOutput = provisionOutput;
84
+ },
85
+ title: 'Provisioning template',
86
+ },
87
+ {
88
+ async task(ctx) {
89
+ const backend = await getBackend();
90
+ if (!backend.saveProjectMetadata) {
91
+ throw new Error('Templates require cloud backend. Run `hereya login` first.');
92
+ }
93
+ const result = await backend.saveProjectMetadata({
94
+ metadata: { env: ctx.provisionOutput.env, template: flags.template },
95
+ project: args.project,
96
+ });
97
+ if (!result.success)
98
+ throw new Error(result.reason);
99
+ },
100
+ title: 'Saving template metadata',
101
+ },
102
+ {
103
+ async task(ctx) {
104
+ const gitEnvToResolve = {};
105
+ for (const [key, value] of Object.entries(ctx.provisionOutput.env)) {
106
+ if (key.startsWith('hereyaGit'))
107
+ gitEnvToResolve[key] = value;
108
+ }
109
+ const executor$ = await getExecutorForWorkspace(flags.workspace);
110
+ if (!executor$.success)
111
+ throw new Error(executor$.reason);
112
+ ctx.resolvedGitEnv = await executor$.executor.resolveEnvValues({ env: gitEnvToResolve });
113
+ if (!ctx.resolvedGitEnv.hereyaGitRemoteUrl) {
114
+ throw new Error('Template did not provide hereyaGitRemoteUrl. Templates must export hereyaGitRemoteUrl.');
115
+ }
116
+ },
117
+ title: 'Resolving git credentials',
118
+ },
119
+ {
120
+ async task(ctx) {
121
+ const cloneResult = await gitUtils.cloneWithCredentialHelper({
122
+ gitUrl: ctx.resolvedGitEnv.hereyaGitRemoteUrl,
123
+ hereyaBinPath: process.argv[1],
124
+ password: ctx.resolvedGitEnv.hereyaGitPassword,
125
+ targetDir,
126
+ username: ctx.resolvedGitEnv.hereyaGitUsername,
127
+ });
128
+ if (!cloneResult.success)
129
+ throw new Error(cloneResult.reason);
130
+ },
131
+ title: 'Cloning project repository',
132
+ },
133
+ {
134
+ async task(ctx) {
135
+ const config = {
136
+ project: ctx.initOutput.project.name,
137
+ workspace: ctx.initOutput.workspace.name,
138
+ };
139
+ const configManager = getConfigManager();
140
+ await configManager.saveConfig({ config, projectRootDir: targetDir });
141
+ await gitUtils.setupCredentialHelper({ hereyaBinPath: process.argv[1], projectDir: targetDir });
142
+ const backend = await getBackend();
143
+ await backend.saveState(config, flags.workspace);
144
+ },
145
+ title: 'Setting up project',
146
+ },
147
+ ], { concurrent: false });
148
+ try {
149
+ await task.run();
150
+ const myLogger = new ListrLogger({ useIcons: false });
151
+ myLogger.log(ListrLogLevels.COMPLETED, `Project ${args.project} scaffolded from template ${flags.template}`);
152
+ myLogger.log(ListrLogLevels.COMPLETED, `Project directory: ${targetDir}`);
153
+ }
154
+ catch (error) {
155
+ this.error(`${error.message}\n\nSee ${getLogPath()} for more details`);
156
+ }
157
+ return;
158
+ }
159
+ // Non-template flow (existing behavior, unchanged)
27
160
  const configManager = getConfigManager();
28
161
  const config$ = await configManager.loadConfig({ projectRootDir });
29
162
  if (config$.found) {
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Uninit extends Command {
3
+ static args: {
4
+ project: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ chdir: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }