@valerypopoff/rivet2-cli 2.0.10 → 2.1.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 +12 -0
- package/bin/cli.js +28 -2
- package/bin/cliRuntime.js +228 -0
- package/bin/commandInputs.js +37 -1
- package/bin/commands/doctor.js +188 -0
- package/bin/commands/list.js +81 -0
- package/bin/commands/run.js +90 -39
- package/bin/commands/serve.js +136 -195
- package/bin/commands/serveApp.js +99 -0
- package/bin/http.js +107 -0
- package/bin/output.js +32 -0
- package/dist/types/cliRuntime.d.ts +47 -0
- package/dist/types/commandInputs.d.ts +7 -1
- package/dist/types/commands/doctor.d.ts +28 -0
- package/dist/types/commands/list.d.ts +44 -0
- package/dist/types/commands/run.d.ts +32 -8
- package/dist/types/commands/serve.d.ts +36 -14
- package/dist/types/commands/serveApp.d.ts +35 -0
- package/dist/types/http.d.ts +23 -0
- package/dist/types/output.d.ts +9 -0
- package/package.json +2 -2
package/bin/commands/run.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { createProcessor
|
|
1
|
+
import { createProcessor } from '@valerypopoff/rivet2-node';
|
|
2
|
+
import { configDotenv } from 'dotenv';
|
|
2
3
|
import { resolve } from 'node:path';
|
|
3
|
-
import { parseJsonInputRecord, parseKeyValueInputRecord } from '../commandInputs.js';
|
|
4
|
+
import { parseJsonInputRecord, parseJsonInputRecordFromFile, parseJsonKeyValueInputRecord, parseKeyValueInputRecord, throwIfConflictingInputSources, } from '../commandInputs.js';
|
|
5
|
+
import { addDatasetOptions, addProviderOptions, loadProjectRuntime, throwIfInvalidGraph, throwIfNoMainGraph, withCliProcessorOptions, } from '../cliRuntime.js';
|
|
6
|
+
import { shapeOutputs, validateOutputShapeOptions, writeJsonOutput } from '../output.js';
|
|
4
7
|
export function makeCommand(y) {
|
|
5
|
-
|
|
8
|
+
const command = addProviderOptions(y)
|
|
6
9
|
.positional('projectFile', {
|
|
7
10
|
describe: 'The project file to run',
|
|
8
11
|
type: 'string',
|
|
@@ -16,17 +19,49 @@ export function makeCommand(y) {
|
|
|
16
19
|
describe: 'Read inputs from stdin as JSON',
|
|
17
20
|
type: 'boolean',
|
|
18
21
|
default: false,
|
|
22
|
+
})
|
|
23
|
+
.option('inputs-file', {
|
|
24
|
+
describe: 'Read inputs from a JSON file',
|
|
25
|
+
type: 'string',
|
|
26
|
+
})
|
|
27
|
+
.option('input-json', {
|
|
28
|
+
describe: 'Adds a JSON input to the graph run using key=json',
|
|
29
|
+
type: 'string',
|
|
30
|
+
array: true,
|
|
31
|
+
default: [],
|
|
19
32
|
})
|
|
20
33
|
.option('include-cost', {
|
|
21
34
|
describe: 'Include the total cost in the output',
|
|
22
35
|
type: 'boolean',
|
|
23
36
|
default: false,
|
|
37
|
+
})
|
|
38
|
+
.option('output-key', {
|
|
39
|
+
describe: 'Print only one named output Data Value wrapper',
|
|
40
|
+
type: 'string',
|
|
41
|
+
})
|
|
42
|
+
.option('unwrap-output', {
|
|
43
|
+
describe: 'Print only the .value field from one named output',
|
|
44
|
+
type: 'string',
|
|
45
|
+
})
|
|
46
|
+
.option('output-file', {
|
|
47
|
+
describe: 'Write the JSON output to a file instead of stdout',
|
|
48
|
+
type: 'string',
|
|
24
49
|
})
|
|
25
50
|
.option('context', {
|
|
26
51
|
describe: 'Adds a context value to the graph run',
|
|
27
52
|
type: 'string',
|
|
28
53
|
array: true,
|
|
29
54
|
default: [],
|
|
55
|
+
})
|
|
56
|
+
.option('context-json', {
|
|
57
|
+
describe: 'Adds a JSON context value using key=json',
|
|
58
|
+
type: 'string',
|
|
59
|
+
array: true,
|
|
60
|
+
default: [],
|
|
61
|
+
})
|
|
62
|
+
.option('context-file', {
|
|
63
|
+
describe: 'Read context values from a JSON file',
|
|
64
|
+
type: 'string',
|
|
30
65
|
})
|
|
31
66
|
.option('input', {
|
|
32
67
|
describe: 'Adds an input to the graph run',
|
|
@@ -34,44 +69,60 @@ export function makeCommand(y) {
|
|
|
34
69
|
array: true,
|
|
35
70
|
default: [],
|
|
36
71
|
});
|
|
72
|
+
return addDatasetOptions(command);
|
|
37
73
|
}
|
|
38
74
|
export async function run(args) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
67
|
-
const outputs = await run();
|
|
68
|
-
if (!args.includeCost) {
|
|
69
|
-
delete outputs.cost;
|
|
70
|
-
}
|
|
71
|
-
console.log(JSON.stringify(outputs, null, 2));
|
|
75
|
+
configDotenv();
|
|
76
|
+
validateOutputShapeOptions(args);
|
|
77
|
+
const runtime = await loadProjectRuntime(args.projectFile, args);
|
|
78
|
+
throwIfNoMainGraph(runtime.project, args.graphName, runtime.projectPath, 'positional');
|
|
79
|
+
throwIfInvalidGraph(runtime.project, args.graphName, 'positional');
|
|
80
|
+
const { run: runProcessor } = createProcessor(runtime.project, withCliProcessorOptions(runtime, args, {
|
|
81
|
+
context: await readContext(args),
|
|
82
|
+
graph: args.graphName,
|
|
83
|
+
inputs: await readInputs(args),
|
|
84
|
+
}));
|
|
85
|
+
await writeJsonOutput(shapeOutputs(await runProcessor(), {
|
|
86
|
+
includeCost: args.includeCost,
|
|
87
|
+
outputKey: args.outputKey,
|
|
88
|
+
unwrapOutput: args.unwrapOutput,
|
|
89
|
+
}), args.outputFile);
|
|
90
|
+
}
|
|
91
|
+
async function readInputs(args) {
|
|
92
|
+
throwIfConflictingInputSources([
|
|
93
|
+
{ enabled: args.inputsStdin, name: '--inputs-stdin' },
|
|
94
|
+
{ enabled: args.inputsFile != null, name: '--inputs-file' },
|
|
95
|
+
{ enabled: args.input.length > 0 || args.inputJson.length > 0, name: '--input/--input-json' },
|
|
96
|
+
], 'input');
|
|
97
|
+
if (args.inputsStdin) {
|
|
98
|
+
return parseJsonInputRecord(await readStdin(), 'Input stdin');
|
|
99
|
+
}
|
|
100
|
+
if (args.inputsFile) {
|
|
101
|
+
return parseJsonInputRecordFromFile(resolve(process.cwd(), args.inputsFile), 'Input file');
|
|
72
102
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
return {
|
|
104
|
+
...parseKeyValueInputRecord(args.input, 'input'),
|
|
105
|
+
...parseJsonKeyValueInputRecord(args.inputJson, 'input-json'),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function readContext(args) {
|
|
109
|
+
throwIfConflictingInputSources([
|
|
110
|
+
{ enabled: args.contextFile != null, name: '--context-file' },
|
|
111
|
+
{ enabled: args.context.length > 0 || args.contextJson.length > 0, name: '--context/--context-json' },
|
|
112
|
+
], 'context');
|
|
113
|
+
if (args.contextFile) {
|
|
114
|
+
return parseJsonInputRecordFromFile(resolve(process.cwd(), args.contextFile), 'Context file');
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
...parseKeyValueInputRecord(args.context, 'context'),
|
|
118
|
+
...parseJsonKeyValueInputRecord(args.contextJson, 'context-json'),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function readStdin() {
|
|
122
|
+
process.stdin.setEncoding('utf8');
|
|
123
|
+
let inputText = '';
|
|
124
|
+
for await (const chunk of process.stdin) {
|
|
125
|
+
inputText += chunk;
|
|
76
126
|
}
|
|
127
|
+
return inputText;
|
|
77
128
|
}
|
package/bin/commands/serve.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
import { serve as serveHono } from '@hono/node-server';
|
|
2
1
|
import { createProcessor, getSingleNodeStream, loadProjectFromFile } from '@valerypopoff/rivet2-node';
|
|
3
2
|
import chalk from 'chalk';
|
|
4
|
-
import didYouMean from 'didyoumean2';
|
|
5
3
|
import { configDotenv } from 'dotenv';
|
|
6
4
|
import { Hono } from 'hono';
|
|
7
|
-
import { readdir, stat } from 'node:fs/promises';
|
|
8
|
-
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
9
5
|
import { parseJsonInputRecord } from '../commandInputs.js';
|
|
6
|
+
import { addDatasetOptions, addProviderOptions, loadProjectRuntime, resolveServedGraphName, throwIfInvalidGraph, throwIfNoMainGraph, warnIfServerSavesDatasets, withCliProcessorOptions, withProviderProcessorOptions, } from '../cliRuntime.js';
|
|
7
|
+
import { CliHttpError, addHttpOptions, createHttpMiddleware, formatListenUrl, jsonErrorResponse, jsonTimedResponse, startHttpServer, } from '../http.js';
|
|
8
|
+
import { shapeOutputs } from '../output.js';
|
|
10
9
|
export function makeCommand(y) {
|
|
11
|
-
|
|
10
|
+
const command = addHttpOptions(addProviderOptions(y)
|
|
12
11
|
.option('port', {
|
|
13
12
|
describe: 'The port to serve on',
|
|
14
13
|
type: 'number',
|
|
15
14
|
default: 3000,
|
|
15
|
+
})
|
|
16
|
+
.option('host', {
|
|
17
|
+
describe: 'The host interface to bind to',
|
|
18
|
+
type: 'string',
|
|
19
|
+
default: '0.0.0.0',
|
|
16
20
|
})
|
|
17
21
|
.option('dev', {
|
|
18
22
|
describe: 'Run in development mode: rereads the project file on each request',
|
|
@@ -29,25 +33,21 @@ export function makeCommand(y) {
|
|
|
29
33
|
type: 'boolean',
|
|
30
34
|
default: false,
|
|
31
35
|
})
|
|
32
|
-
.option('
|
|
33
|
-
describe: '
|
|
34
|
-
type: 'string',
|
|
35
|
-
demandOption: false,
|
|
36
|
-
})
|
|
37
|
-
.option('openai-endpoint', {
|
|
38
|
-
describe: 'The OpenAI API endpoint to use for the project. If omitted, the environment variable OPENAI_ENDPOINT is used.',
|
|
36
|
+
.option('endpoint', {
|
|
37
|
+
describe: 'Expose a named endpoint alias using endpointName=graphNameOrId',
|
|
39
38
|
type: 'string',
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
type: 'string',
|
|
45
|
-
demandOption: false,
|
|
46
|
-
})
|
|
39
|
+
array: true,
|
|
40
|
+
default: [],
|
|
41
|
+
}));
|
|
42
|
+
return addDatasetOptions(command)
|
|
47
43
|
.option('expose-cost', {
|
|
48
44
|
describe: 'Expose the cost of the graph run in the response',
|
|
49
45
|
type: 'boolean',
|
|
50
46
|
default: false,
|
|
47
|
+
})
|
|
48
|
+
.option('unwrap-output', {
|
|
49
|
+
describe: 'Respond with only the .value field from one named output',
|
|
50
|
+
type: 'string',
|
|
51
51
|
})
|
|
52
52
|
.option('stream', {
|
|
53
53
|
describe: 'Turns on streaming mode. Rivet events will be sent to the client using SSE (Server-Sent Events). If this is set to a Node ID or node title, only events for that node will be sent.',
|
|
@@ -66,94 +66,96 @@ export function makeCommand(y) {
|
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
export async function serve(args) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
configDotenv();
|
|
70
|
+
const { app, projectFilePath, servedGraphName } = await createServeApp(args);
|
|
71
|
+
startHttpServer(app, args.host, args.port);
|
|
72
|
+
console.log(chalk.green(`Serving project file ${chalk.bold.white(projectFilePath)} at ${chalk.bold.white(formatListenUrl(args.host, args.port))}.\nServing graph "${chalk.bold.white(servedGraphName)}".`));
|
|
73
|
+
}
|
|
74
|
+
export async function createServeApp(args) {
|
|
75
|
+
const runtime = await loadProjectRuntime(args.projectFile, args);
|
|
76
|
+
const endpointAliases = parseEndpointAliases(args.endpoint, runtime.project);
|
|
77
|
+
throwIfNoMainGraph(runtime.project, args.graph, runtime.projectPath);
|
|
78
|
+
throwIfInvalidGraph(runtime.project, args.graph);
|
|
79
|
+
if (args.stream != null) {
|
|
80
|
+
console.log('Streaming is enabled');
|
|
81
|
+
}
|
|
82
|
+
warnIfServerSavesDatasets(args.saveDatasets, 'CLI server');
|
|
83
|
+
if (args.streamNode != null) {
|
|
84
|
+
if (args.stream == null) {
|
|
85
|
+
throw new Error('--stream-node requires --stream.');
|
|
78
86
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
console.log(`Streaming node ${chalk.bold(args.streamNode)}`);
|
|
88
|
+
}
|
|
89
|
+
const app = new Hono();
|
|
90
|
+
app.use('*', createHttpMiddleware(args));
|
|
91
|
+
app.get('/healthz', (c) => c.json({
|
|
92
|
+
ok: true,
|
|
93
|
+
projectPath: runtime.projectPath,
|
|
94
|
+
}));
|
|
95
|
+
app.post('/', async (c) => handleGraphRequest(c, args, runtime, args.graph));
|
|
96
|
+
app.post('/endpoints/:endpointName', async (c) => {
|
|
97
|
+
const endpointName = c.req.param('endpointName');
|
|
98
|
+
const graph = endpointAliases.get(endpointName);
|
|
99
|
+
if (!graph) {
|
|
100
|
+
return jsonErrorResponse(c, new CliHttpError(`Endpoint "${endpointName}" not found.`, 404), Date.now(), 404);
|
|
84
101
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
102
|
+
return handleGraphRequest(c, args, runtime, graph);
|
|
103
|
+
});
|
|
104
|
+
if (args.allowSpecifyingGraphId) {
|
|
105
|
+
app.post('/:graphId', async (c) => handleGraphRequest(c, args, runtime, c.req.param('graphId')));
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
app,
|
|
109
|
+
projectFilePath: runtime.projectPath,
|
|
110
|
+
servedGraphName: resolveServedGraphName(runtime.project, args.graph),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function handleGraphRequest(c, args, runtime, graph) {
|
|
114
|
+
const startedAt = Date.now();
|
|
115
|
+
let graphRunArgs;
|
|
116
|
+
try {
|
|
117
|
+
const project = args.dev ? await loadProjectFromFile(runtime.projectPath) : runtime.project;
|
|
118
|
+
throwIfInvalidGraph(project, graph);
|
|
119
|
+
const inputs = parseJsonInputRecord(await c.req.text(), 'Request body');
|
|
120
|
+
graphRunArgs = buildGraphRunArgs(args, runtime, project, inputs, graph);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return jsonErrorResponse(c, toBadRequestError(error), startedAt, 400);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
if (args.stream != null) {
|
|
127
|
+
const stream = await streamGraph({
|
|
128
|
+
...graphRunArgs,
|
|
129
|
+
stream: args.stream,
|
|
130
|
+
streamNode: args.streamNode,
|
|
112
131
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log(chalk.green(`Serving project file ${chalk.bold.white(projectFilePath)} on port ${chalk.bold.white(args.port)}.\nServing graph "${chalk.bold.white(servedGraphName)}".`));
|
|
120
|
-
function shutdown() {
|
|
121
|
-
console.log('Shutting down...');
|
|
122
|
-
server.close((err) => {
|
|
123
|
-
if (err) {
|
|
124
|
-
console.error(err);
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
127
|
-
process.exit(0);
|
|
132
|
+
return new Response(stream, {
|
|
133
|
+
headers: {
|
|
134
|
+
'Cache-Control': 'no-cache',
|
|
135
|
+
Connection: 'keep-alive',
|
|
136
|
+
'Content-Type': 'text/event-stream',
|
|
137
|
+
},
|
|
128
138
|
});
|
|
129
139
|
}
|
|
130
|
-
|
|
131
|
-
process.on('SIGTERM', shutdown);
|
|
140
|
+
return jsonTimedResponse(c, await runGraph(graphRunArgs), startedAt);
|
|
132
141
|
}
|
|
133
|
-
catch (
|
|
134
|
-
|
|
135
|
-
process.exit(1);
|
|
142
|
+
catch (error) {
|
|
143
|
+
return jsonErrorResponse(c, error, startedAt, 500);
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
|
-
function buildGraphRunArgs(args, project, inputs, graph) {
|
|
146
|
+
function buildGraphRunArgs(args, runtime, project, inputs, graph) {
|
|
139
147
|
return {
|
|
140
148
|
exposeCost: args.exposeCost,
|
|
141
149
|
graph,
|
|
142
150
|
inputs,
|
|
143
|
-
openaiApiKey: args.openaiApiKey,
|
|
144
|
-
openaiEndpoint: args.openaiEndpoint,
|
|
145
|
-
openaiOrganization: args.openaiOrganization,
|
|
146
151
|
project,
|
|
152
|
+
providerOptions: args,
|
|
153
|
+
runtime,
|
|
154
|
+
unwrapOutput: args.unwrapOutput,
|
|
147
155
|
};
|
|
148
156
|
}
|
|
149
|
-
async function streamGraph({ project, inputs, graph,
|
|
150
|
-
const { run, processor, getSSEStream } = createProcessor(project, {
|
|
151
|
-
inputs,
|
|
152
|
-
graph,
|
|
153
|
-
openAiKey: openaiApiKey,
|
|
154
|
-
openAiEndpoint: openaiEndpoint,
|
|
155
|
-
openAiOrganization: openaiOrganization,
|
|
156
|
-
});
|
|
157
|
+
async function streamGraph({ project, inputs, graph, providerOptions, runtime, stream, streamNode, }) {
|
|
158
|
+
const { run, processor, getSSEStream } = createProcessor(project, withCliProcessorOptions(runtime, providerOptions, buildStreamingGraphProcessorOptions({ graph, inputs })));
|
|
157
159
|
const responseStream = streamNode
|
|
158
160
|
? getSingleNodeStream(processor, streamNode)
|
|
159
161
|
: getSSEStream(buildStreamEventFilter(stream));
|
|
@@ -162,127 +164,66 @@ async function streamGraph({ project, inputs, graph, openaiApiKey, openaiEndpoin
|
|
|
162
164
|
});
|
|
163
165
|
return responseStream;
|
|
164
166
|
}
|
|
167
|
+
export function buildGraphProcessorOptions({ inputs, graph, openaiApiKey, openaiEndpoint, openaiOrganization, }) {
|
|
168
|
+
return withProviderProcessorOptions({ openaiApiKey, openaiEndpoint, openaiOrganization }, {
|
|
169
|
+
graph,
|
|
170
|
+
inputs,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
export function buildStreamingGraphProcessorOptions(args) {
|
|
174
|
+
return {
|
|
175
|
+
...buildGraphProcessorOptions(args),
|
|
176
|
+
runtimeProfile: 'compatible',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
165
179
|
export function buildStreamEventFilter(stream) {
|
|
166
180
|
const streamTarget = stream?.trim();
|
|
167
181
|
if (!streamTarget) {
|
|
168
182
|
return {
|
|
169
|
-
nodeStart: true,
|
|
170
183
|
nodeFinish: true,
|
|
184
|
+
nodeStart: true,
|
|
171
185
|
partialOutputs: true,
|
|
172
186
|
};
|
|
173
187
|
}
|
|
174
188
|
return {
|
|
175
|
-
nodeStart: [streamTarget],
|
|
176
189
|
nodeFinish: [streamTarget],
|
|
190
|
+
nodeStart: [streamTarget],
|
|
177
191
|
partialOutputs: [streamTarget],
|
|
178
192
|
};
|
|
179
193
|
}
|
|
180
|
-
async function runGraph({ project, inputs, graph,
|
|
181
|
-
const { run } = createProcessor(project, {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
openAiEndpoint: openaiEndpoint,
|
|
186
|
-
openAiOrganization: openaiOrganization,
|
|
194
|
+
async function runGraph({ project, inputs, graph, providerOptions, exposeCost, runtime, unwrapOutput, }) {
|
|
195
|
+
const { run } = createProcessor(project, withCliProcessorOptions(runtime, providerOptions, { graph, inputs }));
|
|
196
|
+
return shapeOutputs(await run(), {
|
|
197
|
+
includeCost: exposeCost,
|
|
198
|
+
unwrapOutput,
|
|
187
199
|
});
|
|
188
|
-
const outputs = await run();
|
|
189
|
-
if (!exposeCost) {
|
|
190
|
-
delete outputs.cost;
|
|
191
|
-
}
|
|
192
|
-
return outputs;
|
|
193
|
-
}
|
|
194
|
-
function getGraphSummaries(project) {
|
|
195
|
-
return Object.values(project.graphs).map((graph) => ({
|
|
196
|
-
id: graph.metadata.id,
|
|
197
|
-
name: graph.metadata.name,
|
|
198
|
-
}));
|
|
199
|
-
}
|
|
200
|
-
function findGraph(project, graphIdOrName) {
|
|
201
|
-
if (!graphIdOrName) {
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
return getGraphSummaries(project).find((graph) => graph.id === graphIdOrName || graph.name === graphIdOrName);
|
|
205
|
-
}
|
|
206
|
-
function formatGraphList(project) {
|
|
207
|
-
return getGraphSummaries(project)
|
|
208
|
-
.map((graph) => `- "${graph.name}" (${graph.id})`)
|
|
209
|
-
.join('\n');
|
|
210
|
-
}
|
|
211
|
-
function resolveServedGraphName(project, graph) {
|
|
212
|
-
const servedGraph = findGraph(project, graph ?? project.metadata.mainGraphId);
|
|
213
|
-
if (!servedGraph) {
|
|
214
|
-
throw new Error(`Project main graph "${project.metadata.mainGraphId}" was not found in the project file.`);
|
|
215
|
-
}
|
|
216
|
-
return servedGraph.name;
|
|
217
200
|
}
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
throw new Error('No graphs found in the project file. Please edit the project file in Rivet and add a graph.');
|
|
225
|
-
}
|
|
226
|
-
const firstExample = `rivet serve ${projectFilePath} --graph ${validGraphs[0].id}`;
|
|
227
|
-
const secondExample = `rivet serve ${projectFilePath} --graph "${validGraphs[0].name}"`;
|
|
228
|
-
throw new Error(`No graph name provided, and project does not specify a main graph. Valid graphs are: \n\n${formatGraphList(project)}\n\nUse either the graph's name or its ID. For example, \n- \`${chalk.bold(firstExample)}\` or\n- \`${chalk.bold(secondExample)}\``);
|
|
229
|
-
}
|
|
230
|
-
function throwIfInvalidGraph(project, graph) {
|
|
231
|
-
if (!graph) {
|
|
232
|
-
const mainGraphId = project.metadata.mainGraphId;
|
|
233
|
-
if (!mainGraphId || findGraph(project, mainGraphId)) {
|
|
234
|
-
return;
|
|
201
|
+
export function parseEndpointAliases(endpointSpecs, project) {
|
|
202
|
+
const aliases = new Map();
|
|
203
|
+
for (const spec of endpointSpecs) {
|
|
204
|
+
const separatorIndex = spec.indexOf('=');
|
|
205
|
+
if (separatorIndex <= 0) {
|
|
206
|
+
throw new Error(`Invalid endpoint "${spec}". Expected endpointName=graphNameOrId.`);
|
|
235
207
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
const validGraphsAndIds = getGraphSummaries(project).flatMap((graph) => [graph.id, graph.name]);
|
|
242
|
-
const suggestion = didYouMean(graph, validGraphsAndIds);
|
|
243
|
-
if (suggestion) {
|
|
244
|
-
throw new Error(`Graph "${graph}" not found in project file. Did you mean \`${chalk.bold(`--graph "${suggestion}"`)}\`?`);
|
|
245
|
-
}
|
|
246
|
-
throw new Error(`Graph "${graph}" not found in project file. Valid graphs are: \n${formatGraphList(project)}`);
|
|
247
|
-
}
|
|
248
|
-
async function getProjectFile(initialProjectFilePath) {
|
|
249
|
-
let projectFilePath = resolve(process.cwd(), initialProjectFilePath ?? (await getProjectFilePathFromDirectory(process.cwd())));
|
|
250
|
-
await throwIfMissingFile(projectFilePath);
|
|
251
|
-
if ((await stat(projectFilePath)).isDirectory()) {
|
|
252
|
-
projectFilePath = await getProjectFilePathFromDirectory(projectFilePath);
|
|
253
|
-
}
|
|
254
|
-
return projectFilePath;
|
|
255
|
-
}
|
|
256
|
-
async function getProjectFilePathFromDirectory(directory) {
|
|
257
|
-
const files = await readdir(directory);
|
|
258
|
-
const projectFiles = files.filter((file) => extname(file).toLowerCase() === '.rivet-project');
|
|
259
|
-
if (projectFiles.length === 0) {
|
|
260
|
-
throw new Error('No project file found in the current directory. Project files should end with .rivet-project.');
|
|
261
|
-
}
|
|
262
|
-
if (projectFiles.length > 1) {
|
|
263
|
-
throw new Error(`Multiple project files found in the current directory. Please specify which one to serve: \n${projectFiles.join('\n')}`);
|
|
264
|
-
}
|
|
265
|
-
return join(directory, projectFiles[0]);
|
|
266
|
-
}
|
|
267
|
-
async function throwIfMissingFile(filePath) {
|
|
268
|
-
try {
|
|
269
|
-
await stat(filePath);
|
|
270
|
-
}
|
|
271
|
-
catch (err) {
|
|
272
|
-
if (err.code !== 'ENOENT') {
|
|
273
|
-
throw err;
|
|
274
|
-
}
|
|
275
|
-
let possibleFiles = [];
|
|
276
|
-
try {
|
|
277
|
-
possibleFiles = await readdir(dirname(filePath));
|
|
208
|
+
const endpointName = spec.slice(0, separatorIndex).trim();
|
|
209
|
+
const graph = spec.slice(separatorIndex + 1).trim();
|
|
210
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(endpointName)) {
|
|
211
|
+
throw new Error(`Invalid endpoint name "${endpointName}". Endpoint names must use letters, numbers, hyphens, or underscores, and must start with a letter or number.`);
|
|
278
212
|
}
|
|
279
|
-
|
|
280
|
-
throw new Error(`
|
|
213
|
+
if (!graph) {
|
|
214
|
+
throw new Error(`Invalid endpoint "${spec}". Expected a graph name or ID after "=".`);
|
|
281
215
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
throw new Error(`Could not find project file "${filePath}". Did you mean "${join(dirname(filePath), suggestion)}"?`);
|
|
216
|
+
if (aliases.has(endpointName)) {
|
|
217
|
+
throw new Error(`Duplicate endpoint "${endpointName}".`);
|
|
285
218
|
}
|
|
286
|
-
|
|
219
|
+
throwIfInvalidGraph(project, graph);
|
|
220
|
+
aliases.set(endpointName, graph);
|
|
221
|
+
}
|
|
222
|
+
return aliases;
|
|
223
|
+
}
|
|
224
|
+
function toBadRequestError(error) {
|
|
225
|
+
if (error instanceof CliHttpError) {
|
|
226
|
+
return error;
|
|
287
227
|
}
|
|
228
|
+
return new CliHttpError(error instanceof Error ? error.message : String(error), 400);
|
|
288
229
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createRivetWebAppHandler } from '@valerypopoff/rivet2-node';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { configDotenv } from 'dotenv';
|
|
4
|
+
import { Hono } from 'hono';
|
|
5
|
+
import { addDatasetOptions, addProviderOptions, loadProjectRuntime, resolveUiGraph, warnIfServerSavesDatasets, withCliProcessorOptions, } from '../cliRuntime.js';
|
|
6
|
+
import { addHttpOptions, createHttpMiddleware, formatListenUrl, jsonErrorResponse, startHttpServer, } from '../http.js';
|
|
7
|
+
export function makeCommand(y) {
|
|
8
|
+
const command = addHttpOptions(addProviderOptions(y)
|
|
9
|
+
.positional('projectFile', {
|
|
10
|
+
describe: 'The project file containing the Rivet web app',
|
|
11
|
+
type: 'string',
|
|
12
|
+
demandOption: true,
|
|
13
|
+
})
|
|
14
|
+
.positional('uiGraph', {
|
|
15
|
+
describe: 'The web app name or ID to serve. If omitted, the only web app in the project is used.',
|
|
16
|
+
type: 'string',
|
|
17
|
+
})
|
|
18
|
+
.option('port', {
|
|
19
|
+
describe: 'The port to serve on',
|
|
20
|
+
type: 'number',
|
|
21
|
+
default: 3000,
|
|
22
|
+
})
|
|
23
|
+
.option('host', {
|
|
24
|
+
describe: 'The host interface to bind to',
|
|
25
|
+
type: 'string',
|
|
26
|
+
default: '0.0.0.0',
|
|
27
|
+
})
|
|
28
|
+
.option('base-path', {
|
|
29
|
+
describe: 'The URL base path where the web app is mounted',
|
|
30
|
+
type: 'string',
|
|
31
|
+
default: '/',
|
|
32
|
+
})
|
|
33
|
+
.option('dev', {
|
|
34
|
+
describe: 'Run in development mode: rereads the project file on each request',
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
default: false,
|
|
37
|
+
})
|
|
38
|
+
.option('revision-key', {
|
|
39
|
+
describe: 'Opaque revision key embedded in HTML and checked on action requests',
|
|
40
|
+
type: 'string',
|
|
41
|
+
}));
|
|
42
|
+
return addDatasetOptions(command);
|
|
43
|
+
}
|
|
44
|
+
export async function serveApp(args) {
|
|
45
|
+
configDotenv();
|
|
46
|
+
const { app, projectFilePath, uiGraphName } = await createWebAppServeApp(args);
|
|
47
|
+
startHttpServer(app, args.host, args.port);
|
|
48
|
+
console.log(chalk.green(`Serving Rivet web app "${chalk.bold.white(uiGraphName)}" from ${chalk.bold.white(projectFilePath)} at ${chalk.bold.white(formatListenUrl(args.host, args.port))}.`));
|
|
49
|
+
}
|
|
50
|
+
export async function createWebAppServeApp(args) {
|
|
51
|
+
throwIfReservedBasePath(args.basePath);
|
|
52
|
+
const runtime = await loadProjectRuntime(args.projectFile, args);
|
|
53
|
+
const uiGraph = resolveUiGraph(runtime.project, args.uiGraph);
|
|
54
|
+
const staticHandler = args.dev ? undefined : createHandler(runtime, args, uiGraph.id);
|
|
55
|
+
warnIfWebAppBearerTokenIsConfigured(args);
|
|
56
|
+
warnIfServerSavesDatasets(args.saveDatasets, 'CLI web-app server');
|
|
57
|
+
const app = new Hono();
|
|
58
|
+
app.use('*', createHttpMiddleware(args));
|
|
59
|
+
app.get('/healthz', (c) => c.json({
|
|
60
|
+
ok: true,
|
|
61
|
+
projectPath: runtime.projectPath,
|
|
62
|
+
uiGraphId: uiGraph.id,
|
|
63
|
+
}));
|
|
64
|
+
app.all('*', async (c) => {
|
|
65
|
+
const startedAt = Date.now();
|
|
66
|
+
try {
|
|
67
|
+
const handler = staticHandler ?? createHandler(await loadProjectRuntime(args.projectFile, args), args, uiGraph.id);
|
|
68
|
+
return handler.handleRequest(c.req.raw);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return jsonErrorResponse(c, error, startedAt, 500);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
app,
|
|
76
|
+
projectFilePath: runtime.projectPath,
|
|
77
|
+
uiGraphName: uiGraph.name,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function createHandler(runtime, args, uiGraphId = resolveUiGraph(runtime.project, args.uiGraph).id) {
|
|
81
|
+
return createRivetWebAppHandler(runtime.project, {
|
|
82
|
+
basePath: args.basePath,
|
|
83
|
+
createProcessorOptions: () => withCliProcessorOptions(runtime, args, {}),
|
|
84
|
+
revisionKey: args.revisionKey,
|
|
85
|
+
uiGraphId,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function throwIfReservedBasePath(basePath) {
|
|
89
|
+
const normalizedBasePath = `/${basePath}`.replace(/\/+/g, '/').replace(/\/$/, '');
|
|
90
|
+
if (normalizedBasePath === '/healthz') {
|
|
91
|
+
throw new Error('--base-path /healthz is reserved for the CLI health endpoint.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function warnIfWebAppBearerTokenIsConfigured(args) {
|
|
95
|
+
if (!args.bearerToken && !process.env.RIVET_CLI_BEARER_TOKEN) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
console.warn(chalk.yellow('serve-app bearer-token auth expects clients to send Authorization headers. Browser navigation to the app HTML cannot add that header by itself; use a reverse proxy or cookie auth for browser-facing deployments.'));
|
|
99
|
+
}
|