@relesio/cli 0.2.6

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.
Files changed (75) hide show
  1. package/README.md +224 -0
  2. package/bin/relesio.js +4 -0
  3. package/dist/commands/auth/index.d.ts +2 -0
  4. package/dist/commands/auth/index.js +9 -0
  5. package/dist/commands/auth/login.d.ts +2 -0
  6. package/dist/commands/auth/login.js +82 -0
  7. package/dist/commands/auth/logout.d.ts +2 -0
  8. package/dist/commands/auth/logout.js +25 -0
  9. package/dist/commands/auth/status.d.ts +2 -0
  10. package/dist/commands/auth/status.js +35 -0
  11. package/dist/commands/deploy.d.ts +2 -0
  12. package/dist/commands/deploy.js +293 -0
  13. package/dist/commands/organizations/index.d.ts +2 -0
  14. package/dist/commands/organizations/index.js +8 -0
  15. package/dist/commands/organizations/list.d.ts +2 -0
  16. package/dist/commands/organizations/list.js +54 -0
  17. package/dist/commands/organizations/set.d.ts +2 -0
  18. package/dist/commands/organizations/set.js +34 -0
  19. package/dist/commands/projects/index.d.ts +2 -0
  20. package/dist/commands/projects/index.js +6 -0
  21. package/dist/commands/projects/list.d.ts +2 -0
  22. package/dist/commands/projects/list.js +49 -0
  23. package/dist/commands/rollback.d.ts +2 -0
  24. package/dist/commands/rollback.js +119 -0
  25. package/dist/commands/status.d.ts +2 -0
  26. package/dist/commands/status.js +59 -0
  27. package/dist/commands/team/get.d.ts +2 -0
  28. package/dist/commands/team/get.js +34 -0
  29. package/dist/commands/team/index.d.ts +2 -0
  30. package/dist/commands/team/index.js +7 -0
  31. package/dist/commands/team/list.d.ts +2 -0
  32. package/dist/commands/team/list.js +46 -0
  33. package/dist/commands/upload.d.ts +2 -0
  34. package/dist/commands/upload.js +365 -0
  35. package/dist/commands/versions/index.d.ts +2 -0
  36. package/dist/commands/versions/index.js +6 -0
  37. package/dist/commands/versions/list.d.ts +2 -0
  38. package/dist/commands/versions/list.js +51 -0
  39. package/dist/commands/whoami.d.ts +2 -0
  40. package/dist/commands/whoami.js +52 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +90 -0
  43. package/dist/lib/api/client.d.ts +32 -0
  44. package/dist/lib/api/client.js +187 -0
  45. package/dist/lib/api/deployments.d.ts +122 -0
  46. package/dist/lib/api/deployments.js +55 -0
  47. package/dist/lib/api/organizations.d.ts +27 -0
  48. package/dist/lib/api/organizations.js +37 -0
  49. package/dist/lib/api/projects.d.ts +38 -0
  50. package/dist/lib/api/projects.js +34 -0
  51. package/dist/lib/api/teams.d.ts +15 -0
  52. package/dist/lib/api/teams.js +44 -0
  53. package/dist/lib/api/types.d.ts +89 -0
  54. package/dist/lib/api/types.js +2 -0
  55. package/dist/lib/api/versions.d.ts +140 -0
  56. package/dist/lib/api/versions.js +53 -0
  57. package/dist/lib/config/manager.d.ts +35 -0
  58. package/dist/lib/config/manager.js +78 -0
  59. package/dist/lib/config/types.d.ts +15 -0
  60. package/dist/lib/config/types.js +15 -0
  61. package/dist/lib/errors/handler.d.ts +1 -0
  62. package/dist/lib/errors/handler.js +75 -0
  63. package/dist/lib/errors/types.d.ts +19 -0
  64. package/dist/lib/errors/types.js +34 -0
  65. package/dist/lib/files/scanner.d.ts +23 -0
  66. package/dist/lib/files/scanner.js +81 -0
  67. package/dist/lib/git/metadata.d.ts +31 -0
  68. package/dist/lib/git/metadata.js +90 -0
  69. package/dist/lib/output/colors.d.ts +14 -0
  70. package/dist/lib/output/colors.js +23 -0
  71. package/dist/lib/output/formatter.d.ts +2 -0
  72. package/dist/lib/output/formatter.js +7 -0
  73. package/dist/lib/output/table.d.ts +7 -0
  74. package/dist/lib/output/table.js +64 -0
  75. package/package.json +58 -0
