droidsembly 0.0.0-stage → 0.4.0-beta.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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.it.md +184 -0
  3. package/README.md +184 -3
  4. package/bin/cubefarm.js +297 -0
  5. package/bin/distribution.d.ts +3 -0
  6. package/bin/distribution.js +48 -0
  7. package/dist/THIRD_PARTY_LICENSES.txt +2542 -0
  8. package/dist/assets/AccessibilitySettings-DSmFk9JC.js +30 -0
  9. package/dist/assets/AccessibilitySettings-HrzBDUTN.css +1 -0
  10. package/dist/assets/AppViewer-alNn4TBB.js +5 -0
  11. package/dist/assets/Balloon-UTs7npGo.js +1 -0
  12. package/dist/assets/Batched-BOgjreKO.js +45 -0
  13. package/dist/assets/Birds-BA2HM-AD.js +1 -0
  14. package/dist/assets/Blimp-CTykuz6B.js +1 -0
  15. package/dist/assets/BufferGeometryUtils-BsmWvcA2.js +1 -0
  16. package/dist/assets/Duck-DH63jUrP.js +1 -0
  17. package/dist/assets/Effects-CNyhhFkE.js +65 -0
  18. package/dist/assets/Fireworks-CJfDJOeb.js +33 -0
  19. package/dist/assets/Helicopter-CW_SP7xP.js +1 -0
  20. package/dist/assets/Hurricane-9WqZGyZg.js +33 -0
  21. package/dist/assets/Kaiju-SpSi_jBg.js +1 -0
  22. package/dist/assets/Meteors-DvjhLfbF.js +1 -0
  23. package/dist/assets/MissionConsole-BEP5CdC8.js +1 -0
  24. package/dist/assets/Office-BBnd6tyn.js +563 -0
  25. package/dist/assets/Office-DMOP02gg.css +1 -0
  26. package/dist/assets/PhotoPanel-DehErnsv.js +1 -0
  27. package/dist/assets/PhotoPanel-v4ANHBsO.css +1 -0
  28. package/dist/assets/PhotoScene-Ce-dzToM.js +115 -0
  29. package/dist/assets/Plane-0nfvzL_c.js +1 -0
  30. package/dist/assets/Pocket-CBoyL0ra.js +2 -0
  31. package/dist/assets/Rainbow-CdcWsmW8.js +28 -0
  32. package/dist/assets/Roof-mkMKniSZ.js +34 -0
  33. package/dist/assets/Shell-BmnZn4UL.js +13 -0
  34. package/dist/assets/Skywriting-06dZPAfg.js +1 -0
  35. package/dist/assets/TerminalView-BrP-ENHg.css +1 -0
  36. package/dist/assets/TerminalView-wlhSixSH.js +17 -0
  37. package/dist/assets/ToyWorld-Cuu9aW-K.js +1 -0
  38. package/dist/assets/Ufo-iHaoe5t1.js +1 -0
  39. package/dist/assets/Whale-DZg88zIX.js +1 -0
  40. package/dist/assets/all-CYErCTCA.js +11 -0
  41. package/dist/assets/billboardLayout-DSKGXPIM.js +1 -0
  42. package/dist/assets/bloomMarks-DariC6lY.js +4231 -0
  43. package/dist/assets/cheerRules-CZGSrxOB.js +1 -0
  44. package/dist/assets/cityLayout-7ckfe3wf.js +1 -0
  45. package/dist/assets/director-CrzFgnoP.js +1 -0
  46. package/dist/assets/dogState-DiOvoobI.js +1 -0
  47. package/dist/assets/doors-BHKj8tkO.js +1 -0
  48. package/dist/assets/gallery-ColCPQQE.js +1 -0
  49. package/dist/assets/gate-BplJKcLG.js +2 -0
  50. package/dist/assets/index-B6qvKFYN.js +10 -0
  51. package/dist/assets/index-COpmc42S.css +1 -0
  52. package/dist/assets/instantReplay-DGiLgvkQ.js +1 -0
  53. package/dist/assets/interact-0qw7BGhL.js +1 -0
  54. package/dist/assets/jsx-runtime-BFUkm7bt.js +1 -0
  55. package/dist/assets/jukeboxSongs-CjbAzAeb.js +1 -0
  56. package/dist/assets/kit-C6ByUQ-Y.js +2 -0
  57. package/dist/assets/mic-B316hcCe.js +1 -0
  58. package/dist/assets/news-CrPol-gz.js +1 -0
  59. package/dist/assets/paint.worker-D95XJ3xX.js +1 -0
  60. package/dist/assets/photoMode-CYxQfgUF.js +1 -0
  61. package/dist/assets/pipeline-BOTOZ69L.js +1215 -0
  62. package/dist/assets/rapier-CXAOW8OC.js +1 -0
  63. package/dist/assets/rig-wAyl3G1G.js +1 -0
  64. package/dist/assets/speechLanguage-BtfdhZNI.js +1 -0
  65. package/dist/assets/store-FA83kfnc.js +5 -0
  66. package/dist/assets/useDayTime-0XsqM14Q.js +1 -0
  67. package/dist/assets/useGraphics-Cif18p7Q.js +1 -0
  68. package/dist/assets/voiceMessages-DtNmUazr.js +1 -0
  69. package/dist/assets/weatherState-ExG1doW-.js +1 -0
  70. package/dist/icons/Sora-OFL.txt +93 -0
  71. package/dist/icons/apple-touch-icon.png +0 -0
  72. package/dist/icons/badge-96.png +0 -0
  73. package/dist/icons/badge.svg +1 -0
  74. package/dist/icons/droid-antenna.svg +29 -0
  75. package/dist/icons/droid-eyes.svg +29 -0
  76. package/dist/icons/droid-shell.svg +29 -0
  77. package/dist/icons/droid-spark.svg +29 -0
  78. package/dist/icons/droid-wire.svg +29 -0
  79. package/dist/icons/favicon-16.png +0 -0
  80. package/dist/icons/favicon-32.png +0 -0
  81. package/dist/icons/favicon.svg +1 -0
  82. package/dist/icons/icon-192.png +0 -0
  83. package/dist/icons/icon-512.png +0 -0
  84. package/dist/icons/icon-maskable-512.png +0 -0
  85. package/dist/icons/icon.svg +29 -0
  86. package/dist/icons/logo-dark.svg +29 -0
  87. package/dist/icons/logo-light.svg +29 -0
  88. package/dist/icons/sora-400.ttf +0 -0
  89. package/dist/icons/sora-700.ttf +0 -0
  90. package/dist/index.html +26 -0
  91. package/dist/manifest.webmanifest +19 -0
  92. package/dist/third-party-licenses.json +1106 -0
  93. package/dist-server/browser-init.js +24 -0
  94. package/dist-server/index.js +20049 -0
  95. package/dist-server/index.js.map +7 -0
  96. package/dist-server/ptyHost.js +234 -0
  97. package/dist-server/ptyHost.js.map +7 -0
  98. package/package.json +98 -6
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+ // Public command: droidsembly. Keep this entry path compatible with existing checkout launchers.
3
+ import { spawn, spawnSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import { createRequire } from 'node:module';
6
+ import net from 'node:net';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { fileURLToPath, pathToFileURL } from 'node:url';
10
+ import { parseArgs } from 'node:util';
11
+ import { isNewer, updateInfo } from './distribution.js';
12
+
13
+ const MIN_NODE = 22;
14
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
16
+ const update = updateInfo(pkg.name, pkg.version);
17
+
18
+ const HELP = `
19
+ Droidsembly ${pkg.version}: a cartoon 3D office where a team of AI coding agents works through your GitHub issues.
20
+
21
+ Usage
22
+ ${update.command} start the office and open it in your browser
23
+ ${update.command} login sign in to Claude Code, the built-in coding agent
24
+ ${update.command} doctor check that this machine is ready
25
+
26
+ Options
27
+ --port <n> port for the office (default 4317)
28
+ --host <address>
29
+ address to listen on (default 127.0.0.1, this machine only). 0.0.0.0 opens the office to your
30
+ network with no sign-in: anyone who reaches it can run your agents. Only on a network you trust
31
+ --demo fake GitHub and fake agents: look around without spending any usage
32
+ --floors <n> --agents <n>
33
+ with --demo: a bigger company, n floors with n people each (up to 20 and 15)
34
+ --no-open don't open the browser
35
+ -v, --version
36
+ -h, --help
37
+
38
+ The office keeps its state and workspaces in ~/.cubefarm (set SWARM_HOME to use another folder).
39
+ `;
40
+
41
+ const color = process.stdout.isTTY && !process.env.NO_COLOR;
42
+ const paint = (open, close) => (s) => (color ? `\x1b[${open}m${s}\x1b[${close}m` : s);
43
+ const green = paint(32, 39);
44
+ const red = paint(31, 39);
45
+ const dim = paint(2, 22);
46
+ const fail = (msg) => {
47
+ console.error(`\n ${red('✗')} ${msg}\n`);
48
+ process.exit(1);
49
+ };
50
+
51
+ if (Number(process.versions.node.split('.')[0]) < MIN_NODE) fail(`Droidsembly needs Node.js ${MIN_NODE} or newer (this is ${process.version}). Get it from https://nodejs.org`);
52
+
53
+ let args;
54
+ try {
55
+ args = parseArgs({
56
+ allowPositionals: true,
57
+ options: {
58
+ port: { type: 'string', short: 'p' },
59
+ host: { type: 'string' },
60
+ bind: { type: 'string' },
61
+ demo: { type: 'boolean' },
62
+ floors: { type: 'string' },
63
+ agents: { type: 'string' },
64
+ 'no-open': { type: 'boolean' },
65
+ version: { type: 'boolean', short: 'v' },
66
+ help: { type: 'boolean', short: 'h' },
67
+ },
68
+ });
69
+ } catch (err) {
70
+ fail(`${err.message}\n Run ${update.command} --help for the options.`);
71
+ }
72
+ const { values, positionals } = args;
73
+ const command = positionals[0] ?? 'start';
74
+
75
+ if (values.help) {
76
+ console.log(HELP);
77
+ process.exit(0);
78
+ }
79
+ if (values.version) {
80
+ console.log(pkg.version);
81
+ process.exit(0);
82
+ }
83
+
84
+ // ---------- Claude Code ----------
85
+
86
+ // The Agent SDK ships Claude Code as a per-platform package. The office runs its agents on it, so logging in with it
87
+ // is logging the agents in. Same lookup as the SDK's own.
88
+ function claudeBinary() {
89
+ let sdk;
90
+ try {
91
+ sdk = createRequire(import.meta.url).resolve('@anthropic-ai/claude-agent-sdk');
92
+ } catch {
93
+ return null;
94
+ }
95
+ const { platform, arch } = process;
96
+ const musl = platform === 'linux' && !process.report?.getReport?.()?.header?.glibcVersionRuntime;
97
+ const targets = platform === 'linux' ? (musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`]) : [`${platform}-${arch}`];
98
+ const fromSdk = createRequire(sdk);
99
+ for (const target of targets) {
100
+ try {
101
+ return fromSdk.resolve(`@anthropic-ai/claude-agent-sdk-${target}/claude${platform === 'win32' ? '.exe' : ''}`);
102
+ } catch {
103
+ // not installed for this target
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ // Like the agents: no API keys or settings inherited from a Claude Code session this was started from.
110
+ function claudeEnv() {
111
+ const env = {};
112
+ for (const [k, v] of Object.entries(process.env)) if (!/^(ANTHROPIC_|CLAUDE)/i.test(k) || k === 'CLAUDE_CONFIG_DIR') env[k] = v;
113
+ return env;
114
+ }
115
+
116
+ // ---------- checks ----------
117
+
118
+ const sh = (cmd, cmdArgs, env) => spawnSync(cmd, cmdArgs, { encoding: 'utf8', windowsHide: true, timeout: 30_000, env });
119
+
120
+ /** What the office needs: { name, ok, detail, fix, required }. */
121
+ function checks() {
122
+ const out = [{ name: 'Node.js', ok: true, detail: process.version }];
123
+
124
+ const git = sh('git', ['--version']);
125
+ out.push({ name: 'git', ok: git.status === 0, detail: git.stdout?.trim().replace(/^git version /, ''), fix: 'install git: https://git-scm.com/downloads', required: true });
126
+
127
+ if (sh('gh', ['--version']).status !== 0) {
128
+ out.push({ name: 'GitHub CLI', ok: false, fix: 'install it from https://cli.github.com, then run: gh auth login' });
129
+ } else {
130
+ const ok = sh('gh', ['auth', 'status']).status === 0;
131
+ out.push({ name: 'GitHub CLI', ok, detail: ok ? 'signed in' : 'not signed in', fix: 'run: gh auth login' });
132
+ }
133
+
134
+ const claude = claudeBinary();
135
+ if (!claude) {
136
+ out.push({ name: 'Claude Code', ok: false, fix: `if using Claude, reinstall ${pkg.name}: Claude Code for ${process.platform}-${process.arch} is missing` });
137
+ } else {
138
+ let status = null;
139
+ try {
140
+ status = JSON.parse(sh(claude, ['auth', 'status', '--json'], claudeEnv()).stdout);
141
+ } catch {
142
+ // unreadable: treated as signed out
143
+ }
144
+ const ok = status?.loggedIn === true;
145
+ out.push({ name: 'Claude', ok, detail: ok ? `signed in${status.subscriptionType ? ` (${status.subscriptionType})` : ''}` : 'not signed in (only needed for Claude sessions)', fix: `if using Claude, run: ${update.command} login` });
146
+ }
147
+
148
+ // Agents test in a browser through Playwright, which drives Google Chrome by default.
149
+ const chrome = chromePaths().some((p) => fs.existsSync(p));
150
+ out.push({ name: 'Chrome', ok: chrome, detail: chrome ? 'for browser testing' : 'Google Chrome not found', fix: 'install Google Chrome so agents can test in a browser' });
151
+ // The pinned Playwright MCP server the agents' browser runs on (server/browser.ts), installed with cubefarm.
152
+ let mcp = null;
153
+ try {
154
+ mcp = JSON.parse(fs.readFileSync(createRequire(import.meta.url).resolve('@playwright/mcp/package.json'), 'utf8')).version;
155
+ } catch {
156
+ // not installed
157
+ }
158
+ out.push({ name: 'Browser tool', ok: !!mcp, detail: mcp ? `@playwright/mcp ${mcp}` : 'not installed', fix: `reinstall ${pkg.name}: ${update.command}` });
159
+ return out;
160
+ }
161
+
162
+ function chromePaths() {
163
+ const env = process.env;
164
+ if (process.platform === 'win32') {
165
+ return [env.PROGRAMFILES, env['PROGRAMFILES(X86)'], env.LOCALAPPDATA].filter(Boolean).map((d) => path.join(d, 'Google', 'Chrome', 'Application', 'chrome.exe'));
166
+ }
167
+ if (process.platform === 'darwin') return ['/Applications/Google Chrome.app', path.join(os.homedir(), 'Applications', 'Google Chrome.app')];
168
+ return ['/opt/google/chrome/chrome', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'];
169
+ }
170
+
171
+ const printCheck = (c) => console.log(` ${c.ok ? green('✓') : red('✗')} ${c.name.padEnd(12)} ${c.detail ? dim(c.detail) : ''}${c.ok ? '' : ` → ${c.fix}`}`);
172
+
173
+ // ---------- helpers ----------
174
+
175
+ // Where this machine reaches an office listening on `host`: loopback for the any-address, else that address (as
176
+ // server/config.ts's localAddress, which also brackets IPv6 for URLs).
177
+ const localHost = (host) => (['0.0.0.0', '::', ''].includes(host) ? '127.0.0.1' : host);
178
+
179
+ function portOpen(port, host) {
180
+ return new Promise((resolve) => {
181
+ const socket = net.connect({ port, host });
182
+ const done = (open) => {
183
+ socket.destroy();
184
+ resolve(open);
185
+ };
186
+ socket.once('connect', () => done(true));
187
+ socket.once('error', () => done(false));
188
+ socket.setTimeout(1000, () => done(false));
189
+ });
190
+ }
191
+
192
+ async function isOffice(url) {
193
+ try {
194
+ const res = await fetch(`${url}/api/state`, { signal: AbortSignal.timeout(3000) });
195
+ const data = await res.json();
196
+ return Array.isArray(data?.repos) && Array.isArray(data?.agents);
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ function openBrowser(url) {
203
+ const [cmd, cmdArgs, opts] =
204
+ process.platform === 'win32'
205
+ ? ['cmd.exe', ['/d', '/s', '/c', `"start "" "${url}""`], { windowsVerbatimArguments: true }]
206
+ : process.platform === 'darwin'
207
+ ? ['open', [url], {}]
208
+ : ['xdg-open', [url], {}];
209
+ try {
210
+ spawn(cmd, cmdArgs, { ...opts, stdio: 'ignore', detached: true, windowsHide: true })
211
+ .on('error', () => undefined)
212
+ .unref();
213
+ } catch {
214
+ // no browser to open: the URL is printed anyway
215
+ }
216
+ }
217
+
218
+ async function checkForUpdate() {
219
+ // A checkout's `npm start` runs this under scripts/office.mjs, which updates the office from its origin instead.
220
+ if (process.env.CI || process.env.SWARM_LAUNCHER === '1') return;
221
+ try {
222
+ const res = await fetch(update.url, { signal: AbortSignal.timeout(4000) });
223
+ if (!res.ok) return;
224
+ const { version } = await res.json();
225
+ if (typeof version === 'string' && isNewer(version, pkg.version)) {
226
+ console.log(`\n Droidsembly ${version} is out (this is ${pkg.version}). Start it with ${green(update.command)} to update.\n`);
227
+ }
228
+ } catch {
229
+ // offline, or the registry is slow: try again next time
230
+ }
231
+ }
232
+
233
+ // ---------- commands ----------
234
+
235
+ if (command === 'doctor') {
236
+ console.log(`\n Droidsembly ${pkg.version}\n`);
237
+ const results = checks();
238
+ results.forEach(printCheck);
239
+ const ok = results.every((c) => c.ok);
240
+ console.log(ok ? `\n All set. Start the office with: ${update.command}\n` : '\n Claude checks apply only to Claude sessions; OpenCode and Codex use their own setup.\n');
241
+ process.exit(ok ? 0 : 1);
242
+ }
243
+
244
+ if (command === 'login') {
245
+ const claude = claudeBinary();
246
+ if (!claude) fail(`Claude Code for ${process.platform}-${process.arch} is missing. Reinstall ${pkg.name}.`);
247
+ console.log('\n Signing in to Claude. Your agents use this login and your subscription.\n');
248
+ const res = spawnSync(claude, ['auth', 'login'], { stdio: 'inherit', env: claudeEnv() });
249
+ process.exit(res.status ?? 1);
250
+ }
251
+
252
+ if (command !== 'start') fail(`Unknown command "${command}". Run ${update.command} --help for the options.`);
253
+
254
+ const port = Number(values.port ?? process.env.SWARM_PORT ?? 4317);
255
+ if (!Number.isInteger(port) || port < 0 || port > 65535) fail(`"${values.port ?? process.env.SWARM_PORT}" isn't a port number.`);
256
+ const host = (values.host ?? values.bind ?? process.env.SWARM_HOST ?? '').trim().replace(/^\[|\]$/g, '') || '127.0.0.1';
257
+ if (/[\s/]/.test(host)) fail(`"${host}" isn't an address to listen on. Try 0.0.0.0 (every network) or one of this machine's IPs.`);
258
+ const local = localHost(host);
259
+ const hostUrl = local.includes(':') ? `[${local}]` : local;
260
+ const url = `http://${local === '127.0.0.1' ? 'localhost' : hostUrl}:${port}`;
261
+ const demo = values.demo || process.env.SWARM_DEMO === '1' || process.env.SWARM_DEMO === 'true';
262
+
263
+ // A second office on the same state would resume every agent's session a second time.
264
+ if (port !== 0 && (await portOpen(port, local))) {
265
+ if (!(await isOffice(`http://${hostUrl}:${port}`))) fail(`Something else is using port ${port}. Start Droidsembly on another one: ${update.command} --port 4400`);
266
+ console.log(`\n Droidsembly is already running: ${url}\n`);
267
+ if (!values['no-open']) openBrowser(url);
268
+ process.exit(0);
269
+ }
270
+
271
+ if (!demo) {
272
+ const problems = checks().filter((c) => !c.ok);
273
+ if (problems.length) {
274
+ console.log('');
275
+ problems.forEach(printCheck);
276
+ if (problems.some((c) => c.required)) fail('Droidsembly can’t run without these.');
277
+ console.log(dim(`\n Starting anyway. Check your selected provider setup, then run: ${update.command} doctor`));
278
+ }
279
+ }
280
+
281
+ const entry = path.join(root, 'dist-server', 'index.js');
282
+ if (!fs.existsSync(entry)) fail(`${path.relative(process.cwd(), entry) || entry} is missing. In a checkout of the repo, run npm run build first.`);
283
+
284
+ process.env.SWARM_PORT = String(port);
285
+ process.env.SWARM_HOST = host;
286
+ if (demo) process.env.SWARM_DEMO = '1';
287
+ if (values.floors) process.env.SWARM_DEMO_FLOORS = values.floors;
288
+ if (values.agents) process.env.SWARM_DEMO_AGENTS = values.agents;
289
+ process.setSourceMapsEnabled(true);
290
+ // The server loads its state, then listens.
291
+ await import(pathToFileURL(entry).href);
292
+
293
+ if (!values['no-open'] && port !== 0) {
294
+ for (let i = 0; i < 100 && !(await portOpen(port, local)); i++) await new Promise((r) => setTimeout(r, 200));
295
+ openBrowser(url);
296
+ }
297
+ void checkForUpdate();
@@ -0,0 +1,3 @@
1
+ export function isNewer(candidate: string, current: string): boolean;
2
+ export function updateInfo(name: string, version: string): { channel: string; command: string; url: string };
3
+ export function releaseInfo(version: string, ref: string): { tag: string; channel: string };
@@ -0,0 +1,48 @@
1
+ // Shared by the CLI and release validation; no registry access or application startup.
2
+ function parseVersion(version) {
3
+ if (typeof version !== 'string') return null;
4
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
5
+ if (!match) return null;
6
+ const pre = match[4]?.split('.') ?? [];
7
+ if (pre.some((part) => /^\d+$/.test(part) && part.length > 1 && part[0] === '0')) return null;
8
+ return { core: match.slice(1, 4).map(BigInt), pre };
9
+ }
10
+
11
+ export function isNewer(candidate, current) {
12
+ const a = parseVersion(candidate), b = parseVersion(current);
13
+ if (!a || !b) return false;
14
+ for (let i = 0; i < 3; i++) {
15
+ if (a.core[i] !== b.core[i]) return a.core[i] > b.core[i];
16
+ }
17
+ if (!a.pre.length || !b.pre.length) return !a.pre.length && !!b.pre.length;
18
+ for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
19
+ const x = a.pre[i], y = b.pre[i];
20
+ if (x === undefined || y === undefined) return x !== undefined;
21
+ if (x === y) continue;
22
+ const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y);
23
+ if (xn && yn) return BigInt(x) > BigInt(y);
24
+ if (xn !== yn) return !xn;
25
+ return x > y;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ export function updateInfo(name, version) {
31
+ const parsed = parseVersion(version);
32
+ if (!parsed) throw new Error(`Invalid package version: ${version}`);
33
+ const channel = parsed.pre.length ? 'next' : 'latest';
34
+ return {
35
+ channel,
36
+ command: `npx ${name}@${channel}`,
37
+ url: `https://registry.npmjs.org/${encodeURIComponent(name)}/${channel}`,
38
+ };
39
+ }
40
+
41
+ export function releaseInfo(version, ref) {
42
+ if (!parseVersion(version) || !/^\d+\.\d+\.\d+(?:-beta\.(?:0|[1-9]\d*))?$/.test(version)) {
43
+ throw new Error('Only stable versions and numbered beta prereleases can be released');
44
+ }
45
+ const tag = `droidsembly-v${version}`;
46
+ if (ref !== tag) throw new Error(`Release tag must be ${tag}`);
47
+ return { tag, channel: updateInfo('droidsembly', version).channel };
48
+ }