@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,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 { TeamsAPI } from "../../lib/api/teams.js";
6
+ import { renderTeamDetails } from "../../lib/output/table.js";
7
+ import { outputJSON, shouldOutputJSON } from "../../lib/output/formatter.js";
8
+ import { AuthenticationError } from "../../lib/errors/types.js";
9
+ export const getCommand = new Command("get")
10
+ .description("Get details of a specific team")
11
+ .argument("<teamId>", "Team ID")
12
+ .action(async (teamId) => {
13
+ const config = ConfigManager.load();
14
+ if (!config.apiToken) {
15
+ throw new AuthenticationError();
16
+ }
17
+ const spinner = ora("Fetching team details...").start();
18
+ try {
19
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
20
+ const teamsAPI = new TeamsAPI(client);
21
+ const team = await teamsAPI.get(teamId);
22
+ spinner.stop();
23
+ if (shouldOutputJSON()) {
24
+ outputJSON(team);
25
+ }
26
+ else {
27
+ renderTeamDetails(team);
28
+ }
29
+ }
30
+ catch (error) {
31
+ spinner.fail("Failed to fetch team details");
32
+ throw error;
33
+ }
34
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const teamCommand: Command;
@@ -0,0 +1,7 @@
1
+ import { Command } from "commander";
2
+ import { listCommand } from "./list.js";
3
+ import { getCommand } from "./get.js";
4
+ export const teamCommand = new Command("team")
5
+ .description("Manage teams")
6
+ .addCommand(listCommand)
7
+ .addCommand(getCommand);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const listCommand: Command;
@@ -0,0 +1,46 @@
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 { TeamsAPI } from "../../lib/api/teams.js";
6
+ import { renderTeamsTable } from "../../lib/output/table.js";
7
+ import { outputJSON, shouldOutputJSON } from "../../lib/output/formatter.js";
8
+ import { AuthenticationError } from "../../lib/errors/types.js";
9
+ export const listCommand = new Command("list")
10
+ .alias("ls")
11
+ .description("List all teams")
12
+ .option("--limit <limit>", "Number of teams to return (default: 50)", "50")
13
+ .option("--offset <offset>", "Offset for pagination (default: 0)", "0")
14
+ .action(async (options, command) => {
15
+ const config = ConfigManager.load();
16
+ if (!config.apiToken) {
17
+ throw new AuthenticationError();
18
+ }
19
+ // Get org context from --org flag or config
20
+ const orgId = command.parent?.opts().org || config.activeOrganizationId;
21
+ if (!orgId) {
22
+ const spinner = ora();
23
+ spinner.fail("No organization context. Use --org flag, set RELESIO_ORG_ID, or run 'relesio organizations set <org-id>'");
24
+ process.exit(1);
25
+ }
26
+ const spinner = ora("Fetching teams...").start();
27
+ try {
28
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
29
+ const teamsAPI = new TeamsAPI(client);
30
+ const response = await teamsAPI.list({
31
+ limit: parseInt(options.limit, 10),
32
+ offset: parseInt(options.offset, 10)
33
+ });
34
+ spinner.stop();
35
+ if (shouldOutputJSON()) {
36
+ outputJSON(response);
37
+ }
38
+ else {
39
+ renderTeamsTable(response.data);
40
+ }
41
+ }
42
+ catch (error) {
43
+ spinner.fail("Failed to fetch teams");
44
+ throw error;
45
+ }
46
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const uploadCommand: Command;
@@ -0,0 +1,365 @@
1
+ import { Command } from "commander";
2
+ import ora from "ora";
3
+ import chalk from "chalk";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { RelesioAPIClient } from "../lib/api/client.js";
7
+ import { ConfigManager } from "../lib/config/manager.js";
8
+ import { getProjectBySlug, createProject } from "../lib/api/projects.js";
9
+ import { createVersion, completeVersion, gitMetadataToApiFormat } from "../lib/api/versions.js";
10
+ import { collectGitMetadata, formatGitMetadata, validateGitState } from "../lib/git/metadata.js";
11
+ import { scanDirectory, formatFileSize, getTotalSize } from "../lib/files/scanner.js";
12
+ import { handleError } from "../lib/errors/handler.js";
13
+ const PACKAGE_JSON = "package.json";
14
+ const validateSlug = (input) => {
15
+ if (!input || input.trim().length === 0) {
16
+ return "Project slug is required";
17
+ }
18
+ if (!/^[a-z0-9-]+$/.test(input)) {
19
+ return "Project slug must contain only lowercase letters, numbers, and hyphens";
20
+ }
21
+ return true;
22
+ };
23
+ function slugify(value) {
24
+ if (!value)
25
+ return undefined;
26
+ const slug = value
27
+ .trim()
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9]+/g, "-")
30
+ .replace(/^-+|-+$/g, "");
31
+ return slug || undefined;
32
+ }
33
+ function loadPackageMetadata(baseDir) {
34
+ const candidates = new Set([
35
+ path.resolve(process.cwd(), PACKAGE_JSON),
36
+ path.resolve(baseDir, PACKAGE_JSON),
37
+ path.resolve(path.dirname(baseDir), PACKAGE_JSON)
38
+ ]);
39
+ for (const candidate of candidates) {
40
+ if (!fs.existsSync(candidate))
41
+ continue;
42
+ try {
43
+ const content = fs.readFileSync(candidate, "utf8");
44
+ const parsed = JSON.parse(content);
45
+ return parsed;
46
+ }
47
+ catch {
48
+ // Ignore parse errors and keep searching
49
+ }
50
+ }
51
+ return {};
52
+ }
53
+ function buildDependencies(pkg) {
54
+ if (!pkg)
55
+ return undefined;
56
+ const deps = pkg.dependencies ?? {};
57
+ const devDeps = pkg.devDependencies ?? {};
58
+ const toList = (obj, type) => Object.entries(obj).map(([name, version]) => ({ name, version, type }));
59
+ const combined = [
60
+ ...toList(deps, "dependency"),
61
+ ...toList(devDeps, "devDependency")
62
+ ];
63
+ return combined.length > 0 ? combined : undefined;
64
+ }
65
+ export const uploadCommand = new Command("upload")
66
+ .description("Upload a new version to a project")
67
+ .argument("[directory]", "Directory containing files to upload (default: dist)", "dist")
68
+ .option("-p, --project <slug>", "Project slug")
69
+ .option("-v, --version <version>", "Version string (e.g., 1.0.0)")
70
+ .option("--require-clean", "Require clean git working directory")
71
+ .option("-y, --yes", "Skip confirmation prompts")
72
+ .option("--json", "Output as JSON")
73
+ .action(async (directory, options) => {
74
+ const spinner = ora("Preparing upload...").start();
75
+ try {
76
+ const config = ConfigManager.load();
77
+ if (!config.apiToken) {
78
+ spinner.fail("Not authenticated. Run 'relesio auth login' first.");
79
+ process.exit(1);
80
+ }
81
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
82
+ const resolvedDirectory = path.resolve(directory || "dist");
83
+ const packageMetadata = loadPackageMetadata(resolvedDirectory);
84
+ const inferredSlug = slugify(packageMetadata.name);
85
+ // Step 1: Collect git metadata
86
+ spinner.text = "Collecting git metadata...";
87
+ const gitMetadata = await collectGitMetadata();
88
+ try {
89
+ validateGitState(gitMetadata, options.requireClean);
90
+ }
91
+ catch (error) {
92
+ spinner.fail(error instanceof Error ? error.message : String(error));
93
+ process.exit(1);
94
+ }
95
+ // Step 2: Scan directory for files
96
+ spinner.text = "Scanning directory...";
97
+ let files;
98
+ try {
99
+ files = await scanDirectory(resolvedDirectory);
100
+ }
101
+ catch (error) {
102
+ spinner.fail(`Failed to scan directory '${resolvedDirectory}': ${error instanceof Error ? error.message : String(error)}`);
103
+ process.exit(1);
104
+ }
105
+ if (files.length === 0) {
106
+ spinner.fail("No files found in directory");
107
+ process.exit(1);
108
+ }
109
+ const totalSize = getTotalSize(files);
110
+ spinner.stop();
111
+ // Step 3: Get or prompt for project
112
+ let projectSlug = options.project || "";
113
+ if (!projectSlug &&
114
+ inferredSlug &&
115
+ validateSlug(inferredSlug) === true) {
116
+ projectSlug = inferredSlug;
117
+ console.log(chalk.dim(`Using project slug from package.json: ${chalk.cyan(projectSlug)}`));
118
+ }
119
+ if (!projectSlug) {
120
+ projectSlug =
121
+ slugify(path.basename(resolvedDirectory)) ||
122
+ slugify(path.basename(process.cwd())) ||
123
+ "project";
124
+ console.log(chalk.dim(`Using inferred project slug: ${chalk.cyan(projectSlug)}`));
125
+ }
126
+ const validationResult = validateSlug(projectSlug);
127
+ if (validationResult !== true) {
128
+ spinner.fail(String(validationResult));
129
+ process.exit(1);
130
+ }
131
+ // Get or create project
132
+ spinner.start("Loading project...");
133
+ let project = await getProjectBySlug(client, projectSlug);
134
+ if (!project) {
135
+ spinner.stop();
136
+ spinner.start("Project not found. Creating...");
137
+ const createResult = await createProject(client, {
138
+ name: projectSlug,
139
+ slug: projectSlug
140
+ });
141
+ project = createResult.project;
142
+ spinner.succeed(`Created project '${project.name}'`);
143
+ }
144
+ else {
145
+ spinner.stop();
146
+ }
147
+ // Step 4: Determine version (non-interactive)
148
+ const semverPattern = /^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$/;
149
+ let versionString = options.version ||
150
+ packageMetadata.version ||
151
+ gitMetadata.tag ||
152
+ "0.0.1";
153
+ const semanticVersion = packageMetadata.version;
154
+ if (!semverPattern.test(versionString)) {
155
+ console.log(chalk.yellow(`Version '${versionString}' is not valid semver. Falling back to 0.0.1.`));
156
+ versionString = "0.0.1";
157
+ }
158
+ if (!options.version && !packageMetadata.version) {
159
+ console.log(chalk.dim(`No package.json version found. Using ${chalk.cyan(versionString)}`));
160
+ }
161
+ // Step 5: Display upload summary (auto-confirm)
162
+ console.log(`\n${chalk.bold("Upload Summary:")}\n` +
163
+ ` Project: ${chalk.cyan(project.name)} (${project.slug})\n` +
164
+ ` Version: ${chalk.cyan(versionString)}\n` +
165
+ ` Files: ${chalk.cyan(files.length.toString())}\n` +
166
+ ` Total Size: ${chalk.cyan(formatFileSize(totalSize))}\n`);
167
+ if (gitMetadata.sha) {
168
+ console.log(chalk.bold("\nGit Metadata:"));
169
+ console.log(formatGitMetadata(gitMetadata));
170
+ }
171
+ console.log(chalk.dim("\nAuto-confirming upload (non-interactive). Proceeding...\n"));
172
+ // Step 6: Create version and get presigned URLs
173
+ spinner.start("Initiating upload...");
174
+ const dependencyList = buildDependencies(packageMetadata);
175
+ const metadata = {};
176
+ if (packageMetadata.name)
177
+ metadata.packageName = packageMetadata.name;
178
+ if (packageMetadata.version)
179
+ metadata.packageVersion = packageMetadata.version;
180
+ if (packageMetadata.dependencies)
181
+ metadata.packageDependencies = packageMetadata.dependencies;
182
+ if (packageMetadata.devDependencies)
183
+ metadata.packageDevDependencies =
184
+ packageMetadata.devDependencies;
185
+ if (dependencyList)
186
+ metadata.dependencies = dependencyList;
187
+ if (gitMetadata.branch)
188
+ metadata.gitBranch = gitMetadata.branch;
189
+ if (gitMetadata.remoteUrl)
190
+ metadata.gitRemote = gitMetadata.remoteUrl;
191
+ metadata.gitDirty = gitMetadata.isDirty;
192
+ const createVersionResponse = await createVersion(client, project.id, {
193
+ version: versionString,
194
+ packageName: packageMetadata.name,
195
+ semanticVersion,
196
+ fileCount: files.length,
197
+ totalSizeBytes: totalSize,
198
+ files: files.map((f) => ({
199
+ path: f.path,
200
+ contentType: f.contentType,
201
+ size: f.size,
202
+ checksum: f.checksum
203
+ })),
204
+ dependencies: dependencyList,
205
+ metadata: Object.keys(metadata).length > 0
206
+ ? Object.fromEntries(Object.entries(metadata).filter(([, value]) => value !== undefined))
207
+ : undefined,
208
+ ...gitMetadataToApiFormat(gitMetadata)
209
+ });
210
+ spinner.succeed(`Version ${versionString} created (${createVersionResponse.versionId})`);
211
+ // Handle duplicate version (content-addressed deduplication)
212
+ if (createVersionResponse.stats?.alreadyExists) {
213
+ console.log(chalk.green("\n✓ Version already exists (identical content)"));
214
+ console.log(chalk.dim(` Files: ${createVersionResponse.stats.existingFileCount}`));
215
+ console.log(chalk.dim(` Preview URL: ${chalk.cyan(createVersionResponse.previewUrl)}`));
216
+ console.log(chalk.dim("\nSkipping upload (no changes needed)\n"));
217
+ return; // Exit early - version complete
218
+ }
219
+ // Display copy stats if incremental upload used (Phase 2)
220
+ if (createVersionResponse.stats &&
221
+ createVersionResponse.stats.copied > 0) {
222
+ console.log(chalk.dim(` Server copied: ${createVersionResponse.stats.copied} unchanged files ` +
223
+ `(saved ${formatFileSize(createVersionResponse.stats.savedBytes)})`));
224
+ }
225
+ // Step 7: Upload files to R2 via presigned URLs (parallel batching)
226
+ const uploadSpinner = ora("Uploading files...").start();
227
+ const CONCURRENT_UPLOADS = 10; // Parallel upload limit
228
+ const uploadUrls = createVersionResponse.uploadUrls;
229
+ if (uploadUrls.length === 0) {
230
+ uploadSpinner.info("No files to upload (all copied server-side)");
231
+ }
232
+ else {
233
+ let uploadedCount = 0;
234
+ // Upload in batches for better performance (prevents 180s timeout)
235
+ for (let i = 0; i < uploadUrls.length; i += CONCURRENT_UPLOADS) {
236
+ const batch = uploadUrls.slice(i, i + CONCURRENT_UPLOADS);
237
+ const uploadPromises = batch.map(async (uploadUrl) => {
238
+ // Find corresponding file
239
+ const file = files.find((f) => f.path === uploadUrl.filePath);
240
+ if (!file) {
241
+ throw new Error(`File not found in manifest: ${uploadUrl.filePath}`);
242
+ }
243
+ try {
244
+ // Read file
245
+ const fileBuffer = fs.readFileSync(file.absolutePath);
246
+ // Convert hex checksum to base64 for AWS ChecksumSHA256 header
247
+ const _checksumBase64 = Buffer.from(file.checksum, "hex").toString("base64");
248
+ // Upload file (either to presigned R2 URL or authenticated API endpoint)
249
+ // NOTE: If using presigned URLs, cannot send x-amz-meta-* headers (signature mismatch)
250
+ // Metadata will be set by worker binding after upload confirmation
251
+ const headers = {
252
+ "Content-Type": file.contentType
253
+ // Note: Content-Length is auto-added by fetch
254
+ };
255
+ // Add auth header if uploading to API endpoint (not presigned R2 URL)
256
+ if (!uploadUrl.uploadUrl.includes("r2.cloudflarestorage.com")) {
257
+ headers.Authorization = `Bearer ${config.apiToken}`;
258
+ }
259
+ const response = await fetch(uploadUrl.uploadUrl, {
260
+ method: "PUT",
261
+ body: fileBuffer,
262
+ headers
263
+ });
264
+ if (!response.ok) {
265
+ // Get response body for detailed error info
266
+ const responseText = await response.text();
267
+ if (process.env.DEBUG) {
268
+ console.error(`\n[DEBUG] Upload failed for ${file.path}`);
269
+ console.error(` Status: ${response.status} ${response.statusText}`);
270
+ console.error(` Response: ${responseText}`);
271
+ console.error(` Headers sent: ${JSON.stringify({
272
+ "Content-Type": file.contentType,
273
+ "Content-Length": file.size.toString(),
274
+ "x-amz-checksum-sha256": Buffer.from(file.checksum, "hex").toString("base64")
275
+ }, null, 2)}`);
276
+ }
277
+ // Map R2/presigned URL errors to user-friendly messages
278
+ const errorMessages = {
279
+ 403: "Upload expired (presigned URL timeout after 180s)",
280
+ 400: "Checksum mismatch (file may be corrupted)",
281
+ 413: "File too large for upload"
282
+ };
283
+ const errorMsg = errorMessages[response.status] ||
284
+ `${response.status} ${response.statusText}`;
285
+ throw new Error(`Upload failed for ${file.path}: ${errorMsg}${responseText ? ` - ${responseText}` : ""}`);
286
+ }
287
+ return uploadUrl.filePath;
288
+ }
289
+ catch (error) {
290
+ throw new Error(`Failed to upload ${file.path}: ${error instanceof Error ? error.message : String(error)}`);
291
+ }
292
+ });
293
+ // Wait for batch to complete (fail fast on any error)
294
+ const results = await Promise.allSettled(uploadPromises);
295
+ const failed = results.find((r) => r.status === "rejected");
296
+ if (failed && failed.status === "rejected") {
297
+ uploadSpinner.fail("Upload failed");
298
+ throw failed.reason;
299
+ }
300
+ uploadedCount += batch.length;
301
+ uploadSpinner.text = `Uploading (${uploadedCount}/${uploadUrls.length})`;
302
+ }
303
+ // Display final upload summary with copy stats (Phase 2)
304
+ const totalFiles = files.length;
305
+ const copied = createVersionResponse.copiedFiles?.length || 0;
306
+ if (copied > 0) {
307
+ uploadSpinner.succeed(`Uploaded ${totalFiles} files (${uploadedCount} uploaded, ${copied} copied server-side)`);
308
+ }
309
+ else {
310
+ uploadSpinner.succeed(`Uploaded ${uploadedCount} files`);
311
+ }
312
+ }
313
+ // Step 8: Complete the version
314
+ const completeSpinner = ora("Finalizing version...").start();
315
+ const completeResponse = await completeVersion(client, project.id, createVersionResponse.versionId, {
316
+ assets: createVersionResponse.uploadUrls.map((u) => {
317
+ const file = files.find((f) => f.path === u.filePath);
318
+ if (!file)
319
+ throw new Error(`File not found for path: ${u.filePath}`);
320
+ return {
321
+ filePath: file.path,
322
+ sizeBytes: file.size,
323
+ contentType: file.contentType,
324
+ r2Key: u.r2Key,
325
+ checksum: file.checksum
326
+ };
327
+ }),
328
+ // Include copied files list for server validation (Phase 2)
329
+ copiedAssets: createVersionResponse.copiedFiles
330
+ });
331
+ completeSpinner.succeed(`Version ${chalk.green(versionString)} upload complete!`);
332
+ // Step 9: Display results
333
+ if (options.json) {
334
+ console.log(JSON.stringify({
335
+ project: {
336
+ id: project.id,
337
+ name: project.name,
338
+ slug: project.slug
339
+ },
340
+ version: {
341
+ id: completeResponse.versionId,
342
+ version: versionString,
343
+ fileCount: completeResponse.fileCount,
344
+ totalSize: completeResponse.totalSize
345
+ },
346
+ previewUrl: completeResponse.previewUrl,
347
+ gitMetadata
348
+ }, null, 2));
349
+ }
350
+ else {
351
+ console.log(`\n${chalk.bold("Upload Successful!")}\n` +
352
+ ` Version: ${chalk.green(versionString)}\n` +
353
+ ` Files: ${chalk.green(completeResponse.fileCount.toString())}\n` +
354
+ ` Total Size: ${chalk.green(formatFileSize(completeResponse.totalSize))}\n` +
355
+ ` Preview URL: ${chalk.cyan(completeResponse.previewUrl)}\n`);
356
+ console.log(chalk.dim(`\nNext steps:\n` +
357
+ ` 1. Test your version at the preview URL\n` +
358
+ ` 2. Deploy to an environment: ${chalk.cyan(`relesio deploy ${project.slug} ${versionString} --env staging`)}\n`));
359
+ }
360
+ }
361
+ catch (error) {
362
+ spinner.fail("Upload failed");
363
+ handleError(error);
364
+ }
365
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const versionsCommand: Command;
@@ -0,0 +1,6 @@
1
+ import { Command } from "commander";
2
+ import { listVersionsCommand } from "./list.js";
3
+ export const versionsCommand = new Command("versions")
4
+ .alias("version")
5
+ .description("Manage versions")
6
+ .addCommand(listVersionsCommand);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const listVersionsCommand: Command;
@@ -0,0 +1,51 @@
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 { getProjectBySlug } from "../../lib/api/projects.js";
6
+ import { listVersions } from "../../lib/api/versions.js";
7
+ import { formatTable } from "../../lib/output/table.js";
8
+ import { handleError } from "../../lib/errors/handler.js";
9
+ export const listVersionsCommand = new Command("list")
10
+ .description("List versions for a project")
11
+ .argument("<project-slug>", "Project slug")
12
+ .option("--json", "Output as JSON")
13
+ .action(async (projectSlug, options) => {
14
+ const spinner = ora("Loading versions...").start();
15
+ try {
16
+ const config = ConfigManager.load();
17
+ if (!config.apiToken) {
18
+ spinner.fail("Not authenticated. Run 'relesio auth login' first.");
19
+ process.exit(1);
20
+ }
21
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
22
+ // Get project by slug
23
+ const project = await getProjectBySlug(client, projectSlug);
24
+ if (!project) {
25
+ spinner.fail(`Project '${projectSlug}' not found`);
26
+ process.exit(1);
27
+ }
28
+ const { versions, count } = await listVersions(client, project.id);
29
+ spinner.stop();
30
+ if (options.json) {
31
+ console.log(JSON.stringify({ versions, count }, null, 2));
32
+ return;
33
+ }
34
+ if (versions.length === 0) {
35
+ console.log(`\n✨ No versions yet for ${project.name}`);
36
+ return;
37
+ }
38
+ console.log(`\n📦 Versions for ${project.name} (${count}):\n`);
39
+ console.log(formatTable(["Version", "Status", "Git SHA", "Branch", "Uploaded"], versions.map((v) => [
40
+ v.semanticVersion || v.version || v.id,
41
+ v.status === "completed" ? "✓" : v.status,
42
+ v.gitSha?.substring(0, 7) || "-",
43
+ v.gitBranch || "-",
44
+ new Date(v.uploadDate).toLocaleString()
45
+ ])));
46
+ }
47
+ catch (error) {
48
+ spinner.fail("Failed to list versions");
49
+ handleError(error);
50
+ }
51
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const whoamiCommand: Command;
@@ -0,0 +1,52 @@
1
+ import { Command } from "commander";
2
+ import { ConfigManager } from "../lib/config/manager.js";
3
+ import { RelesioAPIClient } from "../lib/api/client.js";
4
+ import { OrganizationsAPI } from "../lib/api/organizations.js";
5
+ import { colors, formatWarning } from "../lib/output/colors.js";
6
+ import { outputJSON, shouldOutputJSON } from "../lib/output/formatter.js";
7
+ export const whoamiCommand = new Command("whoami")
8
+ .description("Display current authentication status")
9
+ .action(async () => {
10
+ const config = ConfigManager.load();
11
+ if (!config.apiToken) {
12
+ if (shouldOutputJSON()) {
13
+ outputJSON({ authenticated: false });
14
+ }
15
+ else {
16
+ console.log(formatWarning("Not authenticated"));
17
+ console.log(colors.dim('Run "relesio auth login" to authenticate'));
18
+ }
19
+ return;
20
+ }
21
+ if (shouldOutputJSON()) {
22
+ // For JSON output, just show the config
23
+ outputJSON({
24
+ authenticated: true,
25
+ apiBaseUrl: config.apiBaseUrl,
26
+ currentOrgId: config.currentOrgId,
27
+ currentOrgName: config.currentOrgName
28
+ });
29
+ return;
30
+ }
31
+ // For table output, fetch additional info from API
32
+ const client = new RelesioAPIClient(config.apiBaseUrl, config.apiToken);
33
+ const orgsAPI = new OrganizationsAPI(client);
34
+ try {
35
+ const response = await orgsAPI.getUserOrgs();
36
+ console.log(colors.bold("\n Authentication Status"));
37
+ console.log(colors.dim("─".repeat(50)));
38
+ console.log(`Token: ${colors.dim(config.apiToken.substring(0, 20))}...`);
39
+ console.log(`Current Org: ${colors.highlight(config.currentOrgName || "None")} ${config.currentOrgId ? colors.dim(`(${config.currentOrgId})`) : ""}`);
40
+ console.log(`Organizations: ${response.organizations.length}`);
41
+ console.log(`API Base URL: ${colors.dim(config.apiBaseUrl)}`);
42
+ }
43
+ catch (_error) {
44
+ // If API call fails, show what we have in config
45
+ console.log(colors.bold("\nAuthentication Status"));
46
+ console.log(colors.dim("─".repeat(50)));
47
+ console.log(`Token: ${colors.dim(config.apiToken.substring(0, 20))}...`);
48
+ console.log(`Current Org: ${colors.highlight(config.currentOrgName || "None")}`);
49
+ console.log(`API Base URL: ${colors.dim(config.apiBaseUrl)}`);
50
+ console.log(colors.dim("\n⚠ Could not fetch additional info from API"));
51
+ }
52
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};