@genfeedai/create 0.1.3 → 0.3.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.
- package/LICENSE +661 -0
- package/README.md +40 -12
- package/dist/index.d.ts +23 -20
- package/dist/index.js +268 -204
- package/dist/index.js.map +1 -1
- package/dist/setup.d.ts +3 -0
- package/dist/setup.js +93 -0
- package/dist/setup.js.map +1 -0
- package/dist/wizard.d.ts +4 -0
- package/dist/wizard.js +61 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +18 -15
package/README.md
CHANGED
|
@@ -1,27 +1,55 @@
|
|
|
1
|
-
# genfeedai
|
|
1
|
+
# @genfeedai/create
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Install a checksummed Genfeed.ai Community release with Docker Compose.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx genfeedai
|
|
8
|
+
npx @genfeedai/create my-genfeed
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
The installer resolves the latest GitHub release, downloads its public
|
|
12
|
+
self-hosted bundle, verifies the SHA-256 checksum and release manifest, then
|
|
13
|
+
starts the exact GHCR image associated with that release. The app is available
|
|
14
|
+
at `http://localhost:3000` by default. The wizard asks for an OpenRouter key
|
|
15
|
+
(hidden input) and a domain you own (blank for localhost). It writes the final
|
|
16
|
+
`.env` with owner-only permissions and generated encryption/session secrets
|
|
17
|
+
before starting Docker.
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
- copies `.env.example` to `.env` when present
|
|
15
|
-
- installs dependencies with `bun install` when Bun is available, otherwise `npm install`
|
|
19
|
+
Install a specific release reproducibly:
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
```bash
|
|
22
|
+
npx @genfeedai/create my-genfeed --release v0.5.0
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Prepare and validate the installation without starting containers:
|
|
18
26
|
|
|
19
27
|
```bash
|
|
20
|
-
npx genfeedai
|
|
21
|
-
cd my-app
|
|
22
|
-
bun run dev
|
|
28
|
+
npx @genfeedai/create my-genfeed --release v0.5.0 --no-start
|
|
23
29
|
```
|
|
24
30
|
|
|
31
|
+
Node.js 24 (`>=24 <25`; other versions exit with an error), Docker Compose v2, and `tar` must be installed. The generated directory
|
|
32
|
+
contains the release manifest, pinned environment, Compose file, and
|
|
33
|
+
installation guide.
|
|
34
|
+
|
|
35
|
+
## First sign-up and first generation
|
|
36
|
+
|
|
37
|
+
Sign-up is enabled by default. Supply a real key from
|
|
38
|
+
[openrouter.ai/keys](https://openrouter.ai/keys) during setup: missing or
|
|
39
|
+
placeholder keys stop installation/startup. Format checks do not prove provider
|
|
40
|
+
access or available provider credit.
|
|
41
|
+
|
|
42
|
+
For automation, pass `--yes` and securely provide `OPENROUTER_API_KEY` in the
|
|
43
|
+
environment. `SELFHOSTED_DOMAIN` and the three `SELFHOSTED_*_PORT` variables
|
|
44
|
+
override the defaults; no secret CLI argument is supported. Non-terminal input
|
|
45
|
+
requires `--yes` and a key, rather than silently copying an unusable template.
|
|
46
|
+
|
|
47
|
+
A domain such as `studio.example.com` derives HTTPS app/API/auth URLs and trusted
|
|
48
|
+
origins. You must configure DNS, TLS and reverse-proxy routing separately; the
|
|
49
|
+
release bundle README describes it. Existing installs upgrading to 0.2.15 must
|
|
50
|
+
configure a key before restarting and remove conflicting localhost auth URLs
|
|
51
|
+
when setting a domain.
|
|
52
|
+
|
|
25
53
|
## License
|
|
26
54
|
|
|
27
55
|
AGPL-3.0
|
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
interface
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
interface CliOptions {
|
|
3
|
+
release?: string;
|
|
4
|
+
start: boolean;
|
|
5
|
+
yes?: boolean;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
interface ReleaseManifest {
|
|
8
|
+
schemaVersion: number;
|
|
9
|
+
releaseTag: string;
|
|
10
|
+
image: string;
|
|
11
|
+
}
|
|
12
|
+
interface ReleaseAssetUrls {
|
|
13
|
+
archive: string;
|
|
14
|
+
checksum: string;
|
|
15
|
+
}
|
|
16
|
+
type FetchImplementation = typeof fetch;
|
|
17
|
+
export declare function normalizeReleaseTag(value: string): string;
|
|
18
|
+
export declare function imageTagFromReleaseTag(releaseTag: string): string;
|
|
19
|
+
export declare function getReleaseAssetUrls(releaseTag: string, downloadBaseUrl?: string): ReleaseAssetUrls;
|
|
20
|
+
export declare function resolveReleaseTag(requestedRelease?: string, fetchImplementation?: FetchImplementation): Promise<string>;
|
|
21
|
+
export declare function readPackageVersion(packageJsonUrl?: URL): string;
|
|
22
|
+
export declare function verifyReleaseChecksum(archivePath: string, checksumPath: string): Promise<void>;
|
|
23
|
+
export declare function parseReleaseManifest(content: string, expectedReleaseTag: string): ReleaseManifest;
|
|
24
|
+
export declare function assertSupportedNodeVersion(nodeVersion?: string): void;
|
|
14
25
|
export declare function openBrowser(url: string): void;
|
|
15
|
-
|
|
16
|
-
* Ask the user whether to use managed MongoDB or provide their own URI,
|
|
17
|
-
* then update the project's .env file accordingly.
|
|
18
|
-
*
|
|
19
|
-
* - Managed path: downloads the mongod binary (cached after first run),
|
|
20
|
-
* sets MONGODB_URI + GENFEED_MANAGED_MONGO=true.
|
|
21
|
-
* - Custom path: prompts for a URI and sets only MONGODB_URI.
|
|
22
|
-
*/
|
|
23
|
-
export declare function setupMongoDB(projectDirectory: string): Promise<void>;
|
|
26
|
+
export declare function installRelease(projectName: string, options: CliOptions, fetchImplementation?: FetchImplementation): Promise<void>;
|
|
24
27
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,240 +1,293 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { access, mkdir, mkdtemp, readFile, rm, writeFile, } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { consola } from 'consola';
|
|
10
|
+
import { configureEnvironment } from './wizard.js';
|
|
11
|
+
const REPOSITORY = 'genfeedai/genfeed.ai';
|
|
12
|
+
const RELEASE_API_URL = `https://api.github.com/repos/${REPOSITORY}/releases/latest`;
|
|
13
|
+
const DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${REPOSITORY}/releases/download`;
|
|
14
|
+
const RELEASE_ARCHIVE_NAME = 'genfeed-selfhosted.tar.gz';
|
|
15
|
+
const RELEASE_CHECKSUM_NAME = `${RELEASE_ARCHIVE_NAME}.sha256`;
|
|
16
|
+
const RELEASE_MANIFEST_NAME = 'release.json';
|
|
17
|
+
const EXPECTED_IMAGE_REPOSITORY = 'ghcr.io/genfeedai/genfeed.ai';
|
|
18
|
+
// Keep in sync with "engines.node" (>=24 <25) in package.json.
|
|
19
|
+
const MIN_NODE_MAJOR = 24;
|
|
20
|
+
const MAX_NODE_MAJOR_EXCLUSIVE = 25;
|
|
21
|
+
function isNonNullObject(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null;
|
|
23
|
+
}
|
|
24
|
+
export function normalizeReleaseTag(value) {
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
const releaseTag = trimmed.startsWith('v') ? trimmed : `v${trimmed}`;
|
|
27
|
+
if (!/^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(releaseTag)) {
|
|
28
|
+
throw new Error(`Invalid release "${value}". Expected vX.Y.Z (for example, v0.5.0).`);
|
|
30
29
|
}
|
|
30
|
+
return releaseTag;
|
|
31
|
+
}
|
|
32
|
+
export function imageTagFromReleaseTag(releaseTag) {
|
|
33
|
+
return normalizeReleaseTag(releaseTag).slice(1);
|
|
34
|
+
}
|
|
35
|
+
export function getReleaseAssetUrls(releaseTag, downloadBaseUrl = process.env.GENFEED_RELEASE_DOWNLOAD_BASE_URL ??
|
|
36
|
+
DEFAULT_DOWNLOAD_BASE_URL) {
|
|
37
|
+
const normalizedTag = normalizeReleaseTag(releaseTag);
|
|
38
|
+
const baseUrl = downloadBaseUrl.replace(/\/$/, '');
|
|
31
39
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
label: 'npm install',
|
|
40
|
+
archive: `${baseUrl}/${normalizedTag}/${RELEASE_ARCHIVE_NAME}`,
|
|
41
|
+
checksum: `${baseUrl}/${normalizedTag}/${RELEASE_CHECKSUM_NAME}`,
|
|
35
42
|
};
|
|
36
43
|
}
|
|
37
|
-
async function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return true;
|
|
44
|
+
export async function resolveReleaseTag(requestedRelease, fetchImplementation = fetch) {
|
|
45
|
+
if (requestedRelease) {
|
|
46
|
+
return normalizeReleaseTag(requestedRelease);
|
|
41
47
|
}
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
const response = await fetchImplementation(RELEASE_API_URL, {
|
|
49
|
+
headers: {
|
|
50
|
+
Accept: 'application/vnd.github+json',
|
|
51
|
+
'User-Agent': '@genfeedai/create',
|
|
52
|
+
},
|
|
53
|
+
signal: AbortSignal.timeout(30_000),
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`Could not resolve the latest Genfeed.ai release (HTTP ${response.status}).`);
|
|
57
|
+
}
|
|
58
|
+
const payload = await response.json();
|
|
59
|
+
if (!isNonNullObject(payload)) {
|
|
60
|
+
throw new Error('GitHub returned invalid latest-release metadata.');
|
|
44
61
|
}
|
|
62
|
+
const tagName = payload.tag_name;
|
|
63
|
+
if (typeof tagName !== 'string') {
|
|
64
|
+
throw new Error('GitHub latest release did not include a release tag.');
|
|
65
|
+
}
|
|
66
|
+
return normalizeReleaseTag(tagName);
|
|
45
67
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
PORT=3001
|
|
51
|
-
CORS_ORIGIN=http://localhost:3000
|
|
52
|
-
API_BASE_URL=http://localhost:3001
|
|
53
|
-
|
|
54
|
-
# ─── Redis (BullMQ job queue) ──────────────────────────────────────
|
|
55
|
-
REDIS_HOST=localhost
|
|
56
|
-
REDIS_PORT=6379
|
|
57
|
-
# REDIS_PASSWORD=
|
|
58
|
-
|
|
59
|
-
# ─── AI Providers (add your keys during onboarding) ────────────────
|
|
60
|
-
# REPLICATE_API_TOKEN=
|
|
61
|
-
# FAL_API_KEY=
|
|
62
|
-
# ELEVENLABS_API_KEY=
|
|
63
|
-
# HF_API_TOKEN=
|
|
64
|
-
|
|
65
|
-
# ─── Ollama (local LLM — optional) ────────────────────────────────
|
|
66
|
-
# OLLAMA_ENABLED=false
|
|
67
|
-
# OLLAMA_BASE_URL=http://localhost:11434
|
|
68
|
-
# OLLAMA_DEFAULT_MODEL=llama3.1
|
|
69
|
-
|
|
70
|
-
# ─── Frontend ──────────────────────────────────────────────────────
|
|
71
|
-
NEXT_PUBLIC_API_URL=http://localhost:3001/api
|
|
72
|
-
# NEXT_PUBLIC_URL=http://localhost:3000
|
|
73
|
-
# NEXT_PUBLIC_TTS_ENABLED=false
|
|
74
|
-
`;
|
|
75
|
-
const ENV_LOCATIONS = ['.env', 'apps/api/.env'];
|
|
76
|
-
async function writeDefaultEnv(projectDirectory) {
|
|
77
|
-
for (const location of ENV_LOCATIONS) {
|
|
78
|
-
const envPath = (0, node_path_1.join)(projectDirectory, location);
|
|
79
|
-
const hasEnv = await fileExists(envPath);
|
|
80
|
-
if (!hasEnv) {
|
|
81
|
-
await (0, promises_1.mkdir)((0, node_path_1.dirname)(envPath), { recursive: true });
|
|
82
|
-
await (0, promises_1.writeFile)(envPath, DEFAULT_ENV, 'utf-8');
|
|
83
|
-
}
|
|
68
|
+
export function readPackageVersion(packageJsonUrl = new URL('../package.json', import.meta.url)) {
|
|
69
|
+
const manifest = JSON.parse(readFileSync(fileURLToPath(packageJsonUrl), 'utf8'));
|
|
70
|
+
if (!isNonNullObject(manifest) || typeof manifest.version !== 'string') {
|
|
71
|
+
throw new Error('Could not read the @genfeedai/create package version.');
|
|
84
72
|
}
|
|
85
|
-
|
|
73
|
+
return manifest.version;
|
|
86
74
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
(
|
|
75
|
+
async function downloadFile(url, destination, fetchImplementation) {
|
|
76
|
+
const response = await fetchImplementation(url, {
|
|
77
|
+
headers: { 'User-Agent': '@genfeedai/create' },
|
|
78
|
+
redirect: 'follow',
|
|
79
|
+
signal: AbortSignal.timeout(120_000),
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`Download failed (HTTP ${response.status}): ${url}`);
|
|
95
83
|
}
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
await writeFile(destination, new Uint8Array(await response.arrayBuffer()));
|
|
85
|
+
}
|
|
86
|
+
export async function verifyReleaseChecksum(archivePath, checksumPath) {
|
|
87
|
+
const checksumContent = await readFile(checksumPath, 'utf8');
|
|
88
|
+
const match = checksumContent.match(/^([a-fA-F0-9]{64})\s+/);
|
|
89
|
+
if (!match) {
|
|
90
|
+
throw new Error('Release checksum file is malformed.');
|
|
98
91
|
}
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
const archive = await readFile(archivePath);
|
|
93
|
+
const actualChecksum = createHash('sha256').update(archive).digest('hex');
|
|
94
|
+
const expectedChecksum = match[1].toLowerCase();
|
|
95
|
+
if (actualChecksum !== expectedChecksum) {
|
|
96
|
+
throw new Error(`Release checksum mismatch: expected ${expectedChecksum}, received ${actualChecksum}.`);
|
|
101
97
|
}
|
|
102
98
|
}
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
108
|
-
if (response.ok || response.status === 404) {
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// Server not ready yet
|
|
114
|
-
}
|
|
115
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
99
|
+
export function parseReleaseManifest(content, expectedReleaseTag) {
|
|
100
|
+
const parsed = JSON.parse(content);
|
|
101
|
+
if (!isNonNullObject(parsed)) {
|
|
102
|
+
throw new Error('Release manifest is not an object.');
|
|
116
103
|
}
|
|
117
|
-
|
|
104
|
+
const normalizedTag = normalizeReleaseTag(expectedReleaseTag);
|
|
105
|
+
const expectedImage = `${EXPECTED_IMAGE_REPOSITORY}:${imageTagFromReleaseTag(normalizedTag)}`;
|
|
106
|
+
const schemaVersion = parsed.schemaVersion;
|
|
107
|
+
const releaseTag = parsed.releaseTag;
|
|
108
|
+
const image = parsed.image;
|
|
109
|
+
if (schemaVersion !== 1 ||
|
|
110
|
+
releaseTag !== normalizedTag ||
|
|
111
|
+
image !== expectedImage) {
|
|
112
|
+
throw new Error(`Release manifest does not match ${normalizedTag} and ${expectedImage}.`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
image,
|
|
116
|
+
releaseTag,
|
|
117
|
+
schemaVersion,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function commandExists(command, args = ['--version']) {
|
|
121
|
+
return spawnSync(command, args, { stdio: 'ignore' }).status === 0;
|
|
118
122
|
}
|
|
119
|
-
async function
|
|
120
|
-
const installCommand = getInstallCommand();
|
|
123
|
+
async function runCommand(command, args, options = {}) {
|
|
121
124
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
122
|
-
const child =
|
|
123
|
-
cwd:
|
|
124
|
-
|
|
125
|
-
...process.env,
|
|
126
|
-
HUSKY: '0',
|
|
127
|
-
},
|
|
128
|
-
stdio: 'inherit',
|
|
125
|
+
const child = spawn(command, args, {
|
|
126
|
+
cwd: options.cwd,
|
|
127
|
+
stdio: options.stdio ?? 'inherit',
|
|
129
128
|
});
|
|
130
|
-
child.
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
child.on('close', (code) => {
|
|
129
|
+
child.once('error', rejectPromise);
|
|
130
|
+
child.once('close', (code, signal) => {
|
|
134
131
|
if (code === 0) {
|
|
135
132
|
resolvePromise();
|
|
136
133
|
return;
|
|
137
134
|
}
|
|
138
|
-
rejectPromise(new Error(`${
|
|
135
|
+
rejectPromise(new Error(`${command} ${args.join(' ')} failed with ${code ?? signal ?? 'an unknown error'}.`));
|
|
139
136
|
});
|
|
140
137
|
});
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
}
|
|
139
|
+
export function assertSupportedNodeVersion(nodeVersion = process.versions.node) {
|
|
140
|
+
const major = Number.parseInt(nodeVersion.split('.')[0] ?? '', 10);
|
|
141
|
+
if (!Number.isInteger(major) ||
|
|
142
|
+
major < MIN_NODE_MAJOR ||
|
|
143
|
+
major >= MAX_NODE_MAJOR_EXCLUSIVE) {
|
|
144
|
+
throw new Error(`Node.js ${MIN_NODE_MAJOR} is required (supported range: >=${MIN_NODE_MAJOR} <${MAX_NODE_MAJOR_EXCLUSIVE}); found ${nodeVersion}. Install Node.js ${MIN_NODE_MAJOR} and run again.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function assertInstallPrerequisites() {
|
|
148
|
+
if (!commandExists('tar')) {
|
|
149
|
+
throw new Error('tar is required to extract the Genfeed.ai release bundle.');
|
|
150
|
+
}
|
|
151
|
+
if (!commandExists('docker', ['compose', 'version'])) {
|
|
152
|
+
throw new Error('Docker Compose is required. Install Docker Desktop or the Docker Compose plugin.');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function waitForApp(fetchImplementation, appUrl, timeoutMs = 180_000) {
|
|
156
|
+
const start = Date.now();
|
|
157
|
+
while (Date.now() - start < timeoutMs) {
|
|
158
|
+
try {
|
|
159
|
+
const response = await fetchImplementation(appUrl, {
|
|
160
|
+
redirect: 'manual',
|
|
161
|
+
signal: AbortSignal.timeout(2_000),
|
|
162
|
+
});
|
|
163
|
+
if (response.ok || (response.status >= 300 && response.status < 400)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
154
166
|
}
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
catch {
|
|
168
|
+
// The container is still starting.
|
|
157
169
|
}
|
|
170
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 2_000));
|
|
158
171
|
}
|
|
172
|
+
return false;
|
|
159
173
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
type: 'text',
|
|
177
|
-
});
|
|
178
|
-
await replaceEnvValue(projectDirectory, 'MONGODB_URI', String(uri));
|
|
179
|
-
consola_1.consola.success('MongoDB URI saved to .env');
|
|
174
|
+
export function openBrowser(url) {
|
|
175
|
+
let command;
|
|
176
|
+
let args = [];
|
|
177
|
+
if (process.platform === 'darwin') {
|
|
178
|
+
command = 'open';
|
|
179
|
+
args = [url];
|
|
180
|
+
}
|
|
181
|
+
else if (process.platform === 'linux') {
|
|
182
|
+
command = 'xdg-open';
|
|
183
|
+
args = [url];
|
|
184
|
+
}
|
|
185
|
+
else if (process.platform === 'win32') {
|
|
186
|
+
command = 'cmd';
|
|
187
|
+
args = ['/c', 'start', '', url];
|
|
188
|
+
}
|
|
189
|
+
if (!command) {
|
|
180
190
|
return;
|
|
181
191
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
await replaceEnvValue(projectDirectory, 'MONGODB_URI', MANAGED_MONGO_URI);
|
|
186
|
-
await replaceEnvValue(projectDirectory, 'GENFEED_MANAGED_MONGO', 'true');
|
|
187
|
-
consola_1.consola.success('Managed MongoDB configured — will auto-start on `bun run dev`');
|
|
188
|
-
}
|
|
189
|
-
async function scaffoldProject(projectName) {
|
|
190
|
-
const projectDirectory = (0, node_path_1.resolve)(process.cwd(), projectName);
|
|
191
|
-
consola_1.consola.start(`Scaffolding ${projectName} from ${TEMPLATE_REPOSITORY}`);
|
|
192
|
-
await (0, giget_1.downloadTemplate)(TEMPLATE_REPOSITORY, {
|
|
193
|
-
dir: projectDirectory,
|
|
194
|
-
force: false,
|
|
195
|
-
});
|
|
196
|
-
consola_1.consola.success(`Downloaded template into ${projectDirectory}`);
|
|
197
|
-
await writeDefaultEnv(projectDirectory);
|
|
198
|
-
const installCommand = await runInstall(projectDirectory);
|
|
199
|
-
consola_1.consola.success(`Dependencies installed with ${installCommand.label}`);
|
|
200
|
-
await setupMongoDB(projectDirectory);
|
|
201
|
-
const devCommand = installCommand.command === 'bun' ? 'bun' : 'npm';
|
|
202
|
-
const child = (0, node_child_process_1.spawn)(devCommand, ['run', 'dev'], {
|
|
203
|
-
cwd: projectDirectory,
|
|
204
|
-
stdio: 'inherit',
|
|
192
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
193
|
+
child.once('error', () => {
|
|
194
|
+
// Opening a browser is a convenience; the installation is already ready.
|
|
205
195
|
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
196
|
+
child.unref();
|
|
197
|
+
}
|
|
198
|
+
export async function installRelease(projectName, options, fetchImplementation = fetch) {
|
|
199
|
+
assertInstallPrerequisites();
|
|
200
|
+
const projectDirectory = resolve(process.cwd(), projectName);
|
|
201
|
+
try {
|
|
202
|
+
await access(projectDirectory);
|
|
203
|
+
throw new Error(`Destination already exists: ${projectDirectory}`);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof Error && error.message.startsWith('Destination')) {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const releaseTag = await resolveReleaseTag(options.release, fetchImplementation);
|
|
211
|
+
const assetUrls = getReleaseAssetUrls(releaseTag);
|
|
212
|
+
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'genfeed-create-release-'));
|
|
213
|
+
const archivePath = join(temporaryDirectory, RELEASE_ARCHIVE_NAME);
|
|
214
|
+
const checksumPath = join(temporaryDirectory, RELEASE_CHECKSUM_NAME);
|
|
215
|
+
let createdProjectDirectory = false;
|
|
216
|
+
let startAttempted = false;
|
|
217
|
+
consola.start(`Downloading Genfeed.ai ${releaseTag}`);
|
|
218
|
+
try {
|
|
219
|
+
await Promise.all([
|
|
220
|
+
downloadFile(assetUrls.archive, archivePath, fetchImplementation),
|
|
221
|
+
downloadFile(assetUrls.checksum, checksumPath, fetchImplementation),
|
|
222
|
+
]);
|
|
223
|
+
await verifyReleaseChecksum(archivePath, checksumPath);
|
|
224
|
+
await mkdir(projectDirectory);
|
|
225
|
+
createdProjectDirectory = true;
|
|
226
|
+
await runCommand('tar', ['-xzf', archivePath, '-C', projectDirectory, '--strip-components=1'], { stdio: 'ignore' });
|
|
227
|
+
const releaseManifest = parseReleaseManifest(await readFile(join(projectDirectory, RELEASE_MANIFEST_NAME), 'utf8'), releaseTag);
|
|
228
|
+
const setup = await configureEnvironment(await readFile(join(projectDirectory, '.env.example'), 'utf8'), options.yes);
|
|
229
|
+
await writeFile(join(projectDirectory, '.env'), setup.content, {
|
|
230
|
+
flag: 'wx',
|
|
231
|
+
mode: 0o600,
|
|
227
232
|
});
|
|
228
|
-
|
|
233
|
+
await runCommand('docker', [
|
|
234
|
+
'compose',
|
|
235
|
+
'--env-file',
|
|
236
|
+
'.env',
|
|
237
|
+
'-f',
|
|
238
|
+
'compose.yml',
|
|
239
|
+
'config',
|
|
240
|
+
'--quiet',
|
|
241
|
+
], { cwd: projectDirectory, stdio: 'ignore' });
|
|
242
|
+
consola.success(`Installed ${releaseManifest.releaseTag} (${releaseManifest.image}) in ${projectDirectory}`);
|
|
243
|
+
if (!options.start) {
|
|
244
|
+
consola.info(`Start later with: cd ${projectName} && docker compose --env-file .env -f compose.yml up -d`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
startAttempted = true;
|
|
248
|
+
await runCommand('docker', ['compose', '--env-file', '.env', '-f', 'compose.yml', 'up', '-d'], { cwd: projectDirectory });
|
|
249
|
+
consola.start('Waiting for Genfeed.ai to become ready…');
|
|
250
|
+
if (await waitForApp(fetchImplementation, setup.appUrl)) {
|
|
251
|
+
consola.success(`Genfeed.ai is running at ${setup.appUrl}`);
|
|
252
|
+
openBrowser(setup.appUrl);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
consola.warn(`The containers started, but ${setup.appUrl} is not ready yet.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (createdProjectDirectory && !startAttempted) {
|
|
260
|
+
await rm(projectDirectory, { force: true, recursive: true });
|
|
261
|
+
}
|
|
262
|
+
else if (startAttempted) {
|
|
263
|
+
consola.warn(`Startup failed. Installation files were preserved at ${projectDirectory} for inspection or recovery.`);
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
await rm(temporaryDirectory, { force: true, recursive: true });
|
|
269
|
+
}
|
|
229
270
|
}
|
|
230
271
|
async function main() {
|
|
231
|
-
|
|
272
|
+
try {
|
|
273
|
+
assertSupportedNodeVersion();
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
consola.error(error instanceof Error ? error.message : String(error));
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const { Command } = await import('commander');
|
|
281
|
+
const program = new Command()
|
|
232
282
|
.name('genfeedai')
|
|
233
|
-
.description('
|
|
234
|
-
.version(
|
|
235
|
-
.argument('<project-name>', 'Directory
|
|
236
|
-
.
|
|
237
|
-
|
|
283
|
+
.description('Install a public Genfeed.ai Community release.')
|
|
284
|
+
.version(readPackageVersion())
|
|
285
|
+
.argument('<project-name>', 'Directory for the self-hosted installation')
|
|
286
|
+
.option('--release <tag>', 'Install an exact release (for example, v0.5.0)')
|
|
287
|
+
.option('--yes', 'Use OPENROUTER_API_KEY and optional SELFHOSTED_DOMAIN without prompts')
|
|
288
|
+
.option('--no-start', 'Prepare and verify the installation without starting it')
|
|
289
|
+
.action(async (projectName, options) => {
|
|
290
|
+
await installRelease(projectName, options);
|
|
238
291
|
})
|
|
239
292
|
.showHelpAfterError();
|
|
240
293
|
try {
|
|
@@ -242,11 +295,22 @@ async function main() {
|
|
|
242
295
|
}
|
|
243
296
|
catch (error) {
|
|
244
297
|
const normalizedError = error instanceof Error ? error : new Error('Unknown CLI error');
|
|
245
|
-
|
|
298
|
+
consola.error(normalizedError.message);
|
|
246
299
|
process.exitCode = 1;
|
|
247
300
|
}
|
|
248
301
|
}
|
|
249
|
-
|
|
302
|
+
function isMainModule() {
|
|
303
|
+
if (!process.argv[1]) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (isMainModule()) {
|
|
250
314
|
void main();
|
|
251
315
|
}
|
|
252
316
|
//# sourceMappingURL=index.js.map
|