@@ -0,0 +1,293 @@
1
+ import { Command } from "commander";
2
+ import inquirer from "inquirer";
3
+ import ora from "ora";
4
+ import chalk from "chalk";
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ import { RelesioAPIClient } from "../lib/api/client.js";
8
+ import { ConfigManager } from "../lib/config/manager.js";
9
+ import { getProjectBySlug } from "../lib/api/projects.js";
10
+ import { listVersions } from "../lib/api/versions.js";
11
+ import { listEnvironments, deployVersion, getEnvironmentBySlug } from "../lib/api/deployments.js";
12
+ import { handleError } from "../lib/errors/handler.js";
13
+ const PACKAGE_JSON = "package.json";
14
+ /**
15
+ * Format a UUID string to include dashes (8-4-4-4-12 format)
16
+ * If already formatted, returns as-is
17
+ */
18
+ function formatUUID(uuid) {
19
+ // Remove any existing dashes
20
+ const cleaned = uuid.replace(/-/g, "");
21
+ // If not 32 hex characters, return as-is (might already be formatted or invalid)
22
+ if (cleaned.length !== 32 || !/^[0-9a-f]+$/i.test(cleaned)) {
23
+ return uuid;
24
+ }
25
+ // Format as 8-4-4-4-12
26
+ return `${cleaned.slice(0, 8)}-${cleaned.slice(8, 12)}-${cleaned.slice(12, 16)}-${cleaned.slice(16, 20)}-${cleaned.slice(20)}`;
27
+ }
28
+ const validateSlug = (input) => {
29
+ if (!input || input.trim().length === 0) {
30
+ return "Project slug is required";
31
+ }
32
+ if (!/^[a-z0-9-]+$/.test(input)) {
33
+ return "Project slug must contain only lowercase letters, numbers, and hyphens";
34
+ }
35
+ return true;
36
+ };
37
+ function slugify(value) {
38
+ if (!value)
39
+ return undefined;
40
+ const slug = value
41
+ .trim()
42
+ .toLowerCase()
43
+ .replace(/[^a-z0-9]+/g, "-")
44
+ .replace(/^-+|-+$/g, "");
45
+ return slug || undefined;
46
+ }
47
+ function loadPackageMetadata(baseDir) {
48
+ const candidates = new Set([
49
+ path.resolve(process.cwd(), PACKAGE_JSON),
50
+ path.resolve(baseDir, PACKAGE_JSON),
51
+ path.resolve(path.dirname(baseDir), PACKAGE_JSON)
52
+ ]);
53
+ for (const candidate of candidates) {
54
+ if (!fs.existsSync(candidate))
55
+ continue;
56
+ try {
57
+ const content = fs.readFileSync(candidate, "utf8");
58
+ const parsed = JSON.parse(content);
59
+ return parsed;
60
+ }
61
+ catch {
62
+ // Ignore parse errors and keep searching
63
+ }
64
+ }
65
+ return {};
66
+ }
67
+ export const deployCommand = new Command("deploy")
68
+ .description("Deploy a version to an environment")
69
+ .argument("[project-slug]", "Project slug (optional if package.json exists)")
70
+ .argument("[version]", "Version to deploy (e.g., 1.0.0)")
71
+ .option("-e, --env <environment>", "Environment slug (production, staging, etc.)")
72
+ .option("--latest", "Deploy the latest version")
73
+ .option("-y, --yes", "Skip confirmation prompts")
74
+ .option("--json", "Output as JSON")
75
+ .action(async (projectSlugArg, versionArg, options) => {
76
+ const spinner = ora("Preparing deployment...").start();
77
+ try {
78
+ const config = ConfigManager.load();
79
+ if (!config.apiToken) {
80
+ spinner.fail("Not authenticated. Run 'relesio auth login' first.");
81
+ process.exit(1);
82
+ }
83
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
84
+ // Load package.json metadata
85
+ const packageMetadata = loadPackageMetadata(process.cwd());
86
+ const inferredSlug = slugify(packageMetadata.name);
87
+ // Determine project slug
88
+ let projectSlug = projectSlugArg || "";
89
+ if (!projectSlug &&
90
+ inferredSlug &&
91
+ validateSlug(inferredSlug) === true) {
92
+ projectSlug = inferredSlug;
93
+ spinner.info(`Using project slug from package.json: ${chalk.cyan(projectSlug)}`);
94
+ spinner.start("Preparing deployment...");
95
+ }
96
+ if (!projectSlug) {
97
+ spinner.fail("Project slug is required. Provide it as an argument or ensure package.json has a 'name' field.");
98
+ process.exit(1);
99
+ }
100
+ const validationResult = validateSlug(projectSlug);
101
+ if (validationResult !== true) {
102
+ spinner.fail(String(validationResult));
103
+ process.exit(1);
104
+ }
105
+ // Get project
106
+ spinner.text = "Looking up project...";
107
+ const project = await getProjectBySlug(client, projectSlug);
108
+ if (!project) {
109
+ spinner.fail(`Project '${projectSlug}' not found`);
110
+ // If DEBUG is enabled, show available projects to help diagnose the issue
111
+ if (process.env.DEBUG) {
112
+ const { listProjects } = await import("../lib/api/projects.js");
113
+ const { projects } = await listProjects(client);
114
+ console.error(chalk.dim(`\n[DEBUG] Available projects in this organization (${projects.length} total):`));
115
+ if (projects.length > 0) {
116
+ projects.forEach((p) => {
117
+ console.error(chalk.dim(` - ${p.slug} (name: "${p.name}", id: ${p.id})`));
118
+ });
119
+ }
120
+ else {
121
+ console.error(chalk.dim(` (no projects found)`));
122
+ }
123
+ console.error(chalk.dim(`[DEBUG] Looking for: "${projectSlug}"\n`));
124
+ }
125
+ console.log(chalk.yellow(`\nThe project '${projectSlug}' does not exist yet.\n\n` +
126
+ `Possible reasons:\n` +
127
+ ` 1. You haven't uploaded any versions yet\n` +
128
+ ` 2. The project slug might be incorrect\n` +
129
+ ` 3. The project exists in a different organization\n\n` +
130
+ `Next steps:\n` +
131
+ ` • Upload a version first: ${chalk.cyan(`relesio upload`)}\n` +
132
+ ` • List available projects: ${chalk.cyan(`relesio projects list`)}\n` +
133
+ ` • Or specify the correct project slug: ${chalk.cyan(`relesio deploy <project-slug> --env stage`)}\n`));
134
+ process.exit(1);
135
+ }
136
+ spinner.text = "Loading environments and versions...";
137
+ // Get environments
138
+ const { environments } = await listEnvironments(client, project.id);
139
+ if (environments.length === 0) {
140
+ spinner.fail("No environments found for this project");
141
+ process.exit(1);
142
+ }
143
+ // Get versions
144
+ const { versions } = await listVersions(client, project.id);
145
+ const completedVersions = versions.filter((v) => v.status === "completed");
146
+ if (completedVersions.length === 0) {
147
+ spinner.fail("No completed versions available to deploy");
148
+ process.exit(1);
149
+ }
150
+ spinner.stop();
151
+ // Select environment (if not provided)
152
+ let selectedEnv = options.env
153
+ ? await getEnvironmentBySlug(client, project.id, options.env)
154
+ : null;
155
+ if (!selectedEnv) {
156
+ const { environmentSlug } = await inquirer.prompt([
157
+ {
158
+ type: "list",
159
+ name: "environmentSlug",
160
+ message: "Select environment:",
161
+ choices: environments.map((env) => ({
162
+ name: `${env.name} (${env.slug})${env.currentDeployment
163
+ ? ` - current: ${env.currentDeployment
164
+ .semanticVersion ||
165
+ env.currentDeployment.versionId}`
166
+ : ""}`,
167
+ value: env.slug
168
+ }))
169
+ }
170
+ ]);
171
+ selectedEnv = await getEnvironmentBySlug(client, project.id, environmentSlug);
172
+ }
173
+ if (!selectedEnv) {
174
+ console.error(chalk.red("Environment not found"));
175
+ process.exit(1);
176
+ }
177
+ // Select version (if not provided)
178
+ let selectedVersionId;
179
+ let selectedVersionStr;
180
+ if (options.latest) {
181
+ // Use the most recent version
182
+ const latestVersion = completedVersions[0];
183
+ selectedVersionId = latestVersion.id;
184
+ selectedVersionStr =
185
+ latestVersion.semanticVersion || latestVersion.id;
186
+ }
187
+ else if (versionArg) {
188
+ // Find version by string (match against semanticVersion)
189
+ const version = completedVersions.find((v) => v.semanticVersion === versionArg);
190
+ if (!version) {
191
+ console.error(chalk.red(`Version '${versionArg}' not found or not completed`));
192
+ process.exit(1);
193
+ }
194
+ selectedVersionId = version.id;
195
+ selectedVersionStr = version.semanticVersion || version.id;
196
+ }
197
+ else if (packageMetadata.version) {
198
+ // Try to use version from package.json
199
+ const version = completedVersions.find((v) => v.semanticVersion === packageMetadata.version);
200
+ if (version) {
201
+ selectedVersionId = version.id;
202
+ selectedVersionStr =
203
+ version.semanticVersion || version.id;
204
+ console.log(chalk.dim(`Using version from package.json: ${chalk.cyan(selectedVersionStr)}`));
205
+ }
206
+ else {
207
+ // Version from package.json not found, fall back to interactive
208
+ console.log(chalk.yellow(`Version '${packageMetadata.version}' from package.json not found or not completed. Please select a version:`));
209
+ const { versionId } = await inquirer.prompt([
210
+ {
211
+ type: "list",
212
+ name: "versionId",
213
+ message: "Select version to deploy:",
214
+ choices: completedVersions.map((v) => ({
215
+ name: `${v.semanticVersion || v.id} - ${v.gitSha?.substring(0, 7) || "no git"} - ${new Date(v.uploadDate).toLocaleDateString()}`,
216
+ value: v.id
217
+ }))
218
+ }
219
+ ]);
220
+ selectedVersionId = versionId;
221
+ const selectedVersion = completedVersions.find((v) => v.id === versionId);
222
+ if (!selectedVersion)
223
+ throw new Error("Version not found");
224
+ selectedVersionStr =
225
+ selectedVersion.semanticVersion ||
226
+ selectedVersion.id;
227
+ }
228
+ }
229
+ else {
230
+ // Interactive selection
231
+ const { versionId } = await inquirer.prompt([
232
+ {
233
+ type: "list",
234
+ name: "versionId",
235
+ message: "Select version to deploy:",
236
+ choices: completedVersions.map((v) => ({
237
+ name: `${v.semanticVersion || v.id} - ${v.gitSha?.substring(0, 7) || "no git"} - ${new Date(v.uploadDate).toLocaleDateString()}`,
238
+ value: v.id
239
+ }))
240
+ }
241
+ ]);
242
+ selectedVersionId = versionId;
243
+ const version = completedVersions.find((v) => v.id === versionId);
244
+ if (!version)
245
+ throw new Error("Version not found");
246
+ selectedVersionStr = version.semanticVersion || version.id;
247
+ }
248
+ // Confirmation
249
+ if (!options.yes) {
250
+ console.log(`\n${chalk.bold("Deployment Details:")}\n` +
251
+ ` Project: ${chalk.cyan(project.name)} (${project.slug})\n` +
252
+ ` Environment: ${chalk.cyan(selectedEnv.name)} (${selectedEnv.slug})${selectedEnv.isProduction ? chalk.red(" [PRODUCTION]") : ""}\n` +
253
+ ` Version: ${chalk.cyan(selectedVersionStr)}\n`);
254
+ if (selectedEnv.currentDeployment) {
255
+ console.log(` Current: ${chalk.yellow(selectedEnv.currentDeployment.semanticVersion ||
256
+ selectedEnv.currentDeployment.versionId)}\n`);
257
+ }
258
+ const { confirm } = await inquirer.prompt([
259
+ {
260
+ type: "confirm",
261
+ name: "confirm",
262
+ message: `Deploy version ${selectedVersionStr} to ${selectedEnv.name}?`,
263
+ default: false
264
+ }
265
+ ]);
266
+ if (!confirm) {
267
+ console.log(chalk.yellow("\nDeployment cancelled"));
268
+ return;
269
+ }
270
+ }
271
+ // Deploy
272
+ const deploySpinner = ora("Deploying...").start();
273
+ // Format environment ID with dashes if needed (API expects UUID format)
274
+ const formattedEnvId = formatUUID(selectedEnv.id);
275
+ const result = await deployVersion(client, project.id, formattedEnvId, { versionId: selectedVersionId, projectId: project.id });
276
+ deploySpinner.succeed(`Deployed version ${chalk.green(selectedVersionStr)} to ${chalk.green(selectedEnv.name)}`);
277
+ if (options.json) {
278
+ console.log(JSON.stringify(result, null, 2));
279
+ }
280
+ else {
281
+ console.log(`\n${chalk.bold("Deployment successful!")}\n` +
282
+ ` URL: ${chalk.cyan(result.url)}\n` +
283
+ ` Deployment ID: ${result.deployment.id}\n`);
284
+ if (selectedEnv.isProduction) {
285
+ console.log(chalk.yellow("⚠️ This is a production deployment. Monitor closely!\n"));
286
+ }
287
+ }
288
+ }
289
+ catch (error) {
290
+ spinner.fail("Deployment failed");
291
+ handleError(error);
292
+ }
293
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const organizationsCommand: Command;
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ import { listOrganizationsCommand } from "./list.js";
3
+ import { setOrganizationCommand } from "./set.js";
4
+ export const organizationsCommand = new Command("organizations")
5
+ .alias("orgs")
6
+ .description("Manage organizations")
7
+ .addCommand(listOrganizationsCommand)
8
+ .addCommand(setOrganizationCommand);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const listOrganizationsCommand: Command;
@@ -0,0 +1,54 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import Table from "cli-table3";
5
+ import { ConfigManager } from "../../lib/config/manager.js";
6
+ import { RelesioAPIClient } from "../../lib/api/client.js";
7
+ import { handleError } from "../../lib/errors/handler.js";
8
+ export const listOrganizationsCommand = new Command("list")
9
+ .description("List your organizations")
10
+ .option("--json", "Output as JSON")
11
+ .action(async (options) => {
12
+ const spinner = ora("Fetching organizations...").start();
13
+ try {
14
+ const config = ConfigManager.load();
15
+ if (!config.apiToken) {
16
+ spinner.fail("Not authenticated. Run 'relesio auth login' first.");
17
+ process.exit(1);
18
+ }
19
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
20
+ const response = await client.get("/v1/users/me/organizations", { raw: true });
21
+ spinner.succeed("Organizations retrieved");
22
+ const { organizations: orgs, currentOrganizationId } = response.data;
23
+ const activeOrgId = currentOrganizationId || config.activeOrganizationId;
24
+ if (options.json) {
25
+ console.log(JSON.stringify(orgs, null, 2));
26
+ return;
27
+ }
28
+ if (orgs.length === 0) {
29
+ console.log(chalk.yellow("No organizations found"));
30
+ return;
31
+ }
32
+ const table = new Table({
33
+ head: ["", "Name", "Slug", "Role"],
34
+ colWidths: [3, 30, 25, 20]
35
+ });
36
+ for (const org of orgs) {
37
+ const isActive = org.id === activeOrgId;
38
+ table.push([
39
+ isActive ? chalk.green("✓") : "",
40
+ org.name,
41
+ org.slug,
42
+ org.role ?? "-"
43
+ ]);
44
+ }
45
+ console.log(table.toString());
46
+ if (config.activeOrganizationId) {
47
+ console.log(chalk.dim("\n✓ marks your active organization"));
48
+ }
49
+ }
50
+ catch (error) {
51
+ spinner.fail("Failed to fetch organizations");
52
+ handleError(error);
53
+ }
54
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const setOrganizationCommand: Command;
@@ -0,0 +1,34 @@
1
+ import { Command } from "commander";
2
+ import ora from "ora";
3
+ import { ConfigManager } from "../../lib/config/manager.js";
4
+ import { RelesioAPIClient } from "../../lib/api/client.js";
5
+ import { handleError } from "../../lib/errors/handler.js";
6
+ export const setOrganizationCommand = new Command("set")
7
+ .description("Set your active organization")
8
+ .argument("<org-id>", "Organization ID or slug")
9
+ .action(async (orgIdOrSlug) => {
10
+ const spinner = ora("Switching organization...").start();
11
+ try {
12
+ const config = ConfigManager.load();
13
+ if (!config.apiToken) {
14
+ spinner.fail("Not authenticated. Run 'relesio auth login' first.");
15
+ process.exit(1);
16
+ }
17
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
18
+ // Call API to switch active org (updates session)
19
+ const response = await client.post("/v1/users/me/organization", { organizationId: orgIdOrSlug }, { raw: true });
20
+ // Update local config
21
+ config.activeOrganizationId = response.data.organizationId;
22
+ config.activeOrganizationName = response.data.organizationName;
23
+ config.activeOrganizationSlug = response.data.organizationSlug;
24
+ // Legacy fields
25
+ config.currentOrgId = response.data.organizationId;
26
+ config.currentOrgName = response.data.organizationName;
27
+ ConfigManager.save(config);
28
+ spinner.succeed(`Active organization set to: ${response.data.organizationName} (${response.data.organizationSlug})`);
29
+ }
30
+ catch (error) {
31
+ spinner.fail("Failed to switch organization");
32
+ handleError(error);
33
+ }
34
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const projectsCommand: Command;
@@ -0,0 +1,6 @@
1
+ import { Command } from "commander";
2
+ import { listProjectsCommand } from "./list.js";
3
+ export const projectsCommand = new Command("projects")
4
+ .alias("project")
5
+ .description("Manage projects")
6
+ .addCommand(listProjectsCommand);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const listProjectsCommand: Command;
@@ -0,0 +1,49 @@
1
+ import { Command } from "commander";
2
+ import ora from "ora";
3
+ import { RelesioAPIClient } from "../../lib/api/client.js";
4
+ import { ConfigManager } from "../../lib/config/manager.js";
5
+ import { listProjects } from "../../lib/api/projects.js";
6
+ import { formatTable } from "../../lib/output/table.js";
7
+ import { handleError } from "../../lib/errors/handler.js";
8
+ export const listProjectsCommand = new Command("list")
9
+ .description("List all projects in your organization")
10
+ .option("--json", "Output as JSON")
11
+ .action(async (options, command) => {
12
+ const spinner = ora("Loading projects...").start();
13
+ try {
14
+ const config = ConfigManager.load();
15
+ if (!config.apiToken) {
16
+ spinner.fail("Not authenticated. Run 'relesio auth login' first.");
17
+ process.exit(1);
18
+ }
19
+ // Get org context from --org flag or config
20
+ const orgId = command.parent?.opts().org || config.activeOrganizationId;
21
+ if (!orgId) {
22
+ spinner.fail("No organization context. Use --org flag, set RELESIO_ORG_ID, or run 'relesio organizations set <org-id>'");
23
+ process.exit(1);
24
+ }
25
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
26
+ // Note: API client automatically adds x-organization-id header based on orgId
27
+ const { projects, count } = await listProjects(client);
28
+ spinner.stop();
29
+ if (options.json) {
30
+ console.log(JSON.stringify({ projects, count }, null, 2));
31
+ return;
32
+ }
33
+ if (projects.length === 0) {
34
+ console.log("\n✨ No projects yet. Create one with:");
35
+ console.log(" relesio projects create");
36
+ return;
37
+ }
38
+ console.log(`\n📦 Projects (${count}):\n`);
39
+ console.log(formatTable(["Name", "Slug", "Created"], projects.map((p) => [
40
+ p.name,
41
+ p.slug,
42
+ new Date(p.createdAt).toLocaleDateString()
43
+ ])));
44
+ }
45
+ catch (error) {
46
+ spinner.fail("Failed to list projects");
47
+ handleError(error);
48
+ }
49
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const rollbackCommand: Command;
@@ -0,0 +1,119 @@
1
+ import { Command } from "commander";
2
+ import inquirer from "inquirer";
3
+ import ora from "ora";
4
+ import chalk from "chalk";
5
+ import { RelesioAPIClient } from "../lib/api/client.js";
6
+ import { ConfigManager } from "../lib/config/manager.js";
7
+ import { getProjectBySlug } from "../lib/api/projects.js";
8
+ import { listEnvironments, rollback, getEnvironmentBySlug, getDeploymentHistory } from "../lib/api/deployments.js";
9
+ import { handleError } from "../lib/errors/handler.js";
10
+ export const rollbackCommand = new Command("rollback")
11
+ .description("Rollback to the previous version in an environment")
12
+ .argument("<project-slug>", "Project slug")
13
+ .option("-e, --env <environment>", "Environment slug (production, staging, etc.)")
14
+ .option("-y, --yes", "Skip confirmation prompts")
15
+ .option("--json", "Output as JSON")
16
+ .action(async (projectSlug, options) => {
17
+ const spinner = ora("Preparing rollback...").start();
18
+ try {
19
+ const config = ConfigManager.load();
20
+ if (!config.apiToken) {
21
+ spinner.fail("Not authenticated. Run 'relesio auth login' first.");
22
+ process.exit(1);
23
+ }
24
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
25
+ // Get project
26
+ const project = await getProjectBySlug(client, projectSlug);
27
+ if (!project) {
28
+ spinner.fail(`Project '${projectSlug}' not found`);
29
+ process.exit(1);
30
+ }
31
+ spinner.text = "Loading environments...";
32
+ // Get environments
33
+ const { environments } = await listEnvironments(client, project.id);
34
+ const envsWithDeployments = environments.filter((env) => env.currentDeployment);
35
+ if (envsWithDeployments.length === 0) {
36
+ spinner.fail("No environments with deployments found");
37
+ process.exit(1);
38
+ }
39
+ spinner.stop();
40
+ // Select environment (if not provided)
41
+ let selectedEnv = options.env
42
+ ? await getEnvironmentBySlug(client, project.id, options.env)
43
+ : null;
44
+ if (!selectedEnv) {
45
+ const { environmentSlug } = await inquirer.prompt([
46
+ {
47
+ type: "list",
48
+ name: "environmentSlug",
49
+ message: "Select environment to rollback:",
50
+ choices: envsWithDeployments.map((env) => ({
51
+ name: `${env.name} (${env.slug}) - current: ${env.currentDeployment?.semanticVersion ||
52
+ env.currentDeployment?.version ||
53
+ env.currentDeployment?.versionId}`,
54
+ value: env.slug
55
+ }))
56
+ }
57
+ ]);
58
+ selectedEnv = await getEnvironmentBySlug(client, project.id, environmentSlug);
59
+ }
60
+ if (!selectedEnv || !selectedEnv.currentDeployment) {
61
+ console.error(chalk.red("Environment not found or has no deployments"));
62
+ process.exit(1);
63
+ }
64
+ // Get deployment history
65
+ const historySpinner = ora("Loading deployment history...").start();
66
+ const { deployments } = await getDeploymentHistory(client, project.id, selectedEnv.id, 5);
67
+ historySpinner.stop();
68
+ if (deployments.length < 2) {
69
+ console.error(chalk.red("Cannot rollback: No previous version found in deployment history"));
70
+ process.exit(1);
71
+ }
72
+ const currentVersion = deployments[0].semanticVersion || deployments[0].versionId;
73
+ const previousVersion = deployments[1].semanticVersion || deployments[1].versionId;
74
+ // Confirmation
75
+ if (!options.yes) {
76
+ console.log(`\n${chalk.bold("Rollback Details:")}\n` +
77
+ ` Project: ${chalk.cyan(project.name)} (${project.slug})\n` +
78
+ ` Environment: ${chalk.cyan(selectedEnv.name)} (${selectedEnv.slug})${selectedEnv.isProduction ? chalk.red(" [PRODUCTION]") : ""}\n` +
79
+ ` Current: ${chalk.yellow(currentVersion)}\n` +
80
+ ` Rollback to: ${chalk.green(previousVersion)}\n`);
81
+ console.log(chalk.bold("\nRecent deployments:"));
82
+ deployments.slice(0, 3).forEach((d, i) => {
83
+ console.log(` ${i === 0 ? "→" : " "} ${d.semanticVersion || d.versionId} - ${d.deploymentType} by ${d.deployedBy} (${new Date(d.deployedAt).toLocaleString()})`);
84
+ });
85
+ const { confirm } = await inquirer.prompt([
86
+ {
87
+ type: "confirm",
88
+ name: "confirm",
89
+ message: `Rollback ${selectedEnv.name} from ${currentVersion} to ${previousVersion}?`,
90
+ default: false
91
+ }
92
+ ]);
93
+ if (!confirm) {
94
+ console.log(chalk.yellow("\nRollback cancelled"));
95
+ return;
96
+ }
97
+ }
98
+ // Rollback
99
+ const rollbackSpinner = ora("Rolling back...").start();
100
+ const result = await rollback(client, project.id, selectedEnv.id);
101
+ rollbackSpinner.succeed(`Rolled back to version ${chalk.green(result.version)}`);
102
+ if (options.json) {
103
+ console.log(JSON.stringify(result, null, 2));
104
+ }
105
+ else {
106
+ console.log(`\n${chalk.bold("Rollback successful!")}\n` +
107
+ ` Previous version: ${chalk.red(result.rolledBackFrom)}\n` +
108
+ ` Current version: ${chalk.green(result.version)}\n` +
109
+ ` Propagation: <30 seconds\n`);
110
+ if (selectedEnv.isProduction) {
111
+ console.log(chalk.yellow("⚠️ Production rolled back. Verify the application is working correctly.\n"));
112
+ }
113
+ }
114
+ }
115
+ catch (error) {
116
+ spinner.fail("Rollback failed");
117
+ handleError(error);
118
+ }
119
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const statusCommand: Command;
@@ -0,0 +1,59 @@
1
+ import { Command } from "commander";
2
+ import ora from "ora";
3
+ import chalk from "chalk";
4
+ import { RelesioAPIClient } from "../lib/api/client.js";
5
+ import { ConfigManager } from "../lib/config/manager.js";
6
+ import { getProjectBySlug } from "../lib/api/projects.js";
7
+ import { getDeploymentStatus } from "../lib/api/deployments.js";
8
+ import { formatTable } from "../lib/output/table.js";
9
+ import { handleError } from "../lib/errors/handler.js";
10
+ export const statusCommand = new Command("status")
11
+ .description("Show deployment status across all environments")
12
+ .argument("<project-slug>", "Project slug")
13
+ .option("--json", "Output as JSON")
14
+ .action(async (projectSlug, options) => {
15
+ const spinner = ora("Loading deployment status...").start();
16
+ try {
17
+ const config = ConfigManager.load();
18
+ if (!config.apiToken) {
19
+ spinner.fail("Not authenticated. Run 'relesio auth login' first.");
20
+ process.exit(1);
21
+ }
22
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
23
+ // Get project
24
+ const project = await getProjectBySlug(client, projectSlug);
25
+ if (!project) {
26
+ spinner.fail(`Project '${projectSlug}' not found`);
27
+ process.exit(1);
28
+ }
29
+ const result = await getDeploymentStatus(client, project.id);
30
+ spinner.stop();
31
+ if (options.json) {
32
+ console.log(JSON.stringify(result, null, 2));
33
+ return;
34
+ }
35
+ console.log(`\n📊 Deployment Status for ${chalk.bold(project.name)}\n`);
36
+ if (result.status.length === 0) {
37
+ console.log(" No environments configured\n");
38
+ return;
39
+ }
40
+ console.log(formatTable(["Environment", "Type", "Version", "Deployed"], result.status.map((s) => [
41
+ s.environment.name,
42
+ s.environment.isProduction
43
+ ? chalk.red("production")
44
+ : chalk.gray("staging"),
45
+ s.currentDeployment
46
+ ? chalk.green(s.currentDeployment.semanticVersion ||
47
+ s.currentDeployment.versionId)
48
+ : chalk.gray("-"),
49
+ s.currentDeployment
50
+ ? new Date(s.currentDeployment.deployedAt).toLocaleString()
51
+ : "-"
52
+ ])));
53
+ console.log();
54
+ }
55
+ catch (error) {
56
+ spinner.fail("Failed to get deployment status");
57
+ handleError(error);
58
+ }
59
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const getCommand: Command;