hereya-cli 0.50.0 → 0.52.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.
@@ -1,7 +1,9 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
+ import { getBackend } from '../../../backend/index.js';
2
3
  import { getConfigManager } from '../../../lib/config/index.js';
3
4
  import { buildFlowWorkspaceName } from '../../../lib/flow-utils.js';
4
5
  import { setDebug } from '../../../lib/log.js';
6
+ import { validateFlowCommand } from '../../../lib/workspace-validation.js';
5
7
  import Up from '../../up/index.js';
6
8
  import WorkspaceCreate from '../../workspace/create/index.js';
7
9
  export default class FlowUp extends Command {
@@ -61,8 +63,18 @@ export default class FlowUp extends Command {
61
63
  project: loadConfigOutput.config.project,
62
64
  projectRootDir,
63
65
  });
66
+ // Validate that if workspace exists, it's not a deployment workspace
67
+ const backend = await getBackend();
68
+ const validation = await validateFlowCommand(workspaceName, backend);
69
+ if (!validation.isValid) {
70
+ this.error(validation.message);
71
+ }
72
+ // Import workspace resolution utility
73
+ const { resolveWorkspaceName } = await import('../../../lib/workspace-utils.js');
74
+ // Resolve mirror workspace name with org prefix
75
+ const mirrorWorkspace = resolveWorkspaceName(loadConfigOutput.config.workspace, loadConfigOutput.config.project);
64
76
  // Create workspace with mirror
65
- const createArgs = [workspaceName, '--mirror', loadConfigOutput.config.workspace];
77
+ const createArgs = [workspaceName, '--mirror', mirrorWorkspace];
66
78
  if (projectRootDir) {
67
79
  createArgs.push('--chdir', projectRootDir);
68
80
  }
@@ -8,6 +8,7 @@ import { getLogger, getLogPath, isDebug, setDebug } from '../../lib/log.js';
8
8
  import { getParameterManager } from '../../lib/parameter/index.js';
9
9
  import { getProfileFromWorkspace } from '../../lib/profile-utils.js';
10
10
  import { delay } from '../../lib/shell.js';
