feega 0.1.27 → 0.1.29
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 +3 -1
- package/bin/feega.mjs +47 -6
- package/launcher.mjs +59 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ npx feega my-board.feega
|
|
|
6
6
|
|
|
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
|
+
Run it inside a project with a `dev` script and it asks once to import the folder: its home page lands on the canvas as an editable frame. `--import` imports without asking, `--no-import` never asks.
|
|
10
|
+
|
|
11
|
+
Options: `--port <n>`, `--no-open`, `--import`, `--no-import`. `FEEGA_APP_BUNDLE=<path or url>` uses another bundle.
|
|
10
12
|
|
|
11
13
|
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.
|
|
12
14
|
|
package/bin/feega.mjs
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
7
|
import { Readable } from 'node:stream';
|
|
7
8
|
import { pipeline } from 'node:stream/promises';
|
|
8
9
|
|
|
9
10
|
import { sendTelemetry, telemetryEnv } from '../telemetry.mjs';
|
|
10
|
-
import { MCP_ENTRY, USAGE, appDir, bundleSource, boundUrl, cachedVersions, freePort, isCached, latestAppVersion, markCached, mcpEnv, parseArgs, pickVersion } from '../launcher.mjs';
|
|
11
|
+
import { ImportStep, MCP_ENTRY, USAGE, appDir, bundleSource, boundUrl, cachedVersions, folderApp, freePort, importAsk, importStep, isCached, latestAppVersion, markCached, mcpEnv, parseArgs, pickVersion, placeOf, readyProbe, served } from '../launcher.mjs';
|
|
11
12
|
|
|
12
13
|
const MIN_NODE_MAJOR = 22;
|
|
13
14
|
const WEBSTORAGE_NODE_MAJOR = 25;
|
|
@@ -76,11 +77,13 @@ async function install(dir, version) {
|
|
|
76
77
|
markCached(dir);
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
async function
|
|
80
|
-
while (!(await fetch(url).then((r) => r.
|
|
80
|
+
async function whenReady(url) {
|
|
81
|
+
while (!(await fetch(url, readyProbe).then((r) => served(r.status), () => false))) {
|
|
81
82
|
await new Promise((r) => setTimeout(r, READY_POLL_MS));
|
|
82
83
|
}
|
|
84
|
+
}
|
|
83
85
|
|
|
86
|
+
function openBrowser(url) {
|
|
84
87
|
const [cmd, ...args] = OPENERS[process.platform] ?? ['xdg-open'];
|
|
85
88
|
spawn(cmd, [...args, url], { stdio: 'ignore', detached: true }).on('error', () => {}).unref();
|
|
86
89
|
}
|
|
@@ -95,6 +98,37 @@ async function readyApp() {
|
|
|
95
98
|
return dir;
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
async function wantsImport(app, file, mode) {
|
|
102
|
+
const step = importStep({ mode, app, fileExists: existsSync(file), tty: Boolean(process.stdin.isTTY && process.stdout.isTTY) });
|
|
103
|
+
if (step !== ImportStep.Ask) {
|
|
104
|
+
return step === ImportStep.Import;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
108
|
+
const answer = (await prompt.question(`feega: import the current folder (${basename(app)}) into the canvas? (Y/n) `)).trim().toLowerCase();
|
|
109
|
+
prompt.close();
|
|
110
|
+
return answer === '' || answer.startsWith('y');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function importFolder(url, folder) {
|
|
114
|
+
const home = await fetch(url, readyProbe);
|
|
115
|
+
const place = placeOf(home.headers.get('location'));
|
|
116
|
+
if (!place) {
|
|
117
|
+
throw new Error('could not find the local canvas');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`feega: importing ${basename(folder)} (starts its dev server, can take a minute)…`);
|
|
121
|
+
const { path, body } = importAsk(place, folder);
|
|
122
|
+
const res = await fetch(new URL(path, url), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const { error, detail } = await res.json().catch(() => ({}));
|
|
125
|
+
throw new Error(`folder import failed: ${detail ?? error ?? res.status}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { pages = [] } = await res.json();
|
|
129
|
+
console.log(`feega: imported ${basename(folder)} (${pages.length} pages)`);
|
|
130
|
+
}
|
|
131
|
+
|
|
98
132
|
function forward(child) {
|
|
99
133
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
100
134
|
process.on(signal, () => child.kill(signal));
|
|
@@ -133,7 +167,9 @@ async function main() {
|
|
|
133
167
|
return;
|
|
134
168
|
}
|
|
135
169
|
|
|
136
|
-
const { file, browser } = args;
|
|
170
|
+
const { file, browser, folderImport } = args;
|
|
171
|
+
const app = folderApp(process.cwd());
|
|
172
|
+
const importing = await wantsImport(app, file, folderImport);
|
|
137
173
|
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
138
174
|
void sendTelemetry({
|
|
139
175
|
home: homedir(),
|
|
@@ -152,8 +188,13 @@ async function main() {
|
|
|
152
188
|
const server = spawn(process.execPath, [realpathSync(join(dir, 'scripts/local.mjs')), file, `--port=${port}`], { stdio: 'inherit', env });
|
|
153
189
|
forward(server);
|
|
154
190
|
|
|
191
|
+
await whenReady(url);
|
|
155
192
|
if (browser === 'open') {
|
|
156
|
-
|
|
193
|
+
openBrowser(url);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (importing) {
|
|
197
|
+
await importFolder(url, app).catch((error) => console.error(`feega: ${error.message}`));
|
|
157
198
|
}
|
|
158
199
|
}
|
|
159
200
|
|
package/launcher.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { createServer } from 'node:net';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
|
|
@@ -7,8 +7,15 @@ const DEFAULT_PORT = 0;
|
|
|
7
7
|
export const LOOPBACK = '127.0.0.1';
|
|
8
8
|
export const MCP_PORT = 5173;
|
|
9
9
|
export const MCP_ENTRY = 'mcp.mjs';
|
|
10
|
-
export const USAGE = `usage: feega [file.feega] [--port <n>] [--no-open] [--help] [--version]
|
|
10
|
+
export const USAGE = `usage: feega [file.feega] [--port <n>] [--no-open] [--import | --no-import] [--help] [--version]
|
|
11
11
|
feega mcp [--port <n>] MCP server (stdio) for the local feega on that port (default ${MCP_PORT})`;
|
|
12
|
+
export const FolderImport = { Ask: 'ask', Yes: 'yes', No: 'no' };
|
|
13
|
+
export const ImportStep = { Ask: 'ask', Import: 'import', Skip: 'skip' };
|
|
14
|
+
|
|
15
|
+
const IMPORT_FLAGS = { '--import': FolderImport.Yes, '--no-import': FolderImport.No };
|
|
16
|
+
const HOME_ROUTE = '/';
|
|
17
|
+
const PLACE = /\/p\/([^/]+)\/c\/([^/?#]+)/;
|
|
18
|
+
|
|
12
19
|
const CACHED_MARKER = '.complete';
|
|
13
20
|
const APP_TAG = 'app-v';
|
|
14
21
|
const RELEASES = 'https://github.com/andreabuttarelli/feega/releases/download';
|
|
@@ -61,6 +68,7 @@ export function parseArgs(argv, cwd) {
|
|
|
61
68
|
const files = [];
|
|
62
69
|
let port = DEFAULT_PORT;
|
|
63
70
|
let browser = 'open';
|
|
71
|
+
let folderImport = FolderImport.Ask;
|
|
64
72
|
|
|
65
73
|
while (rest.length) {
|
|
66
74
|
const arg = rest.shift();
|
|
@@ -77,6 +85,11 @@ export function parseArgs(argv, cwd) {
|
|
|
77
85
|
continue;
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
if (IMPORT_FLAGS[arg]) {
|
|
89
|
+
folderImport = IMPORT_FLAGS[arg];
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
const flagged = portFlag(arg, rest);
|
|
81
94
|
if (flagged !== null) {
|
|
82
95
|
port = flagged;
|
|
@@ -90,7 +103,7 @@ export function parseArgs(argv, cwd) {
|
|
|
90
103
|
files.push(arg);
|
|
91
104
|
}
|
|
92
105
|
|
|
93
|
-
return { command: 'run', file: resolve(cwd, files[0] ?? DEFAULT_FILE), port, browser };
|
|
106
|
+
return { command: 'run', file: resolve(cwd, files[0] ?? DEFAULT_FILE), port, browser, folderImport };
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
export const appDir = (home, version) => join(home, '.feega', 'app', version);
|
|
@@ -153,3 +166,46 @@ export const freePort = (port) =>
|
|
|
153
166
|
probe.close(() => done(bound));
|
|
154
167
|
});
|
|
155
168
|
});
|
|
169
|
+
|
|
170
|
+
export function folderApp(dir) {
|
|
171
|
+
const manifest = join(dir, 'package.json');
|
|
172
|
+
if (!existsSync(manifest)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).scripts?.dev ? dir : null;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const STEPS = {
|
|
184
|
+
[FolderImport.No]: () => ImportStep.Skip,
|
|
185
|
+
[FolderImport.Yes]: () => ImportStep.Import,
|
|
186
|
+
[FolderImport.Ask]: ({ fileExists, tty }) => (!fileExists && tty ? ImportStep.Ask : ImportStep.Skip)
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export function importStep({ mode, app, fileExists, tty }) {
|
|
190
|
+
if (!app) {
|
|
191
|
+
return ImportStep.Skip;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return STEPS[mode]({ fileExists, tty });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function placeOf(location) {
|
|
198
|
+
const found = PLACE.exec(location ?? '');
|
|
199
|
+
return found ? { projectId: found[1], canvasId: found[2] } : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const importAsk = (place, folder) => ({
|
|
203
|
+
path: `/api/v1/projects/${place.projectId}/code-import`,
|
|
204
|
+
body: { canvasId: place.canvasId, folder, routes: [HOME_ROUTE], x: 0, y: 0 }
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const HTTP_ERROR_MIN = 400;
|
|
208
|
+
|
|
209
|
+
export const readyProbe = { redirect: 'manual' };
|
|
210
|
+
|
|
211
|
+
export const served = (status) => status < HTTP_ERROR_MIN;
|