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 +52 -4
- package/src/commands/device.ts +274 -0
- package/src/commands/start.ts +29 -0
- package/src/index.ts +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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();
|