simvyn 1.0.0 → 1.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.
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
- "name": "simvyn",
3
- "version": "1.0.0",
4
- "main": "index.js",
5
- "license": "MIT"
2
+ "name": "simvyn",
3
+ "version": "1.0.1",
4
+ "type": "module",
5
+ "description": "Universal mobile device devtool — control iOS Simulators and Android Emulators from a single dashboard and CLI",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "bin": {
9
+ "simvyn": "./src/index.ts"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./src/index.ts",
14
+ "default": "./src/index.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "src/",
19
+ "dist/",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22.12.0"
25
+ },
26
+ "keywords": [
27
+ "simulator",
28
+ "emulator",
29
+ "ios",
30
+ "android",
31
+ "devtools",
32
+ "simctl",
33
+ "adb",
34
+ "mobile"
35
+ ],
36
+ "license": "MIT",
37
+ "author": "Pranshu Chittora",
38
+ "homepage": "https://github.com/pranshuchittora/simvyn#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/pranshuchittora/simvyn/issues"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/pranshuchittora/simvyn.git",
45
+ "directory": "packages/cli"
46
+ },
47
+ "dependencies": {
48
+ "@simvyn/core": "*",
49
+ "@simvyn/server": "*",
50
+ "@simvyn/types": "*",
51
+ "commander": "^14",
52
+ "open": "^10"
53
+ }
6
54
  }
