feega 0.1.4 → 0.1.6
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 +2 -0
- package/bin/feega.mjs +40 -15
- package/launcher.mjs +40 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,3 +7,5 @@ npx feega my-board.feega
|
|
|
7
7
|
Starts feega's local mode on http://localhost:5173 and saves the canvas to `my-board.feega` in the current directory. Needs Node 22+. Each start checks for a newer app and downloads it into `~/.feega/app/<version>/`; offline it uses the newest cached one.
|
|
8
8
|
|
|
9
9
|
Options: `--port <n>`, `--no-open`. `FEEGA_APP_BUNDLE=<path or url>` uses another bundle.
|
|
10
|
+
|
|
11
|
+
AI keys: put them in the environment or in a `.env` in the folder you run `npx feega` from. Kie.ai, OpenRouter, the text LLM, Higgsfield, Wiro and ElevenLabs are supported; see the local mode docs for the variables.
|
package/bin/feega.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
-
import { createWriteStream, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
3
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { Readable } from 'node:stream';
|
|
7
7
|
import { pipeline } from 'node:stream/promises';
|
|
8
8
|
|
|
9
|
-
import { USAGE, appDir, bundleSource, boundUrl, cachedVersions, freePort, isCached, latestAppVersion, markCached, parseArgs, pickVersion } from '../launcher.mjs';
|
|
9
|
+
import { MCP_ENTRY, USAGE, appDir, bundleSource, boundUrl, cachedVersions, freePort, isCached, latestAppVersion, markCached, mcpEnv, parseArgs, pickVersion } from '../launcher.mjs';
|
|
10
10
|
|
|
11
11
|
const MIN_NODE_MAJOR = 22;
|
|
12
12
|
const WEBSTORAGE_NODE_MAJOR = 25;
|
|
@@ -15,6 +15,7 @@ const OPENERS = { darwin: ['open'], win32: ['cmd', '/c', 'start', ''] };
|
|
|
15
15
|
const RELEASES_API = 'https://api.github.com/repos/andreabuttarelli/feega/releases?per_page=50';
|
|
16
16
|
const CHECK_TIMEOUT_MS = 2000;
|
|
17
17
|
const LOCAL_BUNDLE_VERSION = 'local';
|
|
18
|
+
const TO_STDERR = ['ignore', 2, 2];
|
|
18
19
|
|
|
19
20
|
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
20
21
|
|
|
@@ -51,20 +52,20 @@ async function chooseVersion(home) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
async function install(dir, version) {
|
|
54
|
-
console.
|
|
55
|
+
console.error(`feega: downloading the app ${version} (once per version)…`);
|
|
55
56
|
rmSync(dir, { recursive: true, force: true });
|
|
56
57
|
mkdirSync(dir, { recursive: true });
|
|
57
58
|
|
|
58
59
|
const archive = await download(bundleSource(version, process.env), join(tmpdir(), `feega-app-${version}-${process.pid}.tar.gz`));
|
|
59
|
-
const untar = spawnSync('tar', ['-xzf', archive, '-C', dir], { stdio:
|
|
60
|
+
const untar = spawnSync('tar', ['-xzf', archive, '-C', dir], { stdio: TO_STDERR });
|
|
60
61
|
if (untar.status !== 0) {
|
|
61
62
|
throw new Error('could not extract the app bundle');
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
console.
|
|
65
|
+
console.error('feega: installing its dependencies for this machine…');
|
|
65
66
|
const deps = spawnSync('npm', ['ci', '--omit=dev', '--no-audit', '--no-fund', '--loglevel=error'], {
|
|
66
67
|
cwd: dir,
|
|
67
|
-
stdio:
|
|
68
|
+
stdio: TO_STDERR,
|
|
68
69
|
shell: process.platform === 'win32'
|
|
69
70
|
});
|
|
70
71
|
if (deps.status !== 0) {
|
|
@@ -83,6 +84,32 @@ async function openWhenReady(url) {
|
|
|
83
84
|
spawn(cmd, [...args, url], { stdio: 'ignore', detached: true }).on('error', () => {}).unref();
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
async function readyApp() {
|
|
88
|
+
const { version, action } = await chooseVersion(homedir());
|
|
89
|
+
const dir = appDir(homedir(), version);
|
|
90
|
+
if (action === 'download') {
|
|
91
|
+
await install(dir, version);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return dir;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function forward(child) {
|
|
98
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
99
|
+
process.on(signal, () => child.kill(signal));
|
|
100
|
+
}
|
|
101
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function serveMcp(dir, port) {
|
|
105
|
+
const entry = join(dir, MCP_ENTRY);
|
|
106
|
+
if (!existsSync(entry)) {
|
|
107
|
+
throw new Error('this app version has no MCP server: run `npx feega@latest mcp`');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
forward(spawn(process.execPath, [entry], { stdio: 'inherit', env: mcpEnv(port, process.env) }));
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
async function main() {
|
|
87
114
|
if (nodeMajor < MIN_NODE_MAJOR) {
|
|
88
115
|
throw new Error(`Node ${MIN_NODE_MAJOR}+ is required (found ${process.versions.node})`);
|
|
@@ -99,13 +126,14 @@ async function main() {
|
|
|
99
126
|
return;
|
|
100
127
|
}
|
|
101
128
|
|
|
129
|
+
const dir = await readyApp();
|
|
130
|
+
if (args.command === 'mcp') {
|
|
131
|
+
serveMcp(dir, args.port);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
102
135
|
const { file, browser } = args;
|
|
103
136
|
const port = await freePort(args.port);
|
|
104
|
-
const { version, action } = await chooseVersion(homedir());
|
|
105
|
-
const dir = appDir(homedir(), version);
|
|
106
|
-
if (action === 'download') {
|
|
107
|
-
await install(dir, version);
|
|
108
|
-
}
|
|
109
137
|
|
|
110
138
|
const url = boundUrl(port);
|
|
111
139
|
console.log(`feega: saving to ${file}`);
|
|
@@ -114,10 +142,7 @@ async function main() {
|
|
|
114
142
|
const nodeOptions = nodeMajor >= WEBSTORAGE_NODE_MAJOR ? '--no-experimental-webstorage' : '';
|
|
115
143
|
const env = { ...process.env, NODE_OPTIONS: [process.env.NODE_OPTIONS, nodeOptions].filter(Boolean).join(' ') };
|
|
116
144
|
const server = spawn(process.execPath, [realpathSync(join(dir, 'scripts/local.mjs')), file, `--port=${port}`], { stdio: 'inherit', env });
|
|
117
|
-
|
|
118
|
-
process.on(signal, () => server.kill(signal));
|
|
119
|
-
}
|
|
120
|
-
server.on('exit', (code) => process.exit(code ?? 0));
|
|
145
|
+
forward(server);
|
|
121
146
|
|
|
122
147
|
if (browser === 'open') {
|
|
123
148
|
await openWhenReady(url);
|
package/launcher.mjs
CHANGED
|
@@ -5,7 +5,10 @@ import { join, resolve } from 'node:path';
|
|
|
5
5
|
const DEFAULT_FILE = 'canvas.feega';
|
|
6
6
|
const DEFAULT_PORT = 0;
|
|
7
7
|
export const LOOPBACK = '127.0.0.1';
|
|
8
|
-
export const
|
|
8
|
+
export const MCP_PORT = 5173;
|
|
9
|
+
export const MCP_ENTRY = 'mcp.mjs';
|
|
10
|
+
export const USAGE = `usage: feega [file.feega] [--port <n>] [--no-open] [--help] [--version]
|
|
11
|
+
feega mcp [--port <n>] MCP server (stdio) for the local feega on that port (default ${MCP_PORT})`;
|
|
9
12
|
const CACHED_MARKER = '.complete';
|
|
10
13
|
const APP_TAG = 'app-v';
|
|
11
14
|
const RELEASES = 'https://github.com/andreabuttarelli/feega/releases/download';
|
|
@@ -20,8 +23,41 @@ const portOf = (raw) => {
|
|
|
20
23
|
return port;
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
const portFlag = (arg, rest) => {
|
|
27
|
+
if (arg === '--port') {
|
|
28
|
+
return portOf(rest.shift());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (arg.startsWith('--port=')) {
|
|
32
|
+
return portOf(arg.slice('--port='.length));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function parseMcp(rest) {
|
|
39
|
+
let port = MCP_PORT;
|
|
40
|
+
while (rest.length) {
|
|
41
|
+
const arg = rest.shift();
|
|
42
|
+
const flagged = portFlag(arg, rest);
|
|
43
|
+
if (flagged === null) {
|
|
44
|
+
throw new Error(`unknown option: ${arg}\n${USAGE}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
port = flagged;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { command: 'mcp', port };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const mcpEnv = (port, env) => ({ ...env, PUBLIC_APP_URL: boundUrl(port) });
|
|
54
|
+
|
|
23
55
|
export function parseArgs(argv, cwd) {
|
|
24
56
|
const rest = [...argv];
|
|
57
|
+
if (rest[0] === 'mcp') {
|
|
58
|
+
return parseMcp(rest.slice(1));
|
|
59
|
+
}
|
|
60
|
+
|
|
25
61
|
const files = [];
|
|
26
62
|
let port = DEFAULT_PORT;
|
|
27
63
|
let browser = 'open';
|
|
@@ -41,13 +77,9 @@ export function parseArgs(argv, cwd) {
|
|
|
41
77
|
continue;
|
|
42
78
|
}
|
|
43
79
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (arg.startsWith('--port=')) {
|
|
50
|
-
port = portOf(arg.slice('--port='.length));
|
|
80
|
+
const flagged = portFlag(arg, rest);
|
|
81
|
+
if (flagged !== null) {
|
|
82
|
+
port = flagged;
|
|
51
83
|
continue;
|
|
52
84
|
}
|
|
53
85
|
|