@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/README.md CHANGED
@@ -1,27 +1,55 @@
1
- # genfeedai
1
+ # @genfeedai/create
2
2
 
3
- Bootstrap a self-hosted Genfeed Core project from the official template.
3
+ Install a checksummed Genfeed.ai Community release with Docker Compose.
4
4
 
5
- ## Usage
5
+ ## Quick Start
6
6
 
7
7
  ```bash
8
- npx genfeedai init my-app
8
+ npx @genfeedai/create my-genfeed
9
9
  ```
10
10
 
11
- This command:
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
- - downloads the latest `genfeedai/core` template
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
- ## Example
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 init my-app
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 InstallCommand {
3
- command: string;
4
- args: string[];
5
- label: string;
2
+ interface CliOptions {
3
+ release?: string;
4
+ start: boolean;
5
+ yes?: boolean;
6
6
  }
7
- export declare function commandExists(command: string): boolean;
8
- export declare function getInstallCommand(hasCommand?: (command: string) => boolean): InstallCommand;
9
- export declare function writeDefaultEnv(projectDirectory: string): Promise<void>;
10
- /**
11
- * Open a URL in the system default browser.
12
- * Supports macOS, Linux (xdg-open), and Windows.
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
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.commandExists = commandExists;
5
- exports.getInstallCommand = getInstallCommand;
6
- exports.writeDefaultEnv = writeDefaultEnv;
7
- exports.openBrowser = openBrowser;
8
- exports.setupMongoDB = setupMongoDB;
9
- const commander_1 = require("commander");
10
- const consola_1 = require("consola");
11
- const giget_1 = require("giget");
12
- const mongodb_memory_server_1 = require("mongodb-memory-server");
13
- const node_child_process_1 = require("node:child_process");
14
- const promises_1 = require("node:fs/promises");
15
- const node_path_1 = require("node:path");
16
- const TEMPLATE_REPOSITORY = 'github:genfeedai/core#master';
17
- const MANAGED_MONGO_URI = 'mongodb://127.0.0.1:27017/genfeed';
18
- const ONBOARDING_URL = 'http://localhost:3000/onboarding';
19
- function commandExists(command) {
20
- const result = (0, node_child_process_1.spawnSync)(command, ['--version'], { stdio: 'ignore' });
21
- return result.status === 0;
22
- }
23
- function getInstallCommand(hasCommand = commandExists) {
24
- if (hasCommand('bun')) {
25
- return {
26
- args: ['install'],
27
- command: 'bun',
28
- label: 'bun install',
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
- args: ['install'],
33
- command: 'npm',
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 fileExists(filePath) {
38
- try {
39
- await (0, promises_1.access)(filePath);
40
- return true;
44
+ export async function resolveReleaseTag(requestedRelease, fetchImplementation = fetch) {
45
+ if (requestedRelease) {
46
+ return normalizeReleaseTag(requestedRelease);
41
47
  }
42
- catch {
43
- return false;
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
- const DEFAULT_ENV = `# ─── Required ───────────────────────────────────────────────────────
47
- MONGODB_URI=mongodb://127.0.0.1:27017/genfeed
48
-
49
- # ─── API Server ─────────────────────────────────────────────────────
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
- consola_1.consola.success('Created .env with working defaults');
73
+ return manifest.version;
86
74
  }
87
- /**
88
- * Open a URL in the system default browser.
89
- * Supports macOS, Linux (xdg-open), and Windows.
90
- */
91
- function openBrowser(url) {
92
- const platform = process.platform;
93
- if (platform === 'darwin') {
94
- (0, node_child_process_1.execFile)('open', [url]);
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
- else if (platform === 'linux') {
97
- (0, node_child_process_1.execFile)('xdg-open', [url]);
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
- else if (platform === 'win32') {
100
- (0, node_child_process_1.execFile)('cmd', ['/c', 'start', '', url]);
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
- async function waitForServer(url, timeoutMs = 120_000) {
104
- const start = Date.now();
105
- while (Date.now() - start < timeoutMs) {
106
- try {
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
- return false;
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 runInstall(projectDirectory) {
120
- const installCommand = getInstallCommand();
123
+ async function runCommand(command, args, options = {}) {
121
124
  await new Promise((resolvePromise, rejectPromise) => {
122
- const child = (0, node_child_process_1.spawn)(installCommand.command, installCommand.args, {
123
- cwd: projectDirectory,
124
- env: {
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.on('error', (error) => {
131
- rejectPromise(error);
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(`${installCommand.label} failed with exit code ${code ?? 'unknown'}`));
135
+ rejectPromise(new Error(`${command} ${args.join(' ')} failed with ${code ?? signal ?? 'an unknown error'}.`));
139
136
  });
140
137
  });
141
- return installCommand;
142
- }
143
- async function replaceEnvValue(projectDirectory, key, value) {
144
- const pattern = new RegExp(`^${key}=.*$`, 'm');
145
- for (const location of ENV_LOCATIONS) {
146
- const envPath = (0, node_path_1.join)(projectDirectory, location);
147
- const exists = await fileExists(envPath);
148
- if (!exists) {
149
- continue;
150
- }
151
- const content = await (0, promises_1.readFile)(envPath, 'utf-8');
152
- if (pattern.test(content)) {
153
- await (0, promises_1.writeFile)(envPath, content.replace(pattern, `${key}=${value}`), 'utf-8');
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
- else {
156
- await (0, promises_1.appendFile)(envPath, `\n${key}=${value}`, 'utf-8');
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
- * Ask the user whether to use managed MongoDB or provide their own URI,
162
- * then update the project's .env file accordingly.
163
- *
164
- * - Managed path: downloads the mongod binary (cached after first run),
165
- * sets MONGODB_URI + GENFEED_MANAGED_MONGO=true.
166
- * - Custom path: prompts for a URI and sets only MONGODB_URI.
167
- */
168
- async function setupMongoDB(projectDirectory) {
169
- const hasOwnMongo = await consola_1.consola.prompt('Do you have an existing MongoDB instance? (press Enter to use managed MongoDB)', {
170
- initial: false,
171
- type: 'confirm',
172
- });
173
- if (hasOwnMongo) {
174
- const uri = await consola_1.consola.prompt('Enter your MongoDB URI:', {
175
- initial: MANAGED_MONGO_URI,
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
- consola_1.consola.start('Downloading MongoDB binary (cached after first run)…');
183
- await mongodb_memory_server_1.MongoBinary.getPath({});
184
- consola_1.consola.success('MongoDB binary ready');
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
- consola_1.consola.start('Waiting for dev servers to start…');
207
- const [apiReady, webReady] = await Promise.all([
208
- waitForServer('http://localhost:3001/api'),
209
- waitForServer('http://localhost:3000'),
210
- ]);
211
- if (apiReady && webReady) {
212
- consola_1.consola.box([
213
- `Genfeed Core is running.`,
214
- '',
215
- `Opening ${ONBOARDING_URL}`,
216
- ].join('\n'));
217
- openBrowser(ONBOARDING_URL);
218
- }
219
- else {
220
- consola_1.consola.warn('Servers did not start in time. Open manually:');
221
- consola_1.consola.info(ONBOARDING_URL);
222
- }
223
- await new Promise((resolvePromise) => {
224
- child.on('close', (code) => {
225
- process.exitCode = code ?? 0;
226
- resolvePromise();
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
- const program = new commander_1.Command()
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('Genfeed Core — AI workflow engine. Self-host for free.')
234
- .version('0.1.3')
235
- .argument('<project-name>', 'Directory name for the new project')
236
- .action(async (projectName) => {
237
- await scaffoldProject(projectName);
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
- consola_1.consola.error(normalizedError.message);
298
+ consola.error(normalizedError.message);
246
299
  process.exitCode = 1;
247
300
  }
248
301
  }
249
- if (require.main === module) {
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