relay-studio 1.0.0

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 (3) hide show
  1. package/bin/relay-studio.js +106 -0
  2. package/lib.js +40 -0
  3. package/package.json +28 -0
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+ const { pipeline } = require('stream/promises');
7
+ const { Readable } = require('stream');
8
+ const extract = require('extract-zip');
9
+ const { resolveTarget, assetName, assetUrl, cacheDir } = require('../lib');
10
+ const { version } = require('../package.json');
11
+
12
+ async function download(url, dest) {
13
+ const res = await fetch(url, { redirect: 'follow' });
14
+ if (!res.ok) {
15
+ throw new Error(`Download failed (${res.status} ${res.statusText})\n ${url}`);
16
+ }
17
+ process.stdout.write(`Downloading Relay Studio ${version}...\n`);
18
+ await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(dest));
19
+ }
20
+
21
+ // Returns the path to the executable/app/AppImage to launch, installing if needed.
22
+ async function ensureInstalled() {
23
+ const t = resolveTarget(process.platform, process.arch);
24
+ const dir = cacheDir(version);
25
+ const marker = path.join(dir, '.installed');
26
+
27
+ if (!fs.existsSync(marker)) {
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ try {
30
+ const url = assetUrl(version, t);
31
+
32
+ if (t.ext === 'AppImage') {
33
+ const appImage = path.join(dir, 'Relay-Studio.AppImage');
34
+ await download(url, appImage);
35
+ fs.chmodSync(appImage, 0o755);
36
+ } else {
37
+ const zip = path.join(dir, assetName(version, t));
38
+ await download(url, zip);
39
+ await extract(zip, { dir });
40
+ fs.rmSync(zip, { force: true });
41
+ }
42
+ fs.writeFileSync(marker, new Date().toISOString());
43
+ } catch (err) {
44
+ fs.rmSync(dir, { recursive: true, force: true });
45
+ throw err;
46
+ }
47
+ }
48
+ return locateLaunchTarget(dir, t);
49
+ }
50
+
51
+ function locateLaunchTarget(dir, t) {
52
+ if (t.os === 'mac') {
53
+ const app = fs.readdirSync(dir).find((f) => f.endsWith('.app'));
54
+ if (!app) throw new Error(`No .app bundle found in ${dir}`);
55
+ return { kind: 'mac', appPath: path.join(dir, app) };
56
+ }
57
+ if (t.os === 'win') {
58
+ const exe = findFile(dir, (f) => f.toLowerCase().endsWith('.exe'));
59
+ if (!exe) throw new Error(`No .exe found in ${dir}`);
60
+ return { kind: 'win', exePath: exe };
61
+ }
62
+ const appImage = path.join(dir, 'Relay-Studio.AppImage');
63
+ if (!fs.existsSync(appImage)) throw new Error(`AppImage not found in ${dir}`);
64
+ return { kind: 'linux', appImage };
65
+ }
66
+
67
+ // Shallow + one-level search (electron-builder win zip may nest in a subfolder).
68
+ function findFile(dir, pred) {
69
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
70
+ const full = path.join(dir, entry.name);
71
+ if (entry.isFile() && pred(entry.name)) return full;
72
+ if (entry.isDirectory()) {
73
+ const nested = fs.readdirSync(full, { withFileTypes: true })
74
+ .find((e) => e.isFile() && pred(e.name));
75
+ if (nested) return path.join(full, nested.name);
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function launch(target) {
82
+ let child;
83
+ if (target.kind === 'mac') {
84
+ child = spawn('open', ['-a', target.appPath], { detached: true, stdio: 'ignore' });
85
+ } else if (target.kind === 'win') {
86
+ child = spawn(target.exePath, [], { detached: true, stdio: 'ignore' });
87
+ } else {
88
+ child = spawn(target.appImage, [], { detached: true, stdio: 'ignore' });
89
+ }
90
+ child.on('error', (err) => {
91
+ process.stderr.write(`\nFailed to launch Relay Studio: ${err.message}\n`);
92
+ process.exitCode = 1;
93
+ });
94
+ child.unref();
95
+ }
96
+
97
+ (async () => {
98
+ try {
99
+ const target = await ensureInstalled();
100
+ launch(target);
101
+ process.stdout.write('Relay Studio launched.\n');
102
+ } catch (err) {
103
+ process.stderr.write(`\nFailed to start Relay Studio: ${err.message}\n`);
104
+ process.exit(1);
105
+ }
106
+ })();
package/lib.js ADDED
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const REPO = 'YoniRaviv/Relay';
6
+
7
+ function resolveTarget(platform, arch) {
8
+ if (platform === 'darwin') {
9
+ if (arch !== 'arm64') {
10
+ throw new Error(
11
+ `Intel Macs are not supported via npm. Download the .dmg from https://github.com/${REPO}/releases/latest`
12
+ );
13
+ }
14
+ return { os: 'mac', arch: 'arm64', ext: 'zip' };
15
+ }
16
+ if (platform === 'win32') {
17
+ return { os: 'win', arch: 'x64', ext: 'zip' };
18
+ }
19
+ if (platform === 'linux') {
20
+ if (arch !== 'x64') {
21
+ throw new Error(`Unsupported platform: ${platform} (${arch})`);
22
+ }
23
+ return { os: 'linux', arch: 'x64', ext: 'AppImage' };
24
+ }
25
+ throw new Error(`Unsupported platform: ${platform} (${arch})`);
26
+ }
27
+
28
+ function assetName(version, t) {
29
+ return `Relay-Studio-${t.os}-${t.arch}-${version}.${t.ext}`;
30
+ }
31
+
32
+ function assetUrl(version, t) {
33
+ return `https://github.com/${REPO}/releases/download/${version}/${assetName(version, t)}`;
34
+ }
35
+
36
+ function cacheDir(version) {
37
+ return path.join(os.homedir(), '.relay-studio', version);
38
+ }
39
+
40
+ module.exports = { resolveTarget, assetName, assetUrl, cacheDir, REPO };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "relay-studio",
3
+ "version": "1.0.0",
4
+ "description": "Installer/launcher for Relay Studio — a visual Kanban build loop for Claude Code & Codex.",
5
+ "bin": {
6
+ "relay-studio": "bin/relay-studio.js"
7
+ },
8
+ "type": "commonjs",
9
+ "files": [
10
+ "bin",
11
+ "lib.js"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "scripts": {
17
+ "test": "node --test"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/YoniRaviv/Relay.git"
22
+ },
23
+ "license": "GPL-3.0-or-later",
24
+ "author": "Yoni Raviv",
25
+ "dependencies": {
26
+ "extract-zip": "^2.0.1"
27
+ }
28
+ }