@@ -0,0 +1,274 @@
1
+ import { createAvailableAdapters, createDeviceManager, createIosAdapter } from "@simvyn/core";
2
+ import type { Device, Platform, PlatformAdapter } from "@simvyn/types";
3
+ import type { Command } from "commander";
4
+
5
+ function padRight(str: string, len: number): string {
6
+ return str.length >= len ? str : str + " ".repeat(len - str.length);
7
+ }
8
+
9
+ function printTable(devices: Device[]): void {
10
+ if (devices.length === 0) {
11
+ console.log("No devices found.");
12
+ return;
13
+ }
14
+
15
+ const headers = ["ID", "NAME", "PLATFORM", "STATE", "OS VERSION"];
16
+ const rows = devices.map((d) => [d.id, d.name, d.platform, d.state, d.osVersion]);
17
+
18
+ // calculate column widths
19
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
20
+
21
+ const headerLine = headers.map((h, i) => padRight(h, widths[i])).join(" ");
22
+ console.log(headerLine);
23
+ for (const row of rows) {
24
+ console.log(row.map((val, i) => padRight(val, widths[i])).join(" "));
25
+ }
26
+ }
27
+
28
+ async function getAllDevices(
29
+ platform?: string,
30
+ ): Promise<{ devices: Device[]; adapters: PlatformAdapter[] }> {
31
+ const adapters = await createAvailableAdapters();
32
+ const dm = createDeviceManager(adapters);
33
+ const devices = await dm.refresh();
34
+
35
+ const filtered = platform ? devices.filter((d) => d.platform === platform) : devices;
36
+
37
+ return { devices: filtered, adapters };
38
+ }
39
+
40
+ function findAdapter(adapters: PlatformAdapter[], platform: Platform): PlatformAdapter | undefined {
41
+ return adapters.find((a) => a.platform === platform);
42
+ }
43
+
44
+ async function findDevice(id: string): Promise<{ device: Device; adapter: PlatformAdapter }> {
45
+ const { devices, adapters } = await getAllDevices();
46
+ const device = devices.find((d) => d.id === id);
47
+ if (!device) {
48
+ console.error(`Device not found: ${id}`);
49
+ process.exit(1);
50
+ }
51
+
52
+ const adapter = findAdapter(adapters, device.platform);
53
+ if (!adapter) {
54
+ console.error(`No ${device.platform} support on this platform`);
55
+ process.exit(1);
56
+ }
57
+
58
+ return { device, adapter };
59
+ }
60
+
61
+ export function registerDeviceCommand(program: Command): void {
62
+ const device = program.command("device").description("Device management commands");
63
+
64
+ // simvyn device list
65
+ device
66
+ .command("list")
67
+ .description("List available devices")
68
+ .option("--platform <platform>", "Filter by platform (ios|android)")
69
+ .option("--json", "Output as JSON")
70
+ .action(async (opts) => {
71
+ try {
72
+ const { devices } = await getAllDevices(opts.platform);
73
+ if (opts.json) {
74
+ console.log(JSON.stringify(devices, null, 2));
75
+ } else {
76
+ printTable(devices);
77
+ }
78
+ } catch (err) {
79
+ console.error("Failed to list devices:", (err as Error).message);
80
+ process.exit(1);
81
+ }
82
+ });
83
+
84
+ // simvyn device boot <id>
85
+ device
86
+ .command("boot <id>")
87
+ .description("Boot a device")
88
+ .action(async (id) => {
89
+ try {
90
+ const { device: dev, adapter } = await findDevice(id);
91
+ console.log(`Booting ${dev.name}...`);
92
+ await adapter.boot(id);
93
+
94
+ // Poll until booted (max 60s)
95
+ const deadline = Date.now() + 60_000;
96
+ while (Date.now() < deadline) {
97
+ const devices = await adapter.listDevices();
98
+ const updated = devices.find((d) => d.id === id || d.name === dev.name);
99
+ if (updated && updated.state === "booted") {
100
+ console.log(`\u2713 ${dev.name} booted`);
101
+ return;
102
+ }
103
+ await new Promise((r) => setTimeout(r, 2000));
104
+ }
105
+ console.error(`\u2717 Timed out waiting for ${dev.name} to boot`);
106
+ process.exit(1);
107
+ } catch (err) {
108
+ console.error(`Failed to boot device: ${(err as Error).message}`);
109
+ process.exit(1);
110
+ }
111
+ });
112
+
113
+ // simvyn device shutdown <id>
114
+ device
115
+ .command("shutdown <id>")
116
+ .description("Shutdown a device")
117
+ .action(async (id) => {
118
+ try {
119
+ const { device: dev, adapter } = await findDevice(id);
120
+ await adapter.shutdown(id);
121
+ console.log(`\u2713 ${dev.name} shut down`);
122
+ } catch (err) {
123
+ console.error(`Failed to shutdown device: ${(err as Error).message}`);
124
+ process.exit(1);
125
+ }
126
+ });
127
+
128
+ // simvyn device erase <id>
129
+ device
130
+ .command("erase <id>")
131
+ .description("Erase a device (iOS only, must be shutdown)")
132
+ .action(async (id) => {
133
+ try {
134
+ const { device: dev, adapter } = await findDevice(id);
135
+
136
+ if (dev.platform !== "ios") {
137
+ console.error("Erase is only supported on iOS simulators");
138
+ process.exit(1);
139
+ }
140
+
141
+ if (dev.state === "booted") {
142
+ console.error(`${dev.name} is booted — shut it down first: simvyn device shutdown ${id}`);
143
+ process.exit(1);
144
+ }
145
+
146
+ if (!adapter.erase) {
147
+ console.error("Erase not available for this adapter");
148
+ process.exit(1);
149
+ }
150
+
151
+ await adapter.erase(id);
152
+ console.log(`\u2713 ${dev.name} erased`);
153
+ } catch (err) {
154
+ console.error(`Failed to erase device: ${(err as Error).message}`);
155
+ process.exit(1);
156
+ }
157
+ });
158
+
159
+ // simvyn device create <name> <deviceTypeId> [runtimeId]
160
+ device
161
+ .command("create <name> <deviceTypeId> [runtimeId]")
162
+ .description("Create a new iOS simulator")
163
+ .option("--list-types", "List available device types")
164
+ .option("--list-runtimes", "List available runtimes")
165
+ .action(
166
+ async (
167
+ name: string,
168
+ deviceTypeId: string,
169
+ runtimeId: string | undefined,
170
+ opts: { listTypes?: boolean; listRuntimes?: boolean },
171
+ ) => {
172
+ try {
173
+ const adapter = createIosAdapter();
174
+ if (opts.listTypes) {
175
+ const types = await adapter.listDeviceTypes!();
176
+ console.table(types.map((t) => ({ Identifier: t.identifier, Name: t.name })));
177
+ return;
178
+ }
179
+ if (opts.listRuntimes) {
180
+ const runtimes = await adapter.listRuntimes!();
181
+ console.table(
182
+ runtimes.map((r) => ({
183
+ Identifier: r.identifier,
184
+ Name: r.name,
185
+ Version: r.version,
186
+ Available: r.isAvailable,
187
+ })),
188
+ );
189
+ return;
190
+ }
191
+ const newId = await adapter.createDevice!(name, deviceTypeId, runtimeId);
192
+ console.log(`\u2713 Created: ${name} (${newId})`);
193
+ } catch (err) {
194
+ console.error(`Failed to create device: ${(err as Error).message}`);
195
+ process.exit(1);
196
+ }
197
+ },
198
+ );
199
+
200
+ // simvyn device clone <id> <newName>
201
+ device
202
+ .command("clone <id> <newName>")
203
+ .description("Clone an iOS simulator")
204
+ .action(async (id: string, newName: string) => {
205
+ try {
206
+ const { device: dev, adapter } = await findDevice(id);
207
+ if (dev.platform !== "ios") {
208
+ console.error("Clone is only supported for iOS simulators");
209
+ process.exit(1);
210
+ }
211
+ if (!adapter.cloneDevice) {
212
+ console.error("Clone not available for this adapter");
213
+ process.exit(1);
214
+ }
215
+ const newId = await adapter.cloneDevice(dev.id, newName);
216
+ console.log(`\u2713 Cloned: ${dev.name} → ${newName} (${newId})`);
217
+ } catch (err) {
218
+ console.error(`Failed to clone device: ${(err as Error).message}`);
219
+ process.exit(1);
220
+ }
221
+ });
222
+
223
+ // simvyn device rename <id> <newName>
224
+ device
225
+ .command("rename <id> <newName>")
226
+ .description("Rename an iOS simulator")
227
+ .action(async (id: string, newName: string) => {
228
+ try {
229
+ const { device: dev, adapter } = await findDevice(id);
230
+ if (dev.platform !== "ios") {
231
+ console.error("Rename is only supported for iOS simulators");
232
+ process.exit(1);
233
+ }
234
+ if (!adapter.renameDevice) {
235
+ console.error("Rename not available for this adapter");
236
+ process.exit(1);
237
+ }
238
+ await adapter.renameDevice(dev.id, newName);
239
+ console.log(`\u2713 Renamed: ${dev.name} → ${newName} (${dev.id})`);
240
+ } catch (err) {
241
+ console.error(`Failed to rename device: ${(err as Error).message}`);
242
+ process.exit(1);
243
+ }
244
+ });
245
+
246
+ // simvyn device delete <id>
247
+ device
248
+ .command("delete <id>")
249
+ .description("Delete an iOS simulator (must be shutdown)")
250
+ .action(async (id: string) => {
251
+ try {
252
+ const { device: dev, adapter } = await findDevice(id);
253
+ if (dev.platform !== "ios") {
254
+ console.error("Delete is only supported for iOS simulators");
255
+ process.exit(1);
256
+ }
257
+ if (dev.state !== "shutdown") {
258
+ console.error(
259
+ `${dev.name} must be shut down before deleting: simvyn device shutdown ${id}`,
260
+ );
261
+ process.exit(1);
262
+ }
263
+ if (!adapter.deleteDevice) {
264
+ console.error("Delete not available for this adapter");
265
+ process.exit(1);
266
+ }
267
+ await adapter.deleteDevice(dev.id);
268
+ console.log(`\u2713 Deleted: ${dev.name} (${dev.id})`);
269
+ } catch (err) {
270
+ console.error(`Failed to delete device: ${(err as Error).message}`);
271
+ process.exit(1);
272
+ }
273
+ });
274
+ }
@@ -0,0 +1,29 @@
1
+ import { setVerbose } from "@simvyn/core";
2
+ import { startServer } from "@simvyn/server";
3
+ import type { Command } from "commander";
4
+
5
+ async function runStart(opts: {
6
+ port: string;
7
+ host: string;
8
+ open: boolean;
9
+ verbose?: boolean;
10
+ }): Promise<void> {
11
+ if (opts.verbose) setVerbose(true);
12
+
13
+ await startServer({
14
+ port: parseInt(opts.port, 10),
15
+ host: opts.host,
16
+ open: opts.open,
17
+ });
18
+ }
19
+
20
+ export function registerStartCommand(program: Command): void {
21
+ program
22
+ .command("start", { isDefault: true })
23
+ .description("Start the simvyn server and open the dashboard")
24
+ .option("-p, --port <number>", "Port to listen on", "3847")
25
+ .option("-H, --host <string>", "Host to bind to", "127.0.0.1")
26
+ .option("--no-open", "Don't open browser automatically")
27
+ .option("-v, --verbose", "Log every adb/simctl command before execution")
28
+ .action(runStart);
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from "node:module";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { setVerbose } from "@simvyn/core";
7
+ import { Command } from "commander";
8
+ import { registerDeviceCommand } from "./commands/device.js";
9
+ import { registerStartCommand } from "./commands/start.js";
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const require = createRequire(import.meta.url);
13
+ const pkg = require("../package.json");
14
+
15
+ const program = new Command();
16
+ program.name("simvyn").version(pkg.version).description("Universal mobile device devtool");
17
+ program.option("-v, --verbose", "Log every adb/simctl command before execution");
18
+
19
+ program.hook("preAction", (thisCommand) => {
20
+ const opts = thisCommand.optsWithGlobals();
21
+ if (opts.verbose) setVerbose(true);
22
+ });
23
+
24
+ registerStartCommand(program);
25
+ registerDeviceCommand(program);
26
+
27
+ // Discover module CLI registrars from modules directory
28
+ const modulesDir = resolve(__dirname, "..", "..", "modules");
29
+ try {
30
+ const { getModuleCLIRegistrars } = await import("@simvyn/server");
31
+ const registrars = await getModuleCLIRegistrars(modulesDir);
32
+ for (const { name, register } of registrars) {
33
+ try {
34
+ register(program);
35
+ } catch {
36
+ // Skip modules whose CLI conflicts with built-in commands (e.g. device)
37
+ }
38
+ }
39
+ } catch {
40
+ // Module discovery failed — CLI still works for built-in commands
41
+ }
42
+
43
+ program.parse();