infinicode 2.8.7 → 2.8.9
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/dist/robopark/auto-start.d.ts +1 -1
- package/dist/robopark/enroll.d.ts +7 -0
- package/dist/robopark/enroll.js +101 -0
- package/dist/robopark/preview-agent-launcher.d.ts +14 -0
- package/dist/robopark/preview-agent-launcher.js +102 -0
- package/dist/robopark/probe.d.ts +7 -0
- package/dist/robopark/probe.js +138 -0
- package/dist/robopark/serve.js +23 -3
- package/dist/robopark/setup-livekit.d.ts +8 -0
- package/dist/robopark/setup-livekit.js +233 -0
- package/dist/robopark/setup.d.ts +3 -0
- package/dist/robopark/setup.js +32 -0
- package/dist/robopark/verify.d.ts +6 -0
- package/dist/robopark/verify.js +192 -0
- package/dist/robopark-cli.js +68 -1
- package/package.json +1 -1
- package/packages/robopark/scheduler/__pycache__/main.cpython-312.pyc +0 -0
- package/packages/robopark/scheduler/__pycache__/preview_agent.cpython-312.pyc +0 -0
- package/packages/robopark/scheduler/main.py +276 -25
- package/packages/robopark/scheduler/preview_agent.py +639 -0
- package/packages/robopark/scheduler/requirements.txt +4 -0
package/dist/robopark/setup.d.ts
CHANGED
package/dist/robopark/setup.js
CHANGED
|
@@ -160,13 +160,37 @@ async function setupRobot(config, opts, ctx, internal) {
|
|
|
160
160
|
'--auto-update',
|
|
161
161
|
'--supervised',
|
|
162
162
|
];
|
|
163
|
+
const schedulerUrl = `http://${ctx.hub?.ip ?? '127.0.0.1'}:${opts.schedulerPort ? parseInt(opts.schedulerPort, 10) : DEFAULT_SCHEDULER_PORT}`;
|
|
164
|
+
const previewAgentArgs = [
|
|
165
|
+
'preview-agent',
|
|
166
|
+
'--scheduler-url', schedulerUrl,
|
|
167
|
+
'--robot-id', internal.name,
|
|
168
|
+
'--video-device', opts.videoDevice ?? 'auto',
|
|
169
|
+
'--audio-device', opts.audioDevice ?? 'default',
|
|
170
|
+
'--save-config',
|
|
171
|
+
];
|
|
172
|
+
if (opts.enrollmentToken)
|
|
173
|
+
previewAgentArgs.push('--enrollment-token', opts.enrollmentToken);
|
|
163
174
|
console.log(chalk.dim(' robot command:'));
|
|
164
175
|
console.log(' ' + chalk.cyan(`infinicode ${args.join(' ')}`));
|
|
176
|
+
console.log(' ' + chalk.cyan(`robopark ${previewAgentArgs.join(' ')}`));
|
|
165
177
|
console.log();
|
|
166
178
|
if (opts.start) {
|
|
167
179
|
const robotArgv = await infinicodeArgv(args);
|
|
168
180
|
console.log(chalk.dim(' starting robot satellite node…'));
|
|
169
181
|
runDetached(robotArgv[0], robotArgv.slice(1));
|
|
182
|
+
if (opts.enrollmentToken) {
|
|
183
|
+
console.log(chalk.dim(' enrolling robot with scheduler…'));
|
|
184
|
+
const enrollArgv = [process.execPath, process.argv[1], 'enroll',
|
|
185
|
+
'--scheduler-url', schedulerUrl,
|
|
186
|
+
'--robot-id', internal.name,
|
|
187
|
+
'--enrollment-token', opts.enrollmentToken,
|
|
188
|
+
'--save-config'];
|
|
189
|
+
runDetached(enrollArgv[0], enrollArgv.slice(1));
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.dim(' starting preview agent…'));
|
|
192
|
+
const previewArgv = [process.execPath, process.argv[1], ...previewAgentArgs];
|
|
193
|
+
runDetached(previewArgv[0], previewArgv.slice(1));
|
|
170
194
|
}
|
|
171
195
|
if (opts.autoStart) {
|
|
172
196
|
const robotArgv = await infinicodeArgv(args);
|
|
@@ -177,6 +201,14 @@ async function setupRobot(config, opts, ctx, internal) {
|
|
|
177
201
|
args: robotArgv.slice(1),
|
|
178
202
|
});
|
|
179
203
|
console.log(reg.ok ? chalk.green(` ✓ ${reg.message}`) : chalk.yellow(` ⚠ ${reg.message}`));
|
|
204
|
+
const previewArgv = [process.execPath, process.argv[1], ...previewAgentArgs, '--foreground'];
|
|
205
|
+
const reg2 = await registerAutoStart({
|
|
206
|
+
role: 'robot-preview-agent',
|
|
207
|
+
name: internal.name,
|
|
208
|
+
command: previewArgv[0],
|
|
209
|
+
args: previewArgv.slice(1),
|
|
210
|
+
});
|
|
211
|
+
console.log(reg2.ok ? chalk.green(` ✓ ${reg2.message}`) : chalk.yellow(` ⚠ ${reg2.message}`));
|
|
180
212
|
}
|
|
181
213
|
}
|
|
182
214
|
async function setupControl(config, opts, ctx, internal) {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark — `robopark verify`.
|
|
3
|
+
*
|
|
4
|
+
* Checks the fleet end-to-end and reports what's working vs. missing:
|
|
5
|
+
* - hub / scheduler reachable
|
|
6
|
+
* - LiveKit configured and server registered
|
|
7
|
+
* - production mode enabled
|
|
8
|
+
* - robots/devices enrolled
|
|
9
|
+
* - preview agent running on robots
|
|
10
|
+
* - preview room can be started
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* robopark verify
|
|
14
|
+
* robopark verify --scheduler-url http://livekit-1:8080 --robot-id robobmw
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { homedir, hostname } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import chalk from 'chalk';
|
|
20
|
+
import { discoverContext } from './discovery.js';
|
|
21
|
+
function loadDeviceToken(robotId) {
|
|
22
|
+
const paths = [
|
|
23
|
+
join(homedir(), '.robopark', 'device_token'),
|
|
24
|
+
];
|
|
25
|
+
for (const p of paths) {
|
|
26
|
+
if (existsSync(p))
|
|
27
|
+
return readFile(p);
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
function readFile(p) {
|
|
32
|
+
const { readFileSync } = require('node:fs');
|
|
33
|
+
return readFileSync(p, 'utf8').trim();
|
|
34
|
+
}
|
|
35
|
+
async function fetchJson(url, init) {
|
|
36
|
+
const res = await fetch(url, { ...init, signal: AbortSignal.timeout(5000) });
|
|
37
|
+
if (!res.ok)
|
|
38
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
39
|
+
return res.json();
|
|
40
|
+
}
|
|
41
|
+
export async function roboparkVerify(opts) {
|
|
42
|
+
const ctx = await discoverContext();
|
|
43
|
+
const schedulerUrl = (opts.schedulerUrl ?? (ctx.hub ? `http://${ctx.hub.ip}:8080` : 'http://localhost:8080')).replace(/\/$/, '');
|
|
44
|
+
const robotId = opts.robotId ?? hostname().split('.')[0];
|
|
45
|
+
console.log(chalk.bold('\n robopark verify'));
|
|
46
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
47
|
+
console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
|
|
48
|
+
console.log(` robot: ${chalk.cyan(robotId)}`);
|
|
49
|
+
console.log();
|
|
50
|
+
const checks = [];
|
|
51
|
+
// 1. Scheduler reachable
|
|
52
|
+
try {
|
|
53
|
+
const settings = (await fetchJson(`${schedulerUrl}/api/settings`));
|
|
54
|
+
checks.push({ name: 'scheduler reachable', ok: true, message: `production_mode=${settings.production_mode ?? false}` });
|
|
55
|
+
if (!settings.production_mode) {
|
|
56
|
+
checks.push({ name: 'production mode', ok: false, message: 'Production mode is OFF. Toggle it in the dashboard or PUT /api/settings', detail: 'Pi preview/session enrollment is blocked until production mode is ON.' });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
checks.push({ name: 'production mode', ok: true, message: 'ON' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
checks.push({ name: 'scheduler reachable', ok: false, message: `could not reach scheduler: ${e.message}`, detail: 'Is robopark serve running on the hub? Is the scheduler port open?' });
|
|
64
|
+
}
|
|
65
|
+
// 2. LiveKit configured
|
|
66
|
+
try {
|
|
67
|
+
const lk = (await fetchJson(`${schedulerUrl}/api/livekit/config`));
|
|
68
|
+
if (lk.url && lk.has_secret) {
|
|
69
|
+
checks.push({ name: 'livekit configured', ok: true, message: `url=${lk.url}` });
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
checks.push({ name: 'livekit configured', ok: false, message: 'LiveKit URL or API secret missing', detail: 'Set LiveKit config in the dashboard or via robopark setup-livekit.' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
checks.push({ name: 'livekit configured', ok: false, message: `error: ${e.message}` });
|
|
77
|
+
}
|
|
78
|
+
// 3. LiveKit server registered
|
|
79
|
+
try {
|
|
80
|
+
const servers = (await fetchJson(`${schedulerUrl}/api/servers`));
|
|
81
|
+
const online = servers.filter(s => s.status === 'online');
|
|
82
|
+
if (online.length > 0) {
|
|
83
|
+
checks.push({ name: 'livekit server registered', ok: true, message: `${online.length} online server(s)` });
|
|
84
|
+
}
|
|
85
|
+
else if (servers.length > 0) {
|
|
86
|
+
checks.push({ name: 'livekit server registered', ok: false, message: `${servers.length} server(s) but none online`, detail: 'Check that the LiveKit container/process is running and reachable from the hub.' });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
checks.push({ name: 'livekit server registered', ok: false, message: 'no servers in scheduler', detail: 'Add a LiveKit server in the dashboard or run robopark setup-livekit.' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
checks.push({ name: 'livekit server registered', ok: false, message: `error: ${e.message}` });
|
|
94
|
+
}
|
|
95
|
+
// 4. Device enrolled
|
|
96
|
+
let deviceToken;
|
|
97
|
+
let deviceId;
|
|
98
|
+
try {
|
|
99
|
+
const devices = (await fetchJson(`${schedulerUrl}/api/devices`));
|
|
100
|
+
const device = devices.find(d => d.name === robotId || d.id === robotId);
|
|
101
|
+
if (device) {
|
|
102
|
+
deviceId = device.id;
|
|
103
|
+
checks.push({ name: 'device enrolled', ok: true, message: `${device.id} (${device.status})` });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
checks.push({ name: 'device enrolled', ok: false, message: `${robotId} not found in scheduler`, detail: 'Run robopark enroll on the robot with the scheduler enrollment token.' });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
checks.push({ name: 'device enrolled', ok: false, message: `error: ${e.message}` });
|
|
111
|
+
}
|
|
112
|
+
// 5. Local device token
|
|
113
|
+
deviceToken = loadDeviceToken(robotId);
|
|
114
|
+
if (deviceToken) {
|
|
115
|
+
checks.push({ name: 'local device token', ok: true, message: `found ~/.robopark/device_token` });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
checks.push({ name: 'local device token', ok: false, message: 'no ~/.robopark/device_token', detail: 'Run robopark enroll on this machine first.' });
|
|
119
|
+
}
|
|
120
|
+
// 6. Robot row exists
|
|
121
|
+
try {
|
|
122
|
+
const robots = (await fetchJson(`${schedulerUrl}/api/robots`));
|
|
123
|
+
const robot = robots.find(r => r.id === (deviceId || robotId) || r.name === robotId);
|
|
124
|
+
if (robot) {
|
|
125
|
+
checks.push({ name: 'robot registered', ok: true, message: `${robot.id} (${robot.status})` });
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
checks.push({ name: 'robot registered', ok: false, message: `${robotId} not in robots table`, detail: 'The device should auto-create a robot row on enrollment. Re-run enroll or check scheduler logs.' });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
checks.push({ name: 'robot registered', ok: false, message: `error: ${e.message}` });
|
|
133
|
+
}
|
|
134
|
+
// 7. Preview agent polling
|
|
135
|
+
if (deviceToken) {
|
|
136
|
+
try {
|
|
137
|
+
const agentRes = (await fetchJson(`${schedulerUrl}/api/robots/${robotId}/preview/agent`, {
|
|
138
|
+
headers: { Authorization: `Bearer ${deviceToken}` },
|
|
139
|
+
}));
|
|
140
|
+
if (agentRes.active) {
|
|
141
|
+
checks.push({ name: 'preview agent polling', ok: true, message: 'preview currently active' });
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
checks.push({ name: 'preview agent polling', ok: true, message: `no active preview (reason=${agentRes.reason || 'none'})`, detail: 'This is normal when the dashboard is not watching. Start a preview from the dashboard.' });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
checks.push({ name: 'preview agent polling', ok: false, message: `error: ${e.message}`, detail: 'Is robopark preview-agent running on the robot?' });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
checks.push({ name: 'preview agent polling', ok: false, message: 'skipped (no device token)' });
|
|
153
|
+
}
|
|
154
|
+
// 8. Try starting a preview from the operator side
|
|
155
|
+
try {
|
|
156
|
+
const preview = (await fetchJson(`${schedulerUrl}/api/robots/${robotId}/preview/start`, { method: 'POST' }));
|
|
157
|
+
if (preview.active) {
|
|
158
|
+
checks.push({ name: 'operator preview start', ok: true, message: `room assigned at ${preview.url}` });
|
|
159
|
+
// stop it so we don't leave a stale preview running
|
|
160
|
+
await fetch(`${schedulerUrl}/api/robots/${robotId}/preview/stop`, { method: 'POST' });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
checks.push({ name: 'operator preview start', ok: false, message: `could not start: ${preview.reason || 'unknown'}`, detail: 'Enable production mode, add a LiveKit server, and ensure the robot is not in a session.' });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
checks.push({ name: 'operator preview start', ok: false, message: `error: ${e.message}` });
|
|
168
|
+
}
|
|
169
|
+
// Print results
|
|
170
|
+
let pass = 0;
|
|
171
|
+
let fail = 0;
|
|
172
|
+
for (const c of checks) {
|
|
173
|
+
const icon = c.ok ? chalk.green('✓') : chalk.red('✗');
|
|
174
|
+
const color = c.ok ? chalk.green : chalk.red;
|
|
175
|
+
console.log(` ${icon} ${color(c.name)} — ${c.message}`);
|
|
176
|
+
if (c.detail) {
|
|
177
|
+
console.log(chalk.dim(` → ${c.detail}`));
|
|
178
|
+
}
|
|
179
|
+
if (c.ok)
|
|
180
|
+
pass++;
|
|
181
|
+
else
|
|
182
|
+
fail++;
|
|
183
|
+
}
|
|
184
|
+
console.log();
|
|
185
|
+
if (fail === 0) {
|
|
186
|
+
console.log(chalk.bold.green(` ${pass}/${checks.length} checks passed — fleet is ready for preview.`));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(chalk.bold.yellow(` ${pass}/${checks.length} passed, ${fail} failed — fix the items above.`));
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
package/dist/robopark-cli.js
CHANGED
|
@@ -17,7 +17,7 @@ const config = new Conf({
|
|
|
17
17
|
});
|
|
18
18
|
const program = new Command('robopark')
|
|
19
19
|
.description('RoboPark fleet control CLI — set up, watch, and drive talking robots')
|
|
20
|
-
.version('2.8.
|
|
20
|
+
.version('2.8.9');
|
|
21
21
|
program
|
|
22
22
|
.command('scan')
|
|
23
23
|
.description('Auto-discover the fleet and print ready-to-run setup commands')
|
|
@@ -31,6 +31,38 @@ program
|
|
|
31
31
|
gateway: opts.gateway,
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
|
+
program
|
|
35
|
+
.command('probe')
|
|
36
|
+
.description('Dispatch opencode agents across the mesh to map infrastructure')
|
|
37
|
+
.option('--token <token>', 'mesh auth token (optional, for context)')
|
|
38
|
+
.option('--timeout <sec>', 'seconds per node', '60')
|
|
39
|
+
.option('--output <file>', 'write JSON results to this file')
|
|
40
|
+
.option('--local', 'probe only this machine')
|
|
41
|
+
.action(async (opts) => {
|
|
42
|
+
const { roboparkProbe } = await import('./robopark/probe.js');
|
|
43
|
+
await roboparkProbe(opts);
|
|
44
|
+
});
|
|
45
|
+
program
|
|
46
|
+
.command('setup-livekit')
|
|
47
|
+
.description('Dispatch an opencode agent on the hub to install LiveKit server via Docker')
|
|
48
|
+
.option('--hub-url <url>', 'hub federation URL, e.g. http://100.64.1.2:47913')
|
|
49
|
+
.option('--token <token>', 'mesh auth token')
|
|
50
|
+
.option('--scheduler-url <url>', 'scheduler URL to register the new server, e.g. http://100.64.1.2:8080')
|
|
51
|
+
.option('--timeout <sec>', 'max seconds to wait for the agent', '180')
|
|
52
|
+
.option('--dry-run', 'print the prompt instead of dispatching')
|
|
53
|
+
.action(async (opts) => {
|
|
54
|
+
const { roboparkSetupLivekit } = await import('./robopark/setup-livekit.js');
|
|
55
|
+
await roboparkSetupLivekit(opts);
|
|
56
|
+
});
|
|
57
|
+
program
|
|
58
|
+
.command('verify')
|
|
59
|
+
.description('Verify the RoboPark fleet is ready for live preview')
|
|
60
|
+
.option('--scheduler-url <url>', 'scheduler base URL, e.g. http://livekit-1:8080')
|
|
61
|
+
.option('--robot-id <id>', 'robot name to verify (defaults to hostname)')
|
|
62
|
+
.action(async (opts) => {
|
|
63
|
+
const { roboparkVerify } = await import('./robopark/verify.js');
|
|
64
|
+
await roboparkVerify(opts);
|
|
65
|
+
});
|
|
34
66
|
program
|
|
35
67
|
.command('serve')
|
|
36
68
|
.description('Start the RoboPark scheduler + web UI')
|
|
@@ -44,6 +76,35 @@ program
|
|
|
44
76
|
.action(async (opts) => {
|
|
45
77
|
await roboparkServe(opts);
|
|
46
78
|
});
|
|
79
|
+
program
|
|
80
|
+
.command('enroll')
|
|
81
|
+
.description('Enroll this robot/satellite with the RoboPark scheduler')
|
|
82
|
+
.option('--scheduler-url <url>', 'scheduler base URL', 'http://localhost:8080')
|
|
83
|
+
.option('--robot-id <id>', 'robot/device name (defaults to hostname)')
|
|
84
|
+
.option('--enrollment-token <token>', 'one-time enrollment token (or ENV ENROLLMENT_TOKEN)')
|
|
85
|
+
.option('--save-config', 'write settings to ~/.robopark/preview_agent.json')
|
|
86
|
+
.action(async (opts) => {
|
|
87
|
+
const { roboparkEnroll } = await import('./robopark/enroll.js');
|
|
88
|
+
await roboparkEnroll(opts);
|
|
89
|
+
});
|
|
90
|
+
program
|
|
91
|
+
.command('preview-agent')
|
|
92
|
+
.description('Start the robot-side preview agent (camera + mic to scheduler)')
|
|
93
|
+
.option('--scheduler-url <url>', 'scheduler base URL', 'http://localhost:8080')
|
|
94
|
+
.option('--robot-id <id>', 'robot/device name (defaults to hostname)')
|
|
95
|
+
.option('--device-token <token>', 'long-lived device token')
|
|
96
|
+
.option('--enrollment-token <token>', 'one-time enrollment token')
|
|
97
|
+
.option('--video-device <dev>', 'camera: auto, /dev/video0, 0, picamera', 'auto')
|
|
98
|
+
.option('--audio-device <dev>', 'microphone: default, none, or name/index', 'default')
|
|
99
|
+
.option('--width <px>', 'video width', '640')
|
|
100
|
+
.option('--height <px>', 'video height', '480')
|
|
101
|
+
.option('--fps <n>', 'video fps', '15')
|
|
102
|
+
.option('--save-config', 'write settings to ~/.robopark/preview_agent.json')
|
|
103
|
+
.option('--foreground', 'stay in foreground; do not detach')
|
|
104
|
+
.action(async (opts) => {
|
|
105
|
+
const { roboparkPreviewAgent } = await import('./robopark/preview-agent-launcher.js');
|
|
106
|
+
await roboparkPreviewAgent(opts);
|
|
107
|
+
});
|
|
47
108
|
program
|
|
48
109
|
.command('setup')
|
|
49
110
|
.description('Set up this device in one pass')
|
|
@@ -61,6 +122,9 @@ program
|
|
|
61
122
|
.option('--scheduler-port <port>', 'scheduler port on hub', '8080')
|
|
62
123
|
.option('--tailscale-auth <token>', 'Tailscale auth key for the hub')
|
|
63
124
|
.option('--tag <tag>', 'Tailscale tag filter')
|
|
125
|
+
.option('--enrollment-token <token>', 'one-time scheduler enrollment token for robots')
|
|
126
|
+
.option('--video-device <dev>', 'default camera for robot preview agent', 'auto')
|
|
127
|
+
.option('--audio-device <dev>', 'default microphone for robot preview agent', 'default')
|
|
64
128
|
.option('--start', 'launch the node now')
|
|
65
129
|
.option('--auto-start', 'register to start on boot')
|
|
66
130
|
.option('--yes', 'non-interactive mode')
|
|
@@ -82,6 +146,9 @@ program
|
|
|
82
146
|
schedulerPort: opts.schedulerPort,
|
|
83
147
|
tailscaleAuth: opts.tailscaleAuth,
|
|
84
148
|
tag: opts.tag,
|
|
149
|
+
enrollmentToken: opts.enrollmentToken,
|
|
150
|
+
videoDevice: opts.videoDevice,
|
|
151
|
+
audioDevice: opts.audioDevice,
|
|
85
152
|
start: opts.start,
|
|
86
153
|
autoStart: opts.autoStart,
|
|
87
154
|
yes: opts.yes,
|
package/package.json
CHANGED
|
Binary file
|