11
+ import { validateDevelopmentWorkspace } from '../../lib/workspace-validation.js';
11
12
  export default class Remove extends Command {
12
13
  static args = {
13
14
  package: Args.string({
@@ -60,6 +61,12 @@ export default class Remove extends Command {
60
61
  ctx.configOutput = loadConfigOutput;
61
62
  // Use workspace from flag if provided, otherwise use config workspace
62
63
  ctx.workspace = flags.workspace || loadConfigOutput.config.workspace;
64
+ // Validate that the workspace is a development workspace
65
+ const backend = await getBackend();
66
+ const validation = await validateDevelopmentWorkspace(ctx.workspace, backend, 'remove');
67
+ if (!validation.isValid) {
68
+ throw new Error(validation.message);
69
+ }
63
70
  const { config } = loadConfigOutput;
64
71
  if (!(ctx.package in (config.packages ?? {})) && !(ctx.package in (config.deploy ?? {}))) {
65
72
  throw new Error(`Package ${ctx.package} not found in the project.`);
@@ -4,6 +4,7 @@ import { getConfigManager } from '../../lib/config/index.js';
4
4
  import { getEnvManager } from '../../lib/env/index.js';
5
5
  import { getProfileFromWorkspace } from '../../lib/profile-utils.js';
6
6
  import { runShell } from '../../lib/shell.js';
7
+ import { validateDevelopmentWorkspace } from '../../lib/workspace-validation.js';
7
8
  export default class Run extends Command {
8
9
  static args = {
9
10
  cmd: Args.string({ description: 'command to run', required: true }),
@@ -43,6 +44,11 @@ export default class Run extends Command {
43
44
  this.error('you must specify a workspace to run the command in');
44
45
  }
45
46
  const backend = await getBackend();
47
+ // Validate that the workspace is a development workspace
48
+ const validation = await validateDevelopmentWorkspace(workspace, backend, 'run');
49
+ if (!validation.isValid) {
50
+ this.error(validation.message);
51
+ }
46
52
  const profile = await getProfileFromWorkspace(backend, workspace, config.project);
47
53
  const envManager = getEnvManager();
48
54
  const getProjectEnvOutput = await envManager.getProjectEnv({
@@ -9,6 +9,7 @@ import { getLogger, getLogPath, isDebug, setDebug } from '../../lib/log.js';
9
9
  import { getParameterManager } from '../../lib/parameter/index.js';
10
10
  import { getProfileFromWorkspace } from '../../lib/profile-utils.js';
11
11
  import { delay } from '../../lib/shell.js';
12
+ import { validateDeploymentWorkspace } from '../../lib/workspace-validation.js';
12
13
  export default class Undeploy extends Command {
13
14
  static description = 'Undeploy a hereya project by removing all resources.';
14
15
  static examples = ['<%= config.bin %> <%= command.id %>'];
@@ -35,6 +36,12 @@ export default class Undeploy extends Command {
35
36
  const { flags } = await this.parse(Undeploy);
36
37
  setDebug(flags.debug);
37
38
  const projectRootDir = path.resolve(flags.chdir || process.env.HEREYA_PROJECT_ROOT_DIR || process.cwd());
39
+ // Validate that the workspace is a deployment workspace
40
+ const backend = await getBackend();
41
+ const validation = await validateDeploymentWorkspace(flags.workspace, backend, 'undeploy');
42
+ if (!validation.isValid) {
43
+ this.error(validation.message);
44
+ }
38
45
  const myLogger = new ListrLogger({ useIcons: false });
39
46
  const task = new Listr([
40
47
  {
@@ -8,6 +8,7 @@ import { getLogger, getLogPath, isDebug, setDebug } from '../../lib/log.js';
8
8
  import { getParameterManager } from '../../lib/parameter/index.js';
9
9
  import { getProfileFromWorkspace } from '../../lib/profile-utils.js';
10
10
  import { delay } from '../../lib/shell.js';
11
+ import { validateDevelopmentWorkspace } from '../../lib/workspace-validation.js';
11
12
  export default class Up extends Command {
12
13
  static description = 'Provision all packages in the project.';
13
14
  static examples = ['<%= config.bin %> <%= command.id %>'];
@@ -58,6 +59,12 @@ export default class Up extends Command {
58
59
  }
59
60
  ctx.configOutput = loadConfigOutput;
60
61
  ctx.workspace = flags.workspace || loadConfigOutput.config.workspace;
62
+ // Validate that the workspace is a development workspace
63
+ const backend = await getBackend();
64
+ const validation = await validateDevelopmentWorkspace(ctx.workspace, backend, 'up');
65
+ if (!validation.isValid) {
66
+ throw new Error(validation.message);
67
+ }
61
68
  await delay(500);
62
69
  },
63
70
  title: 'Loading project config',
@@ -6,6 +6,7 @@ export default class WorkspaceCreate extends Command {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
+ deployment: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
10
  mirror: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  };
@@ -7,6 +7,7 @@ export default class WorkspaceCreate extends Command {
7
7
  static description = 'Create a new workspace if it does not exist.';
8
8
  static examples = ['<%= config.bin %> <%= command.id %> dev'];
9
9
  static flags = {
10
+ deployment: Flags.boolean({ char: 'd', description: 'mark workspace as a deployment workspace', required: false }),
10
11
  mirror: Flags.string({ description: 'workspace to mirror', required: false }),
11
12
  profile: Flags.string({ description: 'workspace profile to set (cloud backend only)', required: false }),
12
13
  };
@@ -14,6 +15,7 @@ export default class WorkspaceCreate extends Command {
14
15
  const { args, flags } = await this.parse(WorkspaceCreate);
15
16
  const backend = await getBackend();
16
17
  const createWorkspaceOutput = await backend.createWorkspace({
18
+ isDeploy: flags.deployment,
17
19
  mirrorOf: flags.mirror,
18
20
  name: args.name,
19
21
  profile: flags.profile,
@@ -19,12 +19,14 @@ export default class WorkspaceList extends Command {
19
19
  const result = await backend.getWorkspace(name);
20
20
  if (result.found && !result.hasError) {
21
21
  return {
22
+ deployment: result.workspace.isDeploy ? 'Yes' : 'No',
22
23
  mirrorOf: result.workspace.mirrorOf || '-',
23
24
  name,
24
25
  profile: result.workspace.profile || '-',
25
26
  };
26
27
  }
27
28
  return {
29
+ deployment: 'No',
28
30
  mirrorOf: '-',
29
31
  name,
30
32
  profile: '-',
@@ -32,6 +34,7 @@ export default class WorkspaceList extends Command {
32
34
  }
33
35
  catch {
34
36
  return {
37
+ deployment: 'No',
35
38
  mirrorOf: '-',
36
39
  name,
37
40
  profile: '-',
@@ -41,13 +44,14 @@ export default class WorkspaceList extends Command {
41
44
  // Calculate column widths for alignment
42
45
  const nameWidth = Math.max(4, ...workspaceDetails.map(w => w.name.length)); // min 4 for "Name"
43
46
  const profileWidth = Math.max(7, ...workspaceDetails.map(w => w.profile.length)); // min 7 for "Profile"
47
+ const deploymentWidth = Math.max(10, ...workspaceDetails.map(w => w.deployment.length)); // min 10 for "Deployment"
44
48
  const mirrorWidth = Math.max(9, ...workspaceDetails.map(w => w.mirrorOf.length)); // min 9 for "Mirror Of"
45
49
  // Display header
46
- this.log(`${'Name'.padEnd(nameWidth)} ${'Profile'.padEnd(profileWidth)} ${'Mirror Of'.padEnd(mirrorWidth)}`);
47
- this.log('-'.repeat(nameWidth + profileWidth + mirrorWidth + 4));
50
+ this.log(`${'Name'.padEnd(nameWidth)} ${'Profile'.padEnd(profileWidth)} ${'Deployment'.padEnd(deploymentWidth)} ${'Mirror Of'.padEnd(mirrorWidth)}`);
51
+ this.log('-'.repeat(nameWidth + profileWidth + deploymentWidth + mirrorWidth + 6));
48
52
  // Display rows
49
53
  for (const workspace of workspaceDetails) {
50
- this.log(`${workspace.name.padEnd(nameWidth)} ${workspace.profile.padEnd(profileWidth)} ${workspace.mirrorOf.padEnd(mirrorWidth)}`);
54
+ this.log(`${workspace.name.padEnd(nameWidth)} ${workspace.profile.padEnd(profileWidth)} ${workspace.deployment.padEnd(deploymentWidth)} ${workspace.mirrorOf.padEnd(mirrorWidth)}`);
51
55
  }
52
56
  }
53
57
  catch (error) {
@@ -6,6 +6,7 @@ export default class WorkspaceSetProfile extends Command {
6
6
  static description: string;
7
7
  static examples: string[];
8
8
  static flags: {
9
+ deployment: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
10
  workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
11
  };
11
12
  run(): Promise<void>;
@@ -4,21 +4,38 @@ export default class WorkspaceSetProfile extends Command {
4
4
  static args = {
5
5
  profile: Args.string({ description: 'AWS profile name to set for the workspace', required: true }),
6
6
  };
7
- static description = 'Set AWS profile for a workspace (cloud backend only).';
7
+ static description = 'Set AWS profile and deployment status for a workspace (cloud backend only).';
8
8
  static examples = ['<%= config.bin %> <%= command.id %> prod-profile -w production'];
9
9
  static flags = {
10
+ deployment: Flags.boolean({
11
+ allowNo: true,
12
+ char: 'd',
13
+ description: 'mark workspace as a deployment workspace (use --no-deployment to unset)',
14
+ required: false,
15
+ }),
10
16
  workspace: Flags.string({ char: 'w', description: 'workspace name', required: true }),
11
17
  };
12
18
  async run() {
13
19
  const { args, flags } = await this.parse(WorkspaceSetProfile);
14
20
  const backend = await getBackend();
15
21
  const updateResult = await backend.updateWorkspace({
22
+ isDeploy: flags.deployment,
16
23
  name: flags.workspace,
17
24
  profile: args.profile,
18
25
  });
19
26
  if (!updateResult.success) {
20
- this.error(`Failed to set profile: ${updateResult.reason}`);
27
+ this.error(`Failed to update workspace: ${updateResult.reason}`);
21
28
  }
22
- this.log(`Profile '${args.profile}' set for workspace '${flags.workspace}' successfully!`);
29
+ const messages = [];
30
+ messages.push(`Profile '${args.profile}' set for workspace '${flags.workspace}'`);
31
+ if (flags.deployment !== undefined) {
32
+ if (flags.deployment) {
33
+ messages.push(`Workspace marked as deployment workspace`);
34
+ }
35
+ else {
36
+ messages.push(`Workspace unmarked as deployment workspace`);
37
+ }
38
+ }
39
+ this.log(messages.join('. ') + '!');
23
40
  }
24
41
  }
@@ -1,6 +1,9 @@
1
1
  import { gitUtils } from './git-utils.js';
2
+ import { addOrgPrefix, extractOrgPrefix } from './org-utils.js';
2
3
  export async function buildFlowWorkspaceName(options) {
3
4
  const { pin, profile, project, projectRootDir } = options;
5
+ // Extract org prefix from project
6
+ const { name: projectName, org } = extractOrgPrefix(project);
4
7
  // Get current git branch
5
8
  const gitBranch = await gitUtils.getCurrentGitBranch(projectRootDir);
6
9
  // Sanitize branch name
@@ -10,13 +13,15 @@ export async function buildFlowWorkspaceName(options) {
10
13
  }
11
14
  // Get commit SHA if pin is enabled
12
15
  const commitSHA = pin ? await gitUtils.getShortCommitSHA(projectRootDir) : null;
13
- // Build workspace name parts
14
- const parts = [project, sanitizedBranch];
16
+ // Build workspace name parts (without org prefix)
17
+ const parts = [projectName, sanitizedBranch];
15
18
  if (profile) {
16
19
  parts.push(profile);
17
20
  }
18
21
  if (commitSHA) {
19
22
  parts.push(commitSHA);
20
23
  }
21
- return parts.join('---');
24
+ // Join parts and add org prefix if present
25
+ const workspaceName = parts.join('---');
26
+ return addOrgPrefix(org, workspaceName);
22
27
  }
@@ -0,0 +1,17 @@
1
+ import { Backend } from '../backend/common.js';
2
+ export interface WorkspaceValidationResult {
3
+ isValid: boolean;
4
+ message?: string;
5
+ }
6
+ /**
7
+ * Validates that a workspace exists and is marked as a deployment workspace (isDeploy=true)
8
+ */
9
+ export declare function validateDeploymentWorkspace(workspaceName: string, backend: Backend, commandName: string): Promise<WorkspaceValidationResult>;
10
+ /**
11
+ * Validates that a workspace exists and is NOT marked as a deployment workspace (isDeploy=false or undefined)
12
+ */
13
+ export declare function validateDevelopmentWorkspace(workspaceName: string, backend: Backend, commandName: string): Promise<WorkspaceValidationResult>;
14
+ /**
15
+ * Validates that flow commands are not used with deployment workspaces
16
+ */
17
+ export declare function validateFlowCommand(workspaceName: string | undefined, backend: Backend): Promise<WorkspaceValidationResult>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Validates that a workspace exists and is marked as a deployment workspace (isDeploy=true)
3
+ */
4
+ export async function validateDeploymentWorkspace(workspaceName, backend, commandName) {
5
+ const result = await backend.getWorkspace(workspaceName);
6
+ if (!result.found) {
7
+ return {
8
+ isValid: false,
9
+ message: `Workspace '${workspaceName}' not found. Create a deployment workspace with:\n$ hereya workspace:create ${workspaceName} --deployment`,
10
+ };
11
+ }
12
+ if (result.hasError) {
13
+ return {
14
+ isValid: false,
15
+ message: `Error accessing workspace '${workspaceName}': ${result.error}`,
16
+ };
17
+ }
18
+ const { workspace } = result;
19
+ if (!workspace.isDeploy) {
20
+ return {
21
+ isValid: false,
22
+ message: `Cannot use '${commandName}' with development workspace '${workspaceName}'.
23
+
24
+ The '${commandName}' command requires a deployment workspace. You have two options:
25
+
26
+ 1. Create a deployment workspace:
27
+ $ hereya workspace:create prod-deployment --deployment
28
+
29
+ 2. Convert existing workspace to deployment (if using cloud backend):
30
+ $ hereya workspace:set-profile ${workspaceName} --deployment
31
+
32
+ Then run: $ hereya ${commandName} -w prod-deployment`,
33
+ };
34
+ }
35
+ return { isValid: true };
36
+ }
37
+ /**
38
+ * Validates that a workspace exists and is NOT marked as a deployment workspace (isDeploy=false or undefined)
39
+ */
40
+ export async function validateDevelopmentWorkspace(workspaceName, backend, commandName) {
41
+ const result = await backend.getWorkspace(workspaceName);
42
+ if (!result.found) {
43
+ return {
44
+ isValid: false,
45
+ message: `Workspace '${workspaceName}' not found. Create a workspace with:\n$ hereya workspace:create ${workspaceName}`,
46
+ };
47
+ }
48
+ if (result.hasError) {
49
+ return {
50
+ isValid: false,
51
+ message: `Error accessing workspace '${workspaceName}': ${result.error}`,
52
+ };
53
+ }
54
+ const { workspace } = result;
55
+ if (workspace.isDeploy === true) {
56
+ return {
57
+ isValid: false,
58
+ message: `Cannot use '${commandName}' with deployment workspace '${workspaceName}'.
59
+
60
+ The '${commandName}' command is for development workspaces only. Deployment workspaces
61
+ should only be managed through 'deploy' and 'undeploy' commands.
62
+
63
+ To work with packages in development:
64
+ 1. Use a development workspace: $ hereya ${commandName} <args> -w dev-workspace
65
+ 2. Or create a new one: $ hereya workspace:create dev-workspace`,
66
+ };
67
+ }
68
+ return { isValid: true };
69
+ }
70
+ /**
71
+ * Validates that flow commands are not used with deployment workspaces
72
+ */
73
+ export async function validateFlowCommand(workspaceName, backend) {
74
+ if (!workspaceName) {
75
+ // If no workspace specified, flow commands will create their own
76
+ return { isValid: true };
77
+ }
78
+ const result = await backend.getWorkspace(workspaceName);
79
+ if (!result.found) {
80
+ // Workspace doesn't exist, flow command will create it
81
+ return { isValid: true };
82
+ }
83
+ if (result.hasError) {
84
+ return {
85
+ isValid: false,
86
+ message: `Error accessing workspace '${workspaceName}': ${result.error}`,
87
+ };
88
+ }
89
+ const { workspace } = result;
90
+ if (workspace.isDeploy === true) {
91
+ return {
92
+ isValid: false,
93
+ message: `Flow commands cannot be used with deployment workspaces.
94
+
95
+ Flow commands are designed for feature branch development workflows.
96
+ For deployment workspaces, use the standard 'deploy' and 'undeploy' commands.
97
+
98
+ Current workspace '${workspaceName}' is marked as a deployment workspace.`,
99
+ };
100
+ }
101
+ return { isValid: true };
102
+ }