@newly/cli 0.1.1 → 0.1.27
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/README.md +20 -19
- package/README.runcloud.md +253 -0
- package/dist/api.js +123 -43
- package/dist/commands/appStore.js +162 -0
- package/dist/commands/appStoreEvaluate.js +515 -0
- package/dist/commands/boxAccess.js +349 -0
- package/dist/commands/build.js +489 -3
- package/dist/commands/create.js +106 -30
- package/dist/commands/deploy.js +100 -129
- package/dist/commands/desktop.js +55 -0
- package/dist/commands/environment.js +290 -0
- package/dist/commands/image.js +145 -0
- package/dist/commands/login.js +266 -19
- package/dist/commands/run-cloud.js +482 -0
- package/dist/commands/sandbox.js +787 -0
- package/dist/commands/sandboxOutput.js +70 -0
- package/dist/commands/secrets.js +203 -0
- package/dist/config.js +72 -7
- package/dist/deviceLogin.js +48 -0
- package/dist/diagnose.js +159 -0
- package/dist/index.js +8 -0
- package/dist/prompt.js +196 -0
- package/dist/run-cloud.js +17 -0
- package/dist/runCloudAuth.js +79 -0
- package/dist/sandboxExec.js +91 -0
- package/dist/services-manifest.js +30 -1
- package/dist/shellSession.js +86 -0
- package/dist/terminal.js +158 -0
- package/dist/version.js +1 -0
- package/package.json +23 -7
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { ApiClient, friendlyApiError } from '../api.js';
|
|
8
|
+
import { requireCredentials } from '../config.js';
|
|
9
|
+
export const RUN_CLOUD_SKILL_NAMES = [
|
|
10
|
+
'run-cloud',
|
|
11
|
+
'run-cloud-ios-simulator',
|
|
12
|
+
'run-cloud-sandboxes',
|
|
13
|
+
];
|
|
14
|
+
const RUN_CLOUD_SKILL_FILE_NAMES = ['SKILL.md', 'agents/openai.yaml'];
|
|
15
|
+
function loadRunCloudSkillFiles(skillName) {
|
|
16
|
+
const embedded = globalThis.__RUN_CLOUD_EMBEDDED_SKILLS__?.[skillName];
|
|
17
|
+
if (embedded)
|
|
18
|
+
return embedded;
|
|
19
|
+
const commandDirectory = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const candidates = [
|
|
21
|
+
resolve(commandDirectory, '../../skills', skillName),
|
|
22
|
+
resolve(commandDirectory, '../../../.claude/skills', skillName),
|
|
23
|
+
];
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
if (RUN_CLOUD_SKILL_FILE_NAMES.every((filename) => existsSync(join(candidate, filename)))) {
|
|
26
|
+
return Object.fromEntries(RUN_CLOUD_SKILL_FILE_NAMES.map((filename) => [
|
|
27
|
+
filename,
|
|
28
|
+
readFileSync(join(candidate, filename), 'utf8'),
|
|
29
|
+
]));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`run.cloud agent skill ${skillName} is missing from the package; reinstall runcloud or use npx skills add newly-app/run-cloud-examples`);
|
|
33
|
+
}
|
|
34
|
+
const RUN_CLOUD_SKILL_FILES = Object.fromEntries(RUN_CLOUD_SKILL_NAMES.map((skillName) => [
|
|
35
|
+
skillName,
|
|
36
|
+
loadRunCloudSkillFiles(skillName),
|
|
37
|
+
]));
|
|
38
|
+
export const RUN_CLOUD_SKILLS = Object.fromEntries(RUN_CLOUD_SKILL_NAMES.map((skillName) => [
|
|
39
|
+
skillName,
|
|
40
|
+
RUN_CLOUD_SKILL_FILES[skillName]['SKILL.md'],
|
|
41
|
+
]));
|
|
42
|
+
export const RUN_CLOUD_SKILL = RUN_CLOUD_SKILLS['run-cloud-ios-simulator'];
|
|
43
|
+
function client() {
|
|
44
|
+
const creds = requireCredentials();
|
|
45
|
+
return new ApiClient(creds.apiUrl, creds.token);
|
|
46
|
+
}
|
|
47
|
+
function print(value, opts = {}) {
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify(value, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
for (const item of value)
|
|
54
|
+
console.log(formatRecord(item));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.log(formatRecord(value));
|
|
58
|
+
}
|
|
59
|
+
function formatRecord(value) {
|
|
60
|
+
if (!value || typeof value !== 'object')
|
|
61
|
+
return String(value);
|
|
62
|
+
const record = value;
|
|
63
|
+
return Object.entries(record)
|
|
64
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
65
|
+
.map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : String(v)}`)
|
|
66
|
+
.join('\n');
|
|
67
|
+
}
|
|
68
|
+
function parseLabels(labels) {
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const raw of labels ?? []) {
|
|
71
|
+
const idx = raw.indexOf('=');
|
|
72
|
+
if (idx <= 0)
|
|
73
|
+
throw new Error(`Invalid --label ${raw}; expected key=value`);
|
|
74
|
+
out[raw.slice(0, idx)] = raw.slice(idx + 1);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function fileBlob(path) {
|
|
79
|
+
const resolved = resolve(path);
|
|
80
|
+
if (!existsSync(resolved))
|
|
81
|
+
throw new Error(`File does not exist: ${path}`);
|
|
82
|
+
if (!statSync(resolved).isFile())
|
|
83
|
+
throw new Error(`Not a file: ${path}`);
|
|
84
|
+
const data = readFileSync(resolved);
|
|
85
|
+
return {
|
|
86
|
+
blob: new Blob([new Uint8Array(data)], { type: 'application/octet-stream' }),
|
|
87
|
+
name: basename(resolved),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function skillBase(agent, scope) {
|
|
91
|
+
if (scope === 'project') {
|
|
92
|
+
if (agent === 'claude')
|
|
93
|
+
return join(process.cwd(), '.claude', 'skills');
|
|
94
|
+
if (agent === 'codex')
|
|
95
|
+
return join(process.cwd(), '.codex', 'skills');
|
|
96
|
+
if (agent === 'cursor')
|
|
97
|
+
return join(process.cwd(), '.agents', 'skills');
|
|
98
|
+
}
|
|
99
|
+
if (agent === 'claude')
|
|
100
|
+
return join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'), 'skills');
|
|
101
|
+
if (agent === 'codex')
|
|
102
|
+
return join(process.env.CODEX_HOME ?? join(homedir(), '.codex'), 'skills');
|
|
103
|
+
if (agent === 'cursor')
|
|
104
|
+
return join(homedir(), '.agents', 'skills');
|
|
105
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
106
|
+
}
|
|
107
|
+
function installSkills(opts) {
|
|
108
|
+
const agents = opts.agents?.length ? opts.agents : ['claude', 'codex', 'cursor'];
|
|
109
|
+
const scope = opts.scope ?? 'project';
|
|
110
|
+
const targets = agents.flatMap((agent) => RUN_CLOUD_SKILL_NAMES.map((skill) => ({
|
|
111
|
+
agent,
|
|
112
|
+
skill,
|
|
113
|
+
path: join(skillBase(agent, scope), skill),
|
|
114
|
+
})));
|
|
115
|
+
if (!opts.force) {
|
|
116
|
+
const existing = targets.find((target) => existsSync(target.path));
|
|
117
|
+
if (existing) {
|
|
118
|
+
throw new Error(`${existing.path} already exists. Pass --force to overwrite.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const results = [];
|
|
122
|
+
for (const { agent, skill, path: target } of targets) {
|
|
123
|
+
if (existsSync(target))
|
|
124
|
+
rmSync(target, { recursive: true, force: true });
|
|
125
|
+
for (const filename of RUN_CLOUD_SKILL_FILE_NAMES) {
|
|
126
|
+
const destination = join(target, filename);
|
|
127
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
128
|
+
writeFileSync(destination, RUN_CLOUD_SKILL_FILES[skill][filename]);
|
|
129
|
+
}
|
|
130
|
+
results.push({ agent, skill, path: target, status: 'installed' });
|
|
131
|
+
}
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
const RUN_CLOUD_DEMOS = [
|
|
135
|
+
'eight-device-mosaic',
|
|
136
|
+
'live-camera-relay',
|
|
137
|
+
];
|
|
138
|
+
export const RUN_CLOUD_SAMPLES = {
|
|
139
|
+
ios: {
|
|
140
|
+
filename: 'run-cloud-sample-ios.app.tar.gz',
|
|
141
|
+
url: 'https://run.cloud/downloads/run-cloud-sample-ios.app.tar.gz',
|
|
142
|
+
sha256: '09c437b1f299d41f79a8453611d99b8b1e4a3f5198dd871f4dad183362f944b3',
|
|
143
|
+
byteSize: 19_516,
|
|
144
|
+
},
|
|
145
|
+
android: {
|
|
146
|
+
filename: 'run-cloud-sample-android.apk',
|
|
147
|
+
url: 'https://run.cloud/downloads/run-cloud-sample-android.apk',
|
|
148
|
+
sha256: '892d4441a24757fc88852d29abe87d42d14f04921cbe89802e369dad6e46edba',
|
|
149
|
+
byteSize: 6_521_225,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
function samplePlatform(value) {
|
|
153
|
+
if (value === 'ios' || value === 'android')
|
|
154
|
+
return value;
|
|
155
|
+
throw new Error(`Unknown sample platform: ${value}. Choose ios or android.`);
|
|
156
|
+
}
|
|
157
|
+
function shellQuote(value) {
|
|
158
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
159
|
+
}
|
|
160
|
+
export async function downloadRunCloudSample(rawPlatform, opts = {}, fetchSample = fetch, samples = RUN_CLOUD_SAMPLES) {
|
|
161
|
+
const platform = samplePlatform(rawPlatform);
|
|
162
|
+
const sample = samples[platform];
|
|
163
|
+
const target = resolve(opts.output ?? sample.filename);
|
|
164
|
+
if (existsSync(target) && !opts.force) {
|
|
165
|
+
throw new Error(`${target} already exists. Pass --force to overwrite it.`);
|
|
166
|
+
}
|
|
167
|
+
const response = await fetchSample(sample.url);
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
throw new Error(`Could not download the ${platform} sample: HTTP ${response.status}`);
|
|
170
|
+
}
|
|
171
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
172
|
+
const sha256 = createHash('sha256').update(body).digest('hex');
|
|
173
|
+
if (body.byteLength !== sample.byteSize || sha256 !== sample.sha256) {
|
|
174
|
+
throw new Error(`The ${platform} sample failed its checksum verification.`);
|
|
175
|
+
}
|
|
176
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
177
|
+
writeFileSync(target, body);
|
|
178
|
+
return {
|
|
179
|
+
platform,
|
|
180
|
+
path: target,
|
|
181
|
+
url: sample.url,
|
|
182
|
+
sha256,
|
|
183
|
+
byteSize: body.byteLength,
|
|
184
|
+
next: `runcloud ${platform} create --install ${shellQuote(target)}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function experimentalWebRtcEnabled() {
|
|
188
|
+
return ['1', 'true', 'yes', 'on'].includes((process.env.RUN_CLOUD_EXPERIMENTAL_WEBRTC ?? '').trim().toLowerCase());
|
|
189
|
+
}
|
|
190
|
+
function assertDevHostAllowed(devHost) {
|
|
191
|
+
if (devHost && process.env.RUN_CLOUD_DEV_FEATURES !== '1') {
|
|
192
|
+
throw new Error('--dev-host is dev-only; set RUN_CLOUD_DEV_FEATURES=1 to use it');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function requestedCodec(codec) {
|
|
196
|
+
const value = codec?.trim().toLowerCase() || 'auto';
|
|
197
|
+
if (value === 'webrtc' && !experimentalWebRtcEnabled()) {
|
|
198
|
+
throw new Error('--codec webrtc is experimental; set RUN_CLOUD_EXPERIMENTAL_WEBRTC=1 to use it');
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
function demoDirectory(name) {
|
|
203
|
+
if (!RUN_CLOUD_DEMOS.some((demoName) => demoName === name)) {
|
|
204
|
+
throw new Error(`Unknown demo: ${name}. Available demos: ${RUN_CLOUD_DEMOS.join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
const source = resolve(dirname(fileURLToPath(import.meta.url)), '../../examples', name);
|
|
207
|
+
if (!existsSync(source))
|
|
208
|
+
throw new Error(`The ${name} demo is missing from this runcloud package.`);
|
|
209
|
+
return source;
|
|
210
|
+
}
|
|
211
|
+
export function initRunCloudDemo(name = 'eight-device-mosaic', directory, opts = {}) {
|
|
212
|
+
const source = demoDirectory(name);
|
|
213
|
+
const target = resolve(directory ?? `run-cloud-${name}`);
|
|
214
|
+
if (existsSync(target)) {
|
|
215
|
+
if (!opts.force)
|
|
216
|
+
throw new Error(`${target} already exists. Pass --force to overwrite it.`);
|
|
217
|
+
rmSync(target, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
cpSync(source, target, { recursive: true });
|
|
220
|
+
return { example: name, path: target, status: 'created' };
|
|
221
|
+
}
|
|
222
|
+
export async function runCloudDemo(name = 'eight-device-mosaic', opts = {}, loadDemo = async (url) => (await import(url))) {
|
|
223
|
+
if (name === 'eight-device-mosaic' && opts.duration) {
|
|
224
|
+
throw new Error('The eight-device mosaic uses a fixed 10-minute lease and does not accept --duration.');
|
|
225
|
+
}
|
|
226
|
+
const args = [];
|
|
227
|
+
if (opts.duration)
|
|
228
|
+
args.push('--duration', opts.duration);
|
|
229
|
+
if (opts.open)
|
|
230
|
+
args.push('--open');
|
|
231
|
+
const source = join(demoDirectory(name), 'demo.mjs');
|
|
232
|
+
const demo = await loadDemo(pathToFileURL(source).href);
|
|
233
|
+
await demo.runDemo(args);
|
|
234
|
+
}
|
|
235
|
+
async function pushAsset(path, opts = {}) {
|
|
236
|
+
const { blob, name } = fileBlob(path);
|
|
237
|
+
const api = client();
|
|
238
|
+
const prepared = await api.post('/run-cloud/assets/uploads', {
|
|
239
|
+
filename: name,
|
|
240
|
+
...(opts.name ? { name: opts.name } : {}),
|
|
241
|
+
contentType: blob.type || 'application/octet-stream',
|
|
242
|
+
byteSize: blob.size,
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
const uploaded = await fetch(prepared.upload.url, {
|
|
246
|
+
method: 'PUT',
|
|
247
|
+
headers: prepared.upload.headers,
|
|
248
|
+
body: blob,
|
|
249
|
+
});
|
|
250
|
+
if (!uploaded.ok)
|
|
251
|
+
throw new Error(`asset storage upload failed with HTTP ${uploaded.status}`);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (prepared.asset.id) {
|
|
255
|
+
await api.delete(`/run-cloud/assets/${encodeURIComponent(prepared.asset.id)}`).catch(() => undefined);
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
return prepared.asset;
|
|
260
|
+
}
|
|
261
|
+
async function createSimulatorSession(platform, opts) {
|
|
262
|
+
assertDevHostAllowed(opts.devHost);
|
|
263
|
+
const uploadedAssets = [];
|
|
264
|
+
for (const file of opts.install ?? [])
|
|
265
|
+
uploadedAssets.push(await pushAsset(file));
|
|
266
|
+
const installAssets = [
|
|
267
|
+
...(opts.installAsset ?? []),
|
|
268
|
+
...uploadedAssets
|
|
269
|
+
.map((asset) => asset.id)
|
|
270
|
+
.filter((id) => typeof id === 'string' && id.length > 0),
|
|
271
|
+
];
|
|
272
|
+
return await client().post(`/run-cloud/${platform}`, {
|
|
273
|
+
model: opts.model,
|
|
274
|
+
region: opts.region,
|
|
275
|
+
displayName: opts.displayName,
|
|
276
|
+
labels: parseLabels(opts.label),
|
|
277
|
+
installAssets,
|
|
278
|
+
inactivityTimeout: opts.inactivityTimeout,
|
|
279
|
+
hardTimeout: opts.hardTimeout,
|
|
280
|
+
autoDelete: opts.rm,
|
|
281
|
+
codec: requestedCodec(opts.codec),
|
|
282
|
+
...(opts.devHost ? { devHost: opts.devHost } : {}),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function registerSimulatorCommands(program, platform) {
|
|
286
|
+
const label = platform === 'android' ? 'Android emulator' : 'iOS simulator';
|
|
287
|
+
const article = platform === 'android' ? 'an' : 'an';
|
|
288
|
+
const simulator = program.command(platform).description(`Create, list, inspect, and delete ${label} sessions`);
|
|
289
|
+
simulator
|
|
290
|
+
.command('create')
|
|
291
|
+
.description(`Create a remote ${label} session`)
|
|
292
|
+
.option('--model <model>', platform === 'ios' ? 'simulator model' : 'emulator model', platform === 'ios' ? 'iphone' : 'pixel')
|
|
293
|
+
.option('--region <region>', 'fleet region')
|
|
294
|
+
.option('--display-name <name>', 'human-readable name')
|
|
295
|
+
.option('--label <key=value>', 'label to attach to the session', (v, p) => [...p, v], [])
|
|
296
|
+
.option('--install <file>', 'local app artifact to upload and install', (v, p) => [...p, v], [])
|
|
297
|
+
.option('--install-asset <name-or-id>', 'previously uploaded asset to install', (v, p) => [...p, v], [])
|
|
298
|
+
.option('--inactivity-timeout <duration>', 'auto-close idle timeout, e.g. 60s, 3m, or none')
|
|
299
|
+
.option('--hard-timeout <duration>', 'maximum lifetime, e.g. 10m or 1h')
|
|
300
|
+
.option('--codec <auto|mjpeg|webrtc>', 'force the simulator viewer stream codec', 'auto')
|
|
301
|
+
.option('--rm', 'release the session when this command exits', false)
|
|
302
|
+
.option('--json', 'output JSON', false)
|
|
303
|
+
.addOption(new Option('--dev-host <host>', 'dev-only: target a specific simulator fleet host').hideHelp())
|
|
304
|
+
.action((opts) => action(async () => {
|
|
305
|
+
print(await createSimulatorSession(platform, opts), opts);
|
|
306
|
+
}));
|
|
307
|
+
simulator
|
|
308
|
+
.command('list')
|
|
309
|
+
.description(`List active ${label} sessions`)
|
|
310
|
+
.option('--all', 'include released sessions', false)
|
|
311
|
+
.option('--json', 'output JSON', false)
|
|
312
|
+
.action((opts) => action(async () => {
|
|
313
|
+
const qs = opts.all ? '?all=1' : '';
|
|
314
|
+
print(await client().get(`/run-cloud/${platform}${qs}`), opts);
|
|
315
|
+
}));
|
|
316
|
+
simulator
|
|
317
|
+
.command('get')
|
|
318
|
+
.description(`Show ${article} ${label} session`)
|
|
319
|
+
.argument('<id>')
|
|
320
|
+
.option('--json', 'output JSON', false)
|
|
321
|
+
.action((id, opts) => action(async () => {
|
|
322
|
+
print(await client().get(`/run-cloud/${platform}/${encodeURIComponent(id)}`), opts);
|
|
323
|
+
}));
|
|
324
|
+
simulator
|
|
325
|
+
.command('delete')
|
|
326
|
+
.description(`Release ${article} ${label} session`)
|
|
327
|
+
.argument('<id>')
|
|
328
|
+
.option('--json', 'output JSON', false)
|
|
329
|
+
.action((id, opts) => action(async () => {
|
|
330
|
+
print(await client().delete(`/run-cloud/${platform}/${encodeURIComponent(id)}`), opts);
|
|
331
|
+
}));
|
|
332
|
+
simulator
|
|
333
|
+
.command('open-url')
|
|
334
|
+
.description(`Open a URL or deep link in ${article} ${label} session`)
|
|
335
|
+
.argument('<url>')
|
|
336
|
+
.requiredOption('--id <id>', 'session id')
|
|
337
|
+
.option('--json', 'output JSON', false)
|
|
338
|
+
.action((url, opts) => action(async () => {
|
|
339
|
+
print(await client().post(`/run-cloud/${platform}/${encodeURIComponent(opts.id)}/open-url`, { url }), opts);
|
|
340
|
+
}));
|
|
341
|
+
return simulator;
|
|
342
|
+
}
|
|
343
|
+
async function action(fn) {
|
|
344
|
+
try {
|
|
345
|
+
await fn();
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
console.error(friendlyApiError(err));
|
|
349
|
+
process.exitCode = 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
export function registerRunCloud(program) {
|
|
353
|
+
const sample = program
|
|
354
|
+
.command('sample')
|
|
355
|
+
.alias('samples')
|
|
356
|
+
.description('Download installable onboarding sample apps');
|
|
357
|
+
sample
|
|
358
|
+
.command('download')
|
|
359
|
+
.description('Download a checksum-verified iOS or Android sample app')
|
|
360
|
+
.argument('<ios|android>', 'sample platform')
|
|
361
|
+
.option('-o, --output <path>', 'output file')
|
|
362
|
+
.option('--force', 'overwrite an existing output file', false)
|
|
363
|
+
.option('--json', 'output JSON', false)
|
|
364
|
+
.action((platform, opts) => action(async () => {
|
|
365
|
+
print(await downloadRunCloudSample(platform, opts), opts);
|
|
366
|
+
}));
|
|
367
|
+
const demo = program.command('demo').description('Create and run run.cloud demo projects');
|
|
368
|
+
demo
|
|
369
|
+
.command('init')
|
|
370
|
+
.description('Scaffold a standalone demo project')
|
|
371
|
+
.argument('[name]', 'demo to create', 'eight-device-mosaic')
|
|
372
|
+
.argument('[directory]', 'output directory')
|
|
373
|
+
.option('--force', 'overwrite an existing output directory', false)
|
|
374
|
+
.option('--json', 'output JSON', false)
|
|
375
|
+
.action((name, directory, opts) => action(async () => {
|
|
376
|
+
print(initRunCloudDemo(name, directory, opts), opts);
|
|
377
|
+
}));
|
|
378
|
+
demo
|
|
379
|
+
.command('run')
|
|
380
|
+
.description('Run a bundled demo without scaffolding a project')
|
|
381
|
+
.argument('[name]', 'demo to run', 'eight-device-mosaic')
|
|
382
|
+
.option('--duration <seconds>', 'duration for live-camera-relay')
|
|
383
|
+
.option('--open', 'open the demo viewer or live sessions in the browser', false)
|
|
384
|
+
.action((name, opts) => action(async () => {
|
|
385
|
+
await runCloudDemo(name, opts);
|
|
386
|
+
}));
|
|
387
|
+
program
|
|
388
|
+
.command('account')
|
|
389
|
+
.description('Show run.cloud account and organization access')
|
|
390
|
+
.option('--json', 'output JSON', false)
|
|
391
|
+
.action((opts) => action(async () => {
|
|
392
|
+
print(await client().get('/run-cloud/account'), opts);
|
|
393
|
+
}));
|
|
394
|
+
const ios = registerSimulatorCommands(program, 'ios');
|
|
395
|
+
registerSimulatorCommands(program, 'android');
|
|
396
|
+
ios
|
|
397
|
+
.command('tunnel')
|
|
398
|
+
.description('Connect a local service to a remote iOS simulator session')
|
|
399
|
+
.argument('<id>')
|
|
400
|
+
.requiredOption('--local-port <port>', 'local service port, for example Metro on 8081')
|
|
401
|
+
.option('--service <name>', 'service name shown in status', 'metro')
|
|
402
|
+
.option('--local-host <host>', 'loopback service host', '127.0.0.1')
|
|
403
|
+
.option('--sidecar-url <url>', 'Newly sidecar URL', 'http://127.0.0.1:4500')
|
|
404
|
+
.option('--json', 'output JSON', false)
|
|
405
|
+
.action((id, opts) => action(async () => {
|
|
406
|
+
const localPort = Number(opts.localPort);
|
|
407
|
+
if (!Number.isInteger(localPort) || localPort < 1 || localPort > 65535)
|
|
408
|
+
throw new Error('--local-port must be a valid TCP port');
|
|
409
|
+
const allocation = await client().post(`/sessions/${encodeURIComponent(id)}/tunnel`, {
|
|
410
|
+
localService: opts.service, localHost: opts.localHost, localPort,
|
|
411
|
+
});
|
|
412
|
+
if (!allocation.tunnel)
|
|
413
|
+
throw new Error('Control plane did not return tunnel credentials');
|
|
414
|
+
const sidecarResponse = await fetch(`${opts.sidecarUrl.replace(/\/$/, '')}/run-cloud/tunnel`, {
|
|
415
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(allocation.tunnel),
|
|
416
|
+
});
|
|
417
|
+
const status = await sidecarResponse.json();
|
|
418
|
+
if (!sidecarResponse.ok)
|
|
419
|
+
throw new Error(`Local tunnel failed: ${status.error ?? sidecarResponse.statusText}`);
|
|
420
|
+
print(status, opts);
|
|
421
|
+
}));
|
|
422
|
+
ios
|
|
423
|
+
.command('tunnel-status')
|
|
424
|
+
.description('Show the local tunnel sidecar status')
|
|
425
|
+
.option('--sidecar-url <url>', 'Newly sidecar URL', 'http://127.0.0.1:4500')
|
|
426
|
+
.option('--json', 'output JSON', false)
|
|
427
|
+
.action((opts) => action(async () => {
|
|
428
|
+
const response = await fetch(`${opts.sidecarUrl.replace(/\/$/, '')}/run-cloud/tunnel`);
|
|
429
|
+
print(await response.json(), opts);
|
|
430
|
+
}));
|
|
431
|
+
const asset = program
|
|
432
|
+
.command('asset')
|
|
433
|
+
.alias('assets')
|
|
434
|
+
.description('Upload, list, inspect, and delete run.cloud assets');
|
|
435
|
+
asset
|
|
436
|
+
.command('push')
|
|
437
|
+
.description('Upload an app asset for simulator installation')
|
|
438
|
+
.argument('<file>')
|
|
439
|
+
.option('-n, --name <name>', 'asset name')
|
|
440
|
+
.option('--json', 'output JSON', false)
|
|
441
|
+
.action((file, opts) => action(async () => {
|
|
442
|
+
print(await pushAsset(file, opts), opts);
|
|
443
|
+
}));
|
|
444
|
+
asset
|
|
445
|
+
.command('list')
|
|
446
|
+
.description('List uploaded assets')
|
|
447
|
+
.option('--json', 'output JSON', false)
|
|
448
|
+
.action((opts) => action(async () => {
|
|
449
|
+
print(await client().get('/run-cloud/assets'), opts);
|
|
450
|
+
}));
|
|
451
|
+
asset
|
|
452
|
+
.command('pull')
|
|
453
|
+
.description('Download an asset')
|
|
454
|
+
.argument('<id>')
|
|
455
|
+
.option('-o, --output <path>', 'output path')
|
|
456
|
+
.action((id, opts) => action(async () => {
|
|
457
|
+
const body = await client().getBinary(`/run-cloud/assets/${encodeURIComponent(id)}/download`);
|
|
458
|
+
const output = opts.output ?? id;
|
|
459
|
+
writeFileSync(output, body);
|
|
460
|
+
console.log(output);
|
|
461
|
+
}));
|
|
462
|
+
asset
|
|
463
|
+
.command('delete')
|
|
464
|
+
.description('Delete an uploaded asset')
|
|
465
|
+
.argument('<id>')
|
|
466
|
+
.option('--json', 'output JSON', false)
|
|
467
|
+
.action((id, opts) => action(async () => {
|
|
468
|
+
print(await client().delete(`/run-cloud/assets/${encodeURIComponent(id)}`), opts);
|
|
469
|
+
}));
|
|
470
|
+
const skills = program.command('skills').description('Install run.cloud skills for AI coding agents');
|
|
471
|
+
skills
|
|
472
|
+
.command('install')
|
|
473
|
+
.description('Install the run.cloud router, mobile-session, and sandbox skills')
|
|
474
|
+
.option('--agents <agent>', 'agent to install for: claude, codex, cursor', (v, p) => [...p, v], [])
|
|
475
|
+
.option('--scope <project|global>', 'install scope', 'project')
|
|
476
|
+
.option('--force', 'overwrite existing skill', false)
|
|
477
|
+
.option('--json', 'output JSON', false)
|
|
478
|
+
.action((opts) => action(async () => {
|
|
479
|
+
const scope = opts.scope === 'global' ? 'global' : 'project';
|
|
480
|
+
print(installSkills({ ...opts, scope }), opts);
|
|
481
|
+
}));
|
|
482
|
+
}
|