@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.
- package/README.md +224 -0
- package/bin/relesio.js +4 -0
- package/dist/commands/auth/index.d.ts +2 -0
- package/dist/commands/auth/index.js +9 -0
- package/dist/commands/auth/login.d.ts +2 -0
- package/dist/commands/auth/login.js +82 -0
- package/dist/commands/auth/logout.d.ts +2 -0
- package/dist/commands/auth/logout.js +25 -0
- package/dist/commands/auth/status.d.ts +2 -0
- package/dist/commands/auth/status.js +35 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.js +293 -0
- package/dist/commands/organizations/index.d.ts +2 -0
- package/dist/commands/organizations/index.js +8 -0
- package/dist/commands/organizations/list.d.ts +2 -0
- package/dist/commands/organizations/list.js +54 -0
- package/dist/commands/organizations/set.d.ts +2 -0
- package/dist/commands/organizations/set.js +34 -0
- package/dist/commands/projects/index.d.ts +2 -0
- package/dist/commands/projects/index.js +6 -0
- package/dist/commands/projects/list.d.ts +2 -0
- package/dist/commands/projects/list.js +49 -0
- package/dist/commands/rollback.d.ts +2 -0
- package/dist/commands/rollback.js +119 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +59 -0
- package/dist/commands/team/get.d.ts +2 -0
- package/dist/commands/team/get.js +34 -0
- package/dist/commands/team/index.d.ts +2 -0
- package/dist/commands/team/index.js +7 -0
- package/dist/commands/team/list.d.ts +2 -0
- package/dist/commands/team/list.js +46 -0
- package/dist/commands/upload.d.ts +2 -0
- package/dist/commands/upload.js +365 -0
- package/dist/commands/versions/index.d.ts +2 -0
- package/dist/commands/versions/index.js +6 -0
- package/dist/commands/versions/list.d.ts +2 -0
- package/dist/commands/versions/list.js +51 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +52 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +90 -0
- package/dist/lib/api/client.d.ts +32 -0
- package/dist/lib/api/client.js +187 -0
- package/dist/lib/api/deployments.d.ts +122 -0
- package/dist/lib/api/deployments.js +55 -0
- package/dist/lib/api/organizations.d.ts +27 -0
- package/dist/lib/api/organizations.js +37 -0
- package/dist/lib/api/projects.d.ts +38 -0
- package/dist/lib/api/projects.js +34 -0
- package/dist/lib/api/teams.d.ts +15 -0
- package/dist/lib/api/teams.js +44 -0
- package/dist/lib/api/types.d.ts +89 -0
- package/dist/lib/api/types.js +2 -0
- package/dist/lib/api/versions.d.ts +140 -0
- package/dist/lib/api/versions.js +53 -0
- package/dist/lib/config/manager.d.ts +35 -0
- package/dist/lib/config/manager.js +78 -0
- package/dist/lib/config/types.d.ts +15 -0
- package/dist/lib/config/types.js +15 -0
- package/dist/lib/errors/handler.d.ts +1 -0
- package/dist/lib/errors/handler.js +75 -0
- package/dist/lib/errors/types.d.ts +19 -0
- package/dist/lib/errors/types.js +34 -0
- package/dist/lib/files/scanner.d.ts +23 -0
- package/dist/lib/files/scanner.js +81 -0
- package/dist/lib/git/metadata.d.ts +31 -0
- package/dist/lib/git/metadata.js +90 -0
- package/dist/lib/output/colors.d.ts +14 -0
- package/dist/lib/output/colors.js +23 -0
- package/dist/lib/output/formatter.d.ts +2 -0
- package/dist/lib/output/formatter.js +7 -0
- package/dist/lib/output/table.d.ts +7 -0
- package/dist/lib/output/table.js +64 -0
- 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,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,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,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,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,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,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
|
+
});
|