@valerypopoff/rivet2-cli 2.0.11 → 2.1.1

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
@@ -6,8 +6,13 @@ Command-line tools for running and serving Rivet 2 projects through the Node run
6
6
 
7
7
  ```bash
8
8
  npx @valerypopoff/rivet2-cli --help
9
+ npx @valerypopoff/rivet2-cli list my-project.rivet-project
10
+ npx @valerypopoff/rivet2-cli inspect my-project.rivet-project
11
+ npx @valerypopoff/rivet2-cli doctor my-project.rivet-project
9
12
  npx @valerypopoff/rivet2-cli run my-project.rivet-project
10
13
  npx @valerypopoff/rivet2-cli serve my-project.rivet-project --port 8080
14
+ npx @valerypopoff/rivet2-cli serve-app my-project.rivet-project "My web app"
15
+ npx @valerypopoff/rivet2-cli completion
11
16
  ```
12
17
 
13
18
  The CLI package exposes the `rivet` binary when installed globally:
@@ -23,6 +28,13 @@ rivet --help
23
28
  yarn workspace @valerypopoff/rivet2-cli run build
24
29
  yarn workspace @valerypopoff/rivet2-cli run test
25
30
  yarn workspace @valerypopoff/rivet2-cli run lint
31
+ yarn workspace @valerypopoff/rivet2-cli run smoke:package
32
+ yarn workspace @valerypopoff/rivet2-cli run smoke:docker
33
+ yarn workspace @valerypopoff/rivet2-cli run verify
26
34
  ```
27
35
 
36
+ `smoke:package` is part of `verify` and checks the generated package contents, direct built binary, and installed `rivet` command. `smoke:docker` is opt-in because it requires a local Docker daemon; it removes its temporary smoke image after the check.
37
+
38
+ Publish the public packages through the repo-root publish script or GitHub workflow, not by running `npm publish` inside `packages/cli`.
39
+
28
40
  See the root [README](../../README.md), [package docs](../../developer-docs/PACKAGES.md), and public CLI docs under [packages/docs/docs/cli.md](../docs/docs/cli.md).
package/bin/cli.js CHANGED
@@ -1,10 +1,36 @@
1
1
  #!/usr/bin/env node
2
+ import chalk from 'chalk';
2
3
  import yargs from 'yargs';
3
4
  import { hideBin } from 'yargs/helpers';
5
+ import { inspect, list, makeInspectCommand, makeListCommand, } from './commands/list.js';
6
+ import { doctor, makeDoctorCommand } from './commands/doctor.js';
4
7
  import { makeCommand as makeRunCommand, run } from './commands/run.js';
5
8
  import { makeCommand as makeServeCommand, serve } from './commands/serve.js';
9
+ import { makeCommand as makeServeAppCommand, serveApp } from './commands/serveApp.js';
6
10
  await yargs(hideBin(process.argv))
7
- .command('run <projectFile> [graphName]', 'Run a graph in a project file, or the main graph if graphName is not specified.', (y) => makeRunCommand(y), (args) => run(args))
8
- .command('serve [projectFile]', 'Serve a project file as a REST API.', (y) => makeServeCommand(y), (args) => serve(args))
11
+ .command('list <projectFile>', 'List graphs, web apps, library nodes, and plugins in a project file.', (y) => makeListCommand(y), (args) => runCli(() => list(args)))
12
+ .command('inspect <projectFile>', 'Print a machine-readable project summary.', (y) => makeInspectCommand(y), (args) => runCli(() => inspect(args.projectFile)))
13
+ .command('doctor <projectFile>', 'Check a project file for common CLI/runtime problems.', (y) => makeDoctorCommand(y), (args) => runCli(() => doctor(args)))
14
+ .command('run <projectFile> [graphName]', 'Run a graph in a project file, or the main graph if graphName is not specified.', (y) => makeRunCommand(y), (args) => runCli(() => run(args)))
15
+ .command('serve [projectFile]', 'Serve a project file as a REST API.', (y) => makeServeCommand(y), (args) => runCli(() => serve(args)))
16
+ .command('serve-app <projectFile> [uiGraph]', 'Serve a project-contained Rivet web app.', (y) => makeServeAppCommand(y), (args) => runCli(() => serveApp(args)))
17
+ .completion('completion', 'Generate a shell completion script.')
9
18
  .demandCommand()
10
19
  .parseAsync();
20
+ async function runCli(action) {
21
+ try {
22
+ await action();
23
+ }
24
+ catch (err) {
25
+ console.error(chalk.red(formatCliError(err)));
26
+ process.exit(1);
27
+ }
28
+ }
29
+ function formatCliError(err) {
30
+ const message = err instanceof Error ? err.message : String(err);
31
+ const stack = err instanceof Error ? err.stack : undefined;
32
+ if (!process.env.RIVET_CLI_DEBUG || !stack) {
33
+ return `Error: ${message}`;
34
+ }
35
+ return `Error: ${message}\n\n${stack}`;
36
+ }
@@ -0,0 +1,228 @@
1
+ import { loadProjectFromFile, NodeDatasetProvider, } from '@valerypopoff/rivet2-node';
2
+ import chalk from 'chalk';
3
+ import didYouMean from 'didyoumean2';
4
+ import { readdir, stat } from 'node:fs/promises';
5
+ import { basename, dirname, extname, join, resolve } from 'node:path';
6
+ export async function loadProjectRuntime(projectFile, datasetOptions) {
7
+ const projectPath = await getProjectFile(projectFile);
8
+ const project = await loadProjectFromFile(projectPath);
9
+ const datasetProvider = await createDatasetProvider(projectPath, datasetOptions);
10
+ return { datasetProvider, project, projectPath };
11
+ }
12
+ export async function createDatasetProvider(projectPath, { datasetFile, requireDatasetFile = false, saveDatasets = false }) {
13
+ const datasetsFilePath = resolveDatasetFilePath(projectPath, datasetFile);
14
+ if (datasetFile) {
15
+ return NodeDatasetProvider.fromDatasetsFile(datasetsFilePath, {
16
+ requireFile: requireDatasetFile,
17
+ save: saveDatasets,
18
+ });
19
+ }
20
+ return NodeDatasetProvider.fromProjectFile(projectPath, {
21
+ requireFile: requireDatasetFile,
22
+ save: saveDatasets,
23
+ });
24
+ }
25
+ export function resolveDatasetFilePath(projectPath, datasetFile) {
26
+ return datasetFile ? resolve(process.cwd(), datasetFile) : projectPath.replace(/\.rivet-project$/i, '.rivet-data');
27
+ }
28
+ export function withRuntimeProcessorOptions(runtime, options) {
29
+ return {
30
+ ...options,
31
+ datasetProvider: runtime.datasetProvider,
32
+ projectPath: runtime.projectPath,
33
+ };
34
+ }
35
+ export function withCliProcessorOptions(runtime, providerOptions, options) {
36
+ return withRuntimeProcessorOptions(runtime, withProviderProcessorOptions(providerOptions, options));
37
+ }
38
+ export function withProviderProcessorOptions(providerOptions, options) {
39
+ const providerSettings = {};
40
+ if (providerOptions.openaiEndpoint != null) {
41
+ providerSettings.openAiEndpoint = providerOptions.openaiEndpoint;
42
+ }
43
+ if (providerOptions.openaiApiKey != null) {
44
+ providerSettings.openAiKey = providerOptions.openaiApiKey;
45
+ }
46
+ if (providerOptions.openaiOrganization != null) {
47
+ providerSettings.openAiOrganization = providerOptions.openaiOrganization;
48
+ }
49
+ return {
50
+ ...options,
51
+ ...providerSettings,
52
+ };
53
+ }
54
+ export function addProviderOptions(y) {
55
+ return y
56
+ .option('openai-api-key', {
57
+ describe: 'The OpenAI API key to use for the project. If omitted, the environment variable OPENAI_API_KEY is used.',
58
+ type: 'string',
59
+ demandOption: false,
60
+ })
61
+ .option('openai-endpoint', {
62
+ describe: 'The OpenAI API endpoint to use for the project. If omitted, the environment variable OPENAI_ENDPOINT is used.',
63
+ type: 'string',
64
+ demandOption: false,
65
+ })
66
+ .option('openai-organization', {
67
+ describe: 'The OpenAI organization to use for the project. If omitted, the environment variable OPENAI_ORGANIZATION is used.',
68
+ type: 'string',
69
+ demandOption: false,
70
+ });
71
+ }
72
+ export function addDatasetOptions(y, { includeSaveDatasets = true } = {}) {
73
+ let result = y.option('dataset-file', {
74
+ describe: 'Use a specific .rivet-data file instead of the project-adjacent default',
75
+ type: 'string',
76
+ });
77
+ if (includeSaveDatasets) {
78
+ result = result.option('save-datasets', {
79
+ describe: 'Persist dataset mutations back to the dataset file',
80
+ type: 'boolean',
81
+ default: false,
82
+ });
83
+ }
84
+ return result.option('require-dataset-file', {
85
+ describe: 'Fail if the dataset file does not exist',
86
+ type: 'boolean',
87
+ default: false,
88
+ });
89
+ }
90
+ export function warnIfServerSavesDatasets(saveDatasets, label = 'CLI server') {
91
+ if (!saveDatasets) {
92
+ return;
93
+ }
94
+ console.warn(chalk.yellow(`--save-datasets persists mutations from this ${label} process. Avoid concurrent production writes; use a wrapper with explicit write policy for shared deployments.`));
95
+ }
96
+ export function getGraphSummaries(project) {
97
+ return Object.values(project.graphs).map((graph) => ({
98
+ id: graph.metadata.id,
99
+ name: graph.metadata.name,
100
+ }));
101
+ }
102
+ export function findGraph(project, graphIdOrName) {
103
+ if (!graphIdOrName) {
104
+ return undefined;
105
+ }
106
+ return getGraphSummaries(project).find((graph) => graph.id === graphIdOrName || graph.name === graphIdOrName);
107
+ }
108
+ export function formatGraphList(project) {
109
+ return getGraphSummaries(project)
110
+ .map((graph) => `- "${graph.name}" (${graph.id})`)
111
+ .join('\n');
112
+ }
113
+ export function resolveServedGraphName(project, graph) {
114
+ const servedGraph = findGraph(project, graph ?? project.metadata.mainGraphId);
115
+ if (!servedGraph) {
116
+ throw new Error(`Project main graph "${project.metadata.mainGraphId}" was not found in the project file.`);
117
+ }
118
+ return servedGraph.name;
119
+ }
120
+ export function throwIfNoMainGraph(project, graph, projectFilePath, argumentStyle = 'option') {
121
+ if (project.metadata.mainGraphId || graph) {
122
+ return;
123
+ }
124
+ const validGraphs = getGraphSummaries(project);
125
+ if (validGraphs.length === 0) {
126
+ throw new Error('No graphs found in the project file. Please edit the project file in Rivet and add a graph.');
127
+ }
128
+ const firstExample = formatGraphSelectionExample(projectFilePath, validGraphs[0].id, argumentStyle);
129
+ const secondExample = formatGraphSelectionExample(projectFilePath, `"${validGraphs[0].name}"`, argumentStyle);
130
+ 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)}\``);
131
+ }
132
+ export function throwIfInvalidGraph(project, graph, argumentStyle = 'option') {
133
+ if (!graph) {
134
+ const mainGraphId = project.metadata.mainGraphId;
135
+ if (!mainGraphId || findGraph(project, mainGraphId)) {
136
+ return;
137
+ }
138
+ throw new Error(`Project main graph "${mainGraphId}" was not found in the project file.`);
139
+ }
140
+ if (findGraph(project, graph)) {
141
+ return;
142
+ }
143
+ const validGraphsAndIds = getGraphSummaries(project).flatMap((validGraph) => [validGraph.id, validGraph.name]);
144
+ const suggestion = didYouMean(graph, validGraphsAndIds);
145
+ if (suggestion) {
146
+ const suggestedArgument = formatGraphArgument(suggestion, argumentStyle);
147
+ throw new Error(`Graph "${graph}" not found in project file. Did you mean \`${chalk.bold(suggestedArgument)}\`?`);
148
+ }
149
+ throw new Error(`Graph "${graph}" not found in project file. Valid graphs are: \n${formatGraphList(project)}`);
150
+ }
151
+ function formatGraphSelectionExample(projectFilePath, graph, argumentStyle) {
152
+ return argumentStyle === 'positional'
153
+ ? `rivet run ${projectFilePath} ${graph}`
154
+ : `rivet serve ${projectFilePath} --graph ${graph}`;
155
+ }
156
+ function formatGraphArgument(graph, argumentStyle) {
157
+ return argumentStyle === 'positional' ? `"${graph}"` : `--graph "${graph}"`;
158
+ }
159
+ export function getUiGraphSummaries(project) {
160
+ return Object.values(project.uiGraphs ?? {}).map((uiGraph) => ({
161
+ id: uiGraph.id,
162
+ name: uiGraph.name,
163
+ }));
164
+ }
165
+ export function resolveUiGraph(project, uiGraphIdOrName) {
166
+ const uiGraphs = Object.values(project.uiGraphs ?? {});
167
+ if (uiGraphIdOrName) {
168
+ const uiGraph = uiGraphs.find((candidate) => candidate.id === uiGraphIdOrName || candidate.name === uiGraphIdOrName);
169
+ if (uiGraph) {
170
+ return uiGraph;
171
+ }
172
+ const validUiGraphs = getUiGraphSummaries(project).flatMap((uiGraph) => [uiGraph.id, uiGraph.name]);
173
+ const suggestion = didYouMean(uiGraphIdOrName, validUiGraphs);
174
+ const suffix = suggestion ? ` Did you mean "${suggestion}"?` : '';
175
+ throw new Error(`Web app "${uiGraphIdOrName}" not found in project file.${suffix}`);
176
+ }
177
+ if (uiGraphs.length === 1) {
178
+ return uiGraphs[0];
179
+ }
180
+ if (uiGraphs.length === 0) {
181
+ throw new Error('No Rivet web apps found in the project file.');
182
+ }
183
+ const validUiGraphs = getUiGraphSummaries(project)
184
+ .map((uiGraph) => `- "${uiGraph.name}" (${uiGraph.id})`)
185
+ .join('\n');
186
+ throw new Error(`Multiple Rivet web apps found. Specify one by name or ID:\n${validUiGraphs}`);
187
+ }
188
+ export async function getProjectFile(initialProjectFilePath) {
189
+ let projectFilePath = resolve(process.cwd(), initialProjectFilePath ?? (await getProjectFilePathFromDirectory(process.cwd())));
190
+ await throwIfMissingFile(projectFilePath);
191
+ if ((await stat(projectFilePath)).isDirectory()) {
192
+ projectFilePath = await getProjectFilePathFromDirectory(projectFilePath);
193
+ }
194
+ return projectFilePath;
195
+ }
196
+ async function getProjectFilePathFromDirectory(directory) {
197
+ const files = await readdir(directory);
198
+ const projectFiles = files.filter((file) => extname(file).toLowerCase() === '.rivet-project');
199
+ if (projectFiles.length === 0) {
200
+ throw new Error('No project file found in the current directory. Project files should end with .rivet-project.');
201
+ }
202
+ if (projectFiles.length > 1) {
203
+ throw new Error(`Multiple project files found in the current directory. Please specify which one to serve: \n${projectFiles.join('\n')}`);
204
+ }
205
+ return join(directory, projectFiles[0]);
206
+ }
207
+ async function throwIfMissingFile(filePath) {
208
+ try {
209
+ await stat(filePath);
210
+ }
211
+ catch (err) {
212
+ if (err.code !== 'ENOENT') {
213
+ throw err;
214
+ }
215
+ let possibleFiles = [];
216
+ try {
217
+ possibleFiles = await readdir(dirname(filePath));
218
+ }
219
+ catch {
220
+ throw new Error(`Could not find project file "${filePath}".`);
221
+ }
222
+ const suggestion = didYouMean(basename(filePath), possibleFiles);
223
+ if (suggestion) {
224
+ throw new Error(`Could not find project file "${filePath}". Did you mean "${join(dirname(filePath), suggestion)}"?`);
225
+ }
226
+ throw new Error(`Could not find project file "${filePath}".`);
227
+ }
228
+ }
@@ -1,3 +1,5 @@
1
+ import { jsonValueToDataValue } from '@valerypopoff/rivet2-node';
2
+ import { readFile } from 'node:fs/promises';
1
3
  function isInputRecord(value) {
2
4
  return typeof value === 'object' && value !== null && !Array.isArray(value);
3
5
  }
@@ -16,7 +18,7 @@ export function parseJsonInputRecord(text, source) {
16
18
  if (!isInputRecord(parsed)) {
17
19
  throw new Error(`${source} must be a JSON object.`);
18
20
  }
19
- return parsed;
21
+ return Object.fromEntries(Object.entries(parsed).map(([key, value]) => [key, jsonValueToInputValue(value)]));
20
22
  }
21
23
  export function parseKeyValueInputRecord(values, source) {
22
24
  return Object.fromEntries(values.map((item) => {
@@ -31,3 +33,37 @@ export function parseKeyValueInputRecord(values, source) {
31
33
  return [key, item.slice(separatorIndex + 1)];
32
34
  }));
33
35
  }
36
+ export function parseJsonKeyValueInputRecord(values, source) {
37
+ return Object.fromEntries(values.map((item) => {
38
+ const separatorIndex = item.indexOf('=');
39
+ if (separatorIndex <= 0) {
40
+ throw new Error(`Invalid ${source} value "${item}". Expected key=json.`);
41
+ }
42
+ const key = item.slice(0, separatorIndex).trim();
43
+ if (!key) {
44
+ throw new Error(`Invalid ${source} value "${item}". Expected a non-empty key.`);
45
+ }
46
+ const json = item.slice(separatorIndex + 1);
47
+ try {
48
+ return [key, jsonValueToInputValue(JSON.parse(json))];
49
+ }
50
+ catch (error) {
51
+ throw new Error(`Invalid ${source} value "${item}". Expected valid JSON after "=".`, { cause: error });
52
+ }
53
+ }));
54
+ }
55
+ export async function parseJsonInputRecordFromFile(filePath, source) {
56
+ return parseJsonInputRecord(await readFile(filePath, 'utf8'), source);
57
+ }
58
+ export function throwIfConflictingInputSources(sources, sourceKind) {
59
+ const enabledSources = sources.filter((source) => source.enabled).map((source) => source.name);
60
+ if (enabledSources.length > 1) {
61
+ throw new Error(`Use only one ${sourceKind} source. Received: ${enabledSources.join(', ')}.`);
62
+ }
63
+ }
64
+ function jsonValueToInputValue(value) {
65
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
66
+ return value;
67
+ }
68
+ return jsonValueToDataValue(value);
69
+ }
@@ -0,0 +1,188 @@
1
+ import { loadProjectFromFile } from '@valerypopoff/rivet2-node';
2
+ import { access } from 'node:fs/promises';
3
+ import { addDatasetOptions, findGraph, getGraphSummaries, getProjectFile, getUiGraphSummaries, resolveDatasetFilePath, } from '../cliRuntime.js';
4
+ export function makeDoctorCommand(y) {
5
+ const command = y
6
+ .positional('projectFile', {
7
+ describe: 'The project file to check',
8
+ type: 'string',
9
+ demandOption: true,
10
+ })
11
+ .option('json', {
12
+ describe: 'Print machine-readable JSON',
13
+ type: 'boolean',
14
+ default: false,
15
+ });
16
+ return addDatasetOptions(command, { includeSaveDatasets: false });
17
+ }
18
+ export async function doctor(args) {
19
+ const report = await buildDoctorReport(args.projectFile, args);
20
+ console.log(args.json ? JSON.stringify(report, null, 2) : formatDoctorReport(report));
21
+ if (!report.ok) {
22
+ process.exitCode = 1;
23
+ }
24
+ }
25
+ export async function buildDoctorReport(projectFile, options = {}) {
26
+ const projectPath = await getProjectFile(projectFile);
27
+ const project = await loadProjectFromFile(projectPath);
28
+ const checks = [
29
+ buildProjectCheck(project),
30
+ buildGraphCheck(project),
31
+ buildMainGraphCheck(project),
32
+ buildWebAppCheck(project),
33
+ buildNodeLibraryCheck(project),
34
+ buildPluginCheck(project),
35
+ buildProjectReferenceCheck(project.references ?? []),
36
+ await buildDatasetCheck(projectPath, options),
37
+ ];
38
+ const warnings = checks.filter((check) => check.status === 'warning').length;
39
+ const errors = checks.filter((check) => check.status === 'error').length;
40
+ return {
41
+ checks,
42
+ ok: errors === 0,
43
+ path: projectPath,
44
+ summary: {
45
+ errors,
46
+ warnings,
47
+ },
48
+ };
49
+ }
50
+ function buildProjectCheck(project) {
51
+ return {
52
+ id: 'project-file',
53
+ message: `Loaded project "${project.metadata.title}" (${project.metadata.id}).`,
54
+ status: 'ok',
55
+ };
56
+ }
57
+ function buildGraphCheck(project) {
58
+ const graphs = getGraphSummaries(project);
59
+ return graphs.length > 0
60
+ ? {
61
+ id: 'graphs',
62
+ message: `Found ${formatCount(graphs.length, 'graph')}.`,
63
+ status: 'ok',
64
+ }
65
+ : {
66
+ id: 'graphs',
67
+ message: 'No workflow graphs found.',
68
+ status: 'error',
69
+ };
70
+ }
71
+ function buildMainGraphCheck(project) {
72
+ const mainGraphId = project.metadata.mainGraphId;
73
+ if (!mainGraphId) {
74
+ return {
75
+ id: 'main-graph',
76
+ message: 'No main graph is set. Commands must specify a graph explicitly.',
77
+ status: 'warning',
78
+ };
79
+ }
80
+ const mainGraph = findGraph(project, mainGraphId);
81
+ return mainGraph
82
+ ? {
83
+ id: 'main-graph',
84
+ message: `Main graph resolves to "${mainGraph.name}" (${mainGraph.id}).`,
85
+ status: 'ok',
86
+ }
87
+ : {
88
+ id: 'main-graph',
89
+ message: `Main graph "${mainGraphId}" is set but does not exist.`,
90
+ status: 'error',
91
+ };
92
+ }
93
+ function buildWebAppCheck(project) {
94
+ const uiGraphs = getUiGraphSummaries(project);
95
+ return {
96
+ id: 'web-apps',
97
+ message: uiGraphs.length > 0
98
+ ? `Found ${formatCount(uiGraphs.length, 'Rivet web app')}.`
99
+ : 'No Rivet web apps found.',
100
+ status: 'ok',
101
+ };
102
+ }
103
+ function buildNodeLibraryCheck(project) {
104
+ const libraryNodes = Object.values(project.nodePrefabs ?? {});
105
+ return {
106
+ id: 'node-library',
107
+ message: libraryNodes.length > 0
108
+ ? `Found ${formatCount(libraryNodes.length, 'Node library item')}.`
109
+ : 'No Node library items found.',
110
+ status: 'ok',
111
+ };
112
+ }
113
+ function buildPluginCheck(project) {
114
+ const plugins = project.plugins ?? [];
115
+ return {
116
+ id: 'plugins',
117
+ message: plugins.length > 0
118
+ ? `Project declares ${formatCount(plugins.length, 'plugin')}: ${plugins.map(formatPlugin).join(', ')}.`
119
+ : 'No plugins declared.',
120
+ status: 'ok',
121
+ };
122
+ }
123
+ function buildProjectReferenceCheck(references) {
124
+ const missingHints = references.filter((reference) => !reference.hintPaths || reference.hintPaths.length === 0);
125
+ if (references.length === 0) {
126
+ return {
127
+ id: 'project-references',
128
+ message: 'No project references declared.',
129
+ status: 'ok',
130
+ };
131
+ }
132
+ if (missingHints.length > 0) {
133
+ return {
134
+ id: 'project-references',
135
+ message: `${formatCount(missingHints.length, 'project reference')} has no hint paths. Runtime resolution may depend on the host loader.`,
136
+ status: 'warning',
137
+ };
138
+ }
139
+ return {
140
+ id: 'project-references',
141
+ message: `Found ${formatCount(references.length, 'project reference')} with hint paths.`,
142
+ status: 'ok',
143
+ };
144
+ }
145
+ async function buildDatasetCheck(projectPath, options) {
146
+ const datasetPath = resolveDatasetFilePath(projectPath, options.datasetFile);
147
+ if (await fileExists(datasetPath)) {
148
+ return {
149
+ id: 'dataset-file',
150
+ message: `Dataset file found at ${datasetPath}.`,
151
+ status: 'ok',
152
+ };
153
+ }
154
+ return {
155
+ id: 'dataset-file',
156
+ message: `Dataset file not found at ${datasetPath}; runs will start without datasets.`,
157
+ status: options.requireDatasetFile ? 'error' : 'ok',
158
+ };
159
+ }
160
+ async function fileExists(path) {
161
+ try {
162
+ await access(path);
163
+ return true;
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ }
169
+ function formatDoctorReport(report) {
170
+ const status = report.ok ? 'OK' : 'Needs attention';
171
+ const lines = [
172
+ 'Rivet project doctor',
173
+ `Path: ${report.path}`,
174
+ `Status: ${status} (${report.summary.errors} errors, ${report.summary.warnings} warnings)`,
175
+ '',
176
+ ...report.checks.map((check) => `${formatCheckStatus(check.status)} ${check.message}`),
177
+ ];
178
+ return lines.join('\n');
179
+ }
180
+ function formatCheckStatus(status) {
181
+ return status === 'ok' ? '[ok] ' : status === 'warning' ? '[warn]' : '[err] ';
182
+ }
183
+ function formatCount(count, singular) {
184
+ return `${count} ${singular}${count === 1 ? '' : 's'}`;
185
+ }
186
+ function formatPlugin(plugin) {
187
+ return typeof plugin === 'string' ? plugin : JSON.stringify(plugin);
188
+ }
@@ -0,0 +1,81 @@
1
+ import { loadProjectFromFile } from '@valerypopoff/rivet2-node';
2
+ import { getGraphSummaries, getProjectFile, getUiGraphSummaries } from '../cliRuntime.js';
3
+ export function makeListCommand(y) {
4
+ return y
5
+ .positional('projectFile', {
6
+ describe: 'The project file to inspect',
7
+ type: 'string',
8
+ demandOption: true,
9
+ })
10
+ .option('json', {
11
+ describe: 'Print machine-readable JSON',
12
+ type: 'boolean',
13
+ default: false,
14
+ });
15
+ }
16
+ export function makeInspectCommand(y) {
17
+ return y.positional('projectFile', {
18
+ describe: 'The project file to inspect',
19
+ type: 'string',
20
+ demandOption: true,
21
+ });
22
+ }
23
+ export async function list(args) {
24
+ const summary = await inspectProject(args.projectFile);
25
+ console.log(args.json ? JSON.stringify(summary, null, 2) : formatProjectInspection(summary));
26
+ }
27
+ export async function inspect(projectFile) {
28
+ console.log(JSON.stringify(await inspectProject(projectFile), null, 2));
29
+ }
30
+ export async function inspectProject(projectFile) {
31
+ const projectPath = await getProjectFile(projectFile);
32
+ const project = await loadProjectFromFile(projectPath);
33
+ return buildProjectInspection(project, projectPath);
34
+ }
35
+ export function buildProjectInspection(project, projectPath) {
36
+ const mainGraphId = project.metadata.mainGraphId;
37
+ return {
38
+ graphs: getGraphSummaries(project).map((graph) => ({
39
+ ...graph,
40
+ main: graph.id === mainGraphId,
41
+ nodes: project.graphs[graph.id]?.nodes.length ?? 0,
42
+ })),
43
+ libraryNodes: Object.values(project.nodePrefabs ?? {}).map((prefab) => ({
44
+ id: prefab.id,
45
+ nodeType: prefab.sourceNode.type,
46
+ title: prefab.sourceNode.title,
47
+ })),
48
+ path: projectPath,
49
+ plugins: project.plugins ?? [],
50
+ project: {
51
+ description: project.metadata.description,
52
+ id: project.metadata.id,
53
+ mainGraphId,
54
+ title: project.metadata.title,
55
+ },
56
+ webApps: getUiGraphSummaries(project).map((uiGraph) => ({
57
+ ...uiGraph,
58
+ components: project.uiGraphs?.[uiGraph.id]?.components.length ?? 0,
59
+ })),
60
+ };
61
+ }
62
+ function formatProjectInspection(summary) {
63
+ const lines = [
64
+ `Project: ${summary.project.title} (${summary.project.id})`,
65
+ `Path: ${summary.path}`,
66
+ `Main graph: ${formatMainGraph(summary)}`,
67
+ '',
68
+ formatSection('Graphs', summary.graphs.map((graph) => `${graph.main ? '* ' : '- '}${graph.name} (${graph.id}) - ${graph.nodes} nodes`)),
69
+ formatSection('Web apps', summary.webApps.map((webApp) => `- ${webApp.name} (${webApp.id}) - ${webApp.components} components`)),
70
+ formatSection('Node library', summary.libraryNodes.map((node) => `- ${node.title} (${node.id}) - ${node.nodeType}`)),
71
+ formatSection('Plugins', summary.plugins.map((plugin) => `- ${typeof plugin === 'string' ? plugin : JSON.stringify(plugin)}`)),
72
+ ];
73
+ return lines.filter(Boolean).join('\n');
74
+ }
75
+ function formatMainGraph(summary) {
76
+ const mainGraph = summary.graphs.find((graph) => graph.main);
77
+ return mainGraph ? `${mainGraph.name} (${mainGraph.id})` : summary.project.mainGraphId ?? 'Not set';
78
+ }
79
+ function formatSection(title, lines) {
80
+ return lines.length > 0 ? `${title}:\n${lines.join('\n')}` : `${title}: none`;
81
+ }