@involvex/syncstuff-cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,159 @@
1
+ import chalk from "chalk";
2
+ import { createBox, printHeader, printSeparator } from "../../utils/ui.js";
3
+
4
+ const commandHelp: Record<string, string> = {
5
+ login: `${chalk.cyan.bold("login")}
6
+
7
+ Log in to your Syncstuff account.
8
+
9
+ ${chalk.bold("Usage:")}
10
+ syncstuff login
11
+
12
+ ${chalk.bold("Description:")}
13
+ Authenticates with the Syncstuff API using your email and password.
14
+ Your credentials are stored securely for future sessions.`,
15
+
16
+ logout: `${chalk.cyan.bold("logout")}
17
+
18
+ Log out from your Syncstuff account.
19
+
20
+ ${chalk.bold("Usage:")}
21
+ syncstuff logout
22
+
23
+ ${chalk.bold("Description:")}
24
+ Clears your stored authentication credentials.`,
25
+
26
+ whoami: `${chalk.cyan.bold("whoami")}
27
+
28
+ Display your user profile.
29
+
30
+ ${chalk.bold("Usage:")}
31
+ syncstuff whoami
32
+
33
+ ${chalk.bold("Description:")}
34
+ Shows information about the currently logged-in user.`,
35
+
36
+ devices: `${chalk.cyan.bold("devices")}
37
+
38
+ List all your connected devices.
39
+
40
+ ${chalk.bold("Usage:")}
41
+ syncstuff devices
42
+
43
+ ${chalk.bold("Description:")}
44
+ Displays a table of all devices registered to your account,
45
+ including their status, platform, and last seen time.`,
46
+
47
+ device: `${chalk.cyan.bold("device")}
48
+
49
+ Connect to a specific device or list available devices.
50
+
51
+ ${chalk.bold("Usage:")}
52
+ syncstuff device [options] [deviceId]
53
+
54
+ ${chalk.bold("Options:")}
55
+ --list, -l List all available devices
56
+
57
+ ${chalk.bold("Examples:")}
58
+ syncstuff device --list List all devices
59
+ syncstuff device Interactive device selection
60
+ syncstuff device abc123 Connect to device with ID abc123`,
61
+
62
+ transfer: `${chalk.cyan.bold("transfer")}
63
+
64
+ Transfer a file to a connected device.
65
+
66
+ ${chalk.bold("Usage:")}
67
+ syncstuff transfer <file>
68
+
69
+ ${chalk.bold("Description:")}
70
+ Sends the specified file to a connected device.
71
+ You must be connected to a device first.`,
72
+
73
+ version: `${chalk.cyan.bold("version")}
74
+
75
+ Show CLI version.
76
+
77
+ ${chalk.bold("Usage:")}
78
+ syncstuff version
79
+ syncstuff --version
80
+ syncstuff -v`,
81
+ };
82
+
83
+ export async function showHelp(command?: string): Promise<void> {
84
+ printHeader();
85
+
86
+ // Show command-specific help if requested
87
+ if (command && commandHelp[command]) {
88
+ const helpContent = createBox(commandHelp[command], {
89
+ title: `Help: ${command}`,
90
+ titleAlignment: "center",
91
+ padding: 1,
92
+ });
93
+
94
+ console.log(helpContent);
95
+ printSeparator();
96
+ return;
97
+ }
98
+
99
+ // Show general help
100
+ const helpContent = createBox(
101
+ chalk.cyan.bold("Available Commands\n\n") +
102
+ chalk.bold("Authentication:\n") +
103
+ " " +
104
+ chalk.green("login") +
105
+ " Login to your Syncstuff account\n" +
106
+ " " +
107
+ chalk.green("logout") +
108
+ " Logout from your account\n" +
109
+ " " +
110
+ chalk.green("whoami") +
111
+ " Display your user profile\n\n" +
112
+ chalk.bold("Device Management:\n") +
113
+ " " +
114
+ chalk.green("devices") +
115
+ " List all your connected devices\n" +
116
+ " " +
117
+ chalk.green("device --list") +
118
+ " List available devices\n" +
119
+ " " +
120
+ chalk.green("device <id>") +
121
+ " Connect to a specific device\n" +
122
+ " " +
123
+ chalk.green("transfer <file>") +
124
+ " Transfer a file to a device\n\n" +
125
+ chalk.bold("General:\n") +
126
+ " " +
127
+ chalk.green("help [command]") +
128
+ " Show help (or command-specific help)\n" +
129
+ " " +
130
+ chalk.green("version") +
131
+ " Show CLI version\n\n" +
132
+ chalk.bold("Global Flags:\n") +
133
+ " " +
134
+ chalk.yellow("-h, --help") +
135
+ " Show help for any command\n" +
136
+ " " +
137
+ chalk.yellow("-d, --debug") +
138
+ " Enable debug mode\n\n" +
139
+ chalk.gray("Examples:\n") +
140
+ " syncstuff devices -d " +
141
+ chalk.gray("List devices with debug output\n") +
142
+ " syncstuff device -h " +
143
+ chalk.gray("Show help for device command"),
144
+ {
145
+ title: "Syncstuff CLI Help",
146
+ titleAlignment: "center",
147
+ padding: 1,
148
+ },
149
+ );
150
+
151
+ console.log(helpContent);
152
+ printSeparator();
153
+ console.log(
154
+ chalk.gray(
155
+ "For more information, visit: https://syncstuff-web.involvex.workers.dev/",
156
+ ),
157
+ );
158
+ printSeparator();
159
+ }
File without changes
@@ -0,0 +1,69 @@
1
+ import inquirer from "inquirer";
2
+ import { apiClient } from "../../utils/api-client.js";
3
+ import { debugLog, type CommandContext } from "../../utils/context.js";
4
+ import {
5
+ createSpinner,
6
+ error,
7
+ info,
8
+ printHeader,
9
+ printSeparator,
10
+ success,
11
+ } from "../../utils/ui.js";
12
+
13
+ export async function login(ctx: CommandContext): Promise<void> {
14
+ printHeader();
15
+ debugLog(ctx, "Starting login flow");
16
+
17
+ try {
18
+ const answers = await inquirer.prompt([
19
+ {
20
+ type: "input",
21
+ name: "email",
22
+ message: "Email address:",
23
+ validate: (input: string) => {
24
+ if (!input || !input.includes("@")) {
25
+ return "Please enter a valid email address";
26
+ }
27
+ return true;
28
+ },
29
+ },
30
+ {
31
+ type: "password",
32
+ name: "password",
33
+ message: "Password:",
34
+ mask: "*",
35
+ validate: (input: string) => {
36
+ if (!input || input.length < 1) {
37
+ return "Password cannot be empty";
38
+ }
39
+ return true;
40
+ },
41
+ },
42
+ ]);
43
+
44
+ const spinner = createSpinner("Logging in...");
45
+ spinner.start();
46
+
47
+ const response = await apiClient.login(answers.email, answers.password);
48
+
49
+ if (response.success && response.data) {
50
+ spinner.succeed("Login successful!");
51
+ printSeparator();
52
+ success(
53
+ `Welcome, ${response.data.user.username || response.data.user.email}!`,
54
+ );
55
+ info(`Role: ${response.data.user.role}`);
56
+ if (response.data.user.full_name) {
57
+ info(`Name: ${response.data.user.full_name}`);
58
+ }
59
+ printSeparator();
60
+ } else {
61
+ spinner.fail("Login failed");
62
+ error(response.error || "Invalid credentials");
63
+ process.exit(1);
64
+ }
65
+ } catch (err) {
66
+ error(`Login error: ${err instanceof Error ? err.message : String(err)}`);
67
+ process.exit(1);
68
+ }
69
+ }
@@ -0,0 +1,23 @@
1
+ import { apiClient } from "../../utils/api-client.js";
2
+ import { debugLog, type CommandContext } from "../../utils/context.js";
3
+ import { error, printHeader, printSeparator, success } from "../../utils/ui.js";
4
+
5
+ export async function logout(ctx: CommandContext): Promise<void> {
6
+ printHeader();
7
+ debugLog(ctx, "Starting logout flow");
8
+
9
+ if (!apiClient.isAuthenticated()) {
10
+ error("You are not logged in");
11
+ process.exit(1);
12
+ }
13
+
14
+ try {
15
+ await apiClient.logout();
16
+ printSeparator();
17
+ success("Logged out successfully");
18
+ printSeparator();
19
+ } catch (err) {
20
+ error(`Logout error: ${err instanceof Error ? err.message : String(err)}`);
21
+ process.exit(1);
22
+ }
23
+ }
@@ -0,0 +1,148 @@
1
+ import chalk from "chalk";
2
+ import { existsSync, statSync } from "fs";
3
+ import inquirer from "inquirer";
4
+ import { resolve } from "path";
5
+ import { apiClient } from "../../utils/api-client.js";
6
+ import { debugLog, type CommandContext } from "../../utils/context.js";
7
+ import {
8
+ createSpinner,
9
+ error,
10
+ info,
11
+ printHeader,
12
+ printSeparator,
13
+ success,
14
+ } from "../../utils/ui.js";
15
+
16
+ export async function transferFile(
17
+ filePath: string | undefined,
18
+ ctx: CommandContext,
19
+ ): Promise<void> {
20
+ printHeader();
21
+ debugLog(ctx, "Starting file transfer", { filePath });
22
+
23
+ if (!apiClient.isAuthenticated()) {
24
+ error("You are not logged in. Please run 'syncstuff login' first.");
25
+ process.exit(1);
26
+ }
27
+
28
+ let targetFile = filePath;
29
+
30
+ // If no file path provided, prompt for it
31
+ if (!targetFile) {
32
+ const answers = await inquirer.prompt([
33
+ {
34
+ type: "input",
35
+ name: "filePath",
36
+ message: "File path to transfer:",
37
+ validate: (input: string) => {
38
+ if (!input) {
39
+ return "Please enter a file path";
40
+ }
41
+ const resolved = resolve(input);
42
+ if (!existsSync(resolved)) {
43
+ return "File does not exist";
44
+ }
45
+ if (!statSync(resolved).isFile()) {
46
+ return "Path is not a file";
47
+ }
48
+ return true;
49
+ },
50
+ },
51
+ ]);
52
+ targetFile = answers.filePath;
53
+ }
54
+
55
+ if (!targetFile) {
56
+ error("No file path provided");
57
+ process.exit(1);
58
+ }
59
+
60
+ const resolvedPath = resolve(targetFile);
61
+
62
+ if (!existsSync(resolvedPath)) {
63
+ error(`File not found: ${resolvedPath}`);
64
+ process.exit(1);
65
+ }
66
+
67
+ if (!statSync(resolvedPath).isFile()) {
68
+ error(`Path is not a file: ${resolvedPath}`);
69
+ process.exit(1);
70
+ }
71
+
72
+ // Get file info
73
+ const stats = statSync(resolvedPath);
74
+ const fileSize = (stats.size / 1024 / 1024).toFixed(2);
75
+
76
+ info(`File: ${resolvedPath}`);
77
+ info(`Size: ${fileSize} MB`);
78
+
79
+ // Get device list
80
+ const devicesSpinner = createSpinner("Fetching devices...");
81
+ devicesSpinner.start();
82
+
83
+ const devicesResponse = await apiClient.getDevices();
84
+ devicesSpinner.stop();
85
+
86
+ if (
87
+ !devicesResponse.success ||
88
+ !devicesResponse.data ||
89
+ devicesResponse.data.length === 0
90
+ ) {
91
+ error(
92
+ "No devices available. Please ensure at least one device is connected.",
93
+ );
94
+ process.exit(1);
95
+ }
96
+
97
+ // Prompt for device selection
98
+ const deviceChoices = devicesResponse.data.map(device => ({
99
+ name: `${device.name} (${device.platform}) - ${device.is_online ? "Online" : "Offline"}`,
100
+ value: device.id,
101
+ disabled: !device.is_online ? "Offline" : false,
102
+ }));
103
+
104
+ const deviceAnswer = await inquirer.prompt([
105
+ {
106
+ type: "list",
107
+ name: "deviceId",
108
+ message: "Select target device:",
109
+ choices: deviceChoices,
110
+ },
111
+ ]);
112
+
113
+ // Transfer file
114
+ const transferSpinner = createSpinner(
115
+ `Transferring ${chalk.cyan(resolvedPath)} to device...`,
116
+ );
117
+ transferSpinner.start();
118
+
119
+ try {
120
+ const transferResponse = await apiClient.transferFile(
121
+ deviceAnswer.deviceId,
122
+ resolvedPath,
123
+ );
124
+
125
+ if (transferResponse.success) {
126
+ transferSpinner.succeed("File transfer initiated!");
127
+ printSeparator();
128
+ success(`Transfer ID: ${transferResponse.data?.transferId || "N/A"}`);
129
+ info("File is being synced to the target device");
130
+ printSeparator();
131
+ } else {
132
+ transferSpinner.fail("Transfer failed");
133
+ if (
134
+ transferResponse.error?.includes("404") ||
135
+ transferResponse.error?.includes("Not found")
136
+ ) {
137
+ info("File transfer endpoint not yet implemented in API");
138
+ info("This feature will be available soon!");
139
+ } else {
140
+ error(transferResponse.error || "Unknown error");
141
+ }
142
+ }
143
+ } catch (err) {
144
+ transferSpinner.fail("Transfer error");
145
+ error(`Error: ${err instanceof Error ? err.message : String(err)}`);
146
+ process.exit(1);
147
+ }
148
+ }
@@ -0,0 +1,35 @@
1
+ import chalk from "chalk";
2
+ import { readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { createBox, printHeader, printSeparator } from "../../utils/ui.js";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ export function showversion() {
11
+ printHeader();
12
+
13
+ try {
14
+ const packagePath = join(__dirname, "../../../package.json");
15
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
16
+ const version = packageJson.version || "0.0.1";
17
+
18
+ const versionBox = createBox(
19
+ chalk.cyan.bold("Syncstuff CLI\n\n") +
20
+ chalk.bold("Version:") +
21
+ ` ${chalk.green(version)}\n` +
22
+ chalk.bold("Package:") +
23
+ ` @involvex/syncstuff-cli`,
24
+ {
25
+ title: "Version Information",
26
+ titleAlignment: "center",
27
+ },
28
+ );
29
+
30
+ console.log(versionBox);
31
+ printSeparator();
32
+ } catch {
33
+ console.log(chalk.yellow("Version: 0.0.1 (unable to read package.json)"));
34
+ }
35
+ }
@@ -0,0 +1,63 @@
1
+ import chalk from "chalk";
2
+ import { apiClient } from "../../utils/api-client.js";
3
+ import { debugLog, type CommandContext } from "../../utils/context.js";
4
+ import {
5
+ createBox,
6
+ createSpinner,
7
+ error,
8
+ printHeader,
9
+ printSeparator,
10
+ } from "../../utils/ui.js";
11
+
12
+ export async function whoami(ctx: CommandContext): Promise<void> {
13
+ printHeader();
14
+ debugLog(ctx, "Fetching user profile");
15
+
16
+ if (!apiClient.isAuthenticated()) {
17
+ error("You are not logged in. Please run 'syncstuff login' first.");
18
+ process.exit(1);
19
+ }
20
+
21
+ const spinner = createSpinner("Fetching user profile...");
22
+ spinner.start();
23
+
24
+ try {
25
+ const response = await apiClient.getProfile();
26
+
27
+ if (response.success && response.data) {
28
+ spinner.succeed("Profile loaded");
29
+ printSeparator();
30
+
31
+ const user = response.data;
32
+
33
+ const profileBox = createBox(
34
+ chalk.cyan.bold("User Profile\n\n") +
35
+ `${chalk.bold("ID:")} ${user.id}\n` +
36
+ `${chalk.bold("Email:")} ${user.email}\n` +
37
+ `${chalk.bold("Username:")} ${user.username}\n` +
38
+ (user.full_name
39
+ ? `${chalk.bold("Full Name:")} ${user.full_name}\n`
40
+ : "") +
41
+ `${chalk.bold("Role:")} ${chalk.yellow(user.role)}\n` +
42
+ `${chalk.bold("Status:")} ${chalk.green(user.status)}\n` +
43
+ `${chalk.bold("Created:")} ${new Date(user.created_at).toLocaleString()}\n` +
44
+ `${chalk.bold("Updated:")} ${new Date(user.updated_at).toLocaleString()}`,
45
+ {
46
+ title: "Syncstuff Account",
47
+ titleAlignment: "center",
48
+ },
49
+ );
50
+
51
+ console.log(profileBox);
52
+ printSeparator();
53
+ } else {
54
+ spinner.fail("Failed to fetch profile");
55
+ error(response.error || "Unknown error");
56
+ process.exit(1);
57
+ }
58
+ } catch (err) {
59
+ spinner.fail("Error fetching profile");
60
+ error(`Error: ${err instanceof Error ? err.message : String(err)}`);
61
+ process.exit(1);
62
+ }
63
+ }
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import { debugLog, parseArgs, type CommandContext } from "../utils/context.js";
3
+ import { printHeader } from "../utils/ui.js";
4
+
5
+ export async function run() {
6
+ const args = process.argv.slice(2);
7
+ const { command, flags, commandArgs } = parseArgs(args);
8
+
9
+ const ctx: CommandContext = { debug: flags.debug };
10
+
11
+ // Debug mode: log parsed arguments
12
+ debugLog(ctx, "Parsed arguments:", { command, flags, commandArgs });
13
+
14
+ // Handle help flag - show command-specific or general help
15
+ if (flags.help) {
16
+ const { showHelp } = await import("./commands/help.js");
17
+ await showHelp(command);
18
+ return;
19
+ }
20
+
21
+ // Handle no command
22
+ if (!command) {
23
+ const { showHelp } = await import("./commands/help.js");
24
+ await showHelp();
25
+ return;
26
+ }
27
+
28
+ switch (command) {
29
+ case "login":
30
+ {
31
+ const { login } = await import("./commands/login.js");
32
+ await login(ctx);
33
+ }
34
+ break;
35
+ case "whoami":
36
+ {
37
+ const { whoami } = await import("./commands/whoami.js");
38
+ await whoami(ctx);
39
+ }
40
+ break;
41
+ case "logout":
42
+ {
43
+ const { logout } = await import("./commands/logout.js");
44
+ await logout(ctx);
45
+ }
46
+ break;
47
+ case "devices":
48
+ {
49
+ const { listDevices } = await import("./commands/devices.js");
50
+ await listDevices(ctx);
51
+ }
52
+ break;
53
+ case "device":
54
+ {
55
+ const { device } = await import("./commands/device.js");
56
+ await device(commandArgs, ctx);
57
+ }
58
+ break;
59
+ case "transfer":
60
+ {
61
+ const { transferFile } = await import("./commands/transfer.js");
62
+ await transferFile(commandArgs[0], ctx);
63
+ }
64
+ break;
65
+ case "help":
66
+ {
67
+ const { showHelp } = await import("./commands/help.js");
68
+ await showHelp(commandArgs[0]);
69
+ }
70
+ break;
71
+ case "version":
72
+ case "--version":
73
+ case "-v": {
74
+ const { showversion } = await import("./commands/version.js");
75
+ showversion();
76
+ return;
77
+ }
78
+ default:
79
+ printHeader();
80
+ console.log(`❌ Unknown command: ${command}`);
81
+ console.log("Run 'syncstuff help' to see available commands");
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ run();
package/src/core.ts ADDED
@@ -0,0 +1,13 @@
1
+ // Core types moved to utils/api-client.ts
2
+ export type { UserProfile as User } from "./utils/api-client.js";
3
+
4
+ export interface Device {
5
+ id: string;
6
+ name: string;
7
+ platform: string;
8
+ is_online: boolean;
9
+ }
10
+
11
+ export type DebugMode = {
12
+ enabled: boolean;
13
+ };