@quantum-hub/qhubctl 2.2.2-next.1 → 2.2.2
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 +4 -2
- package/dist/commands/compress.d.ts +1 -1
- package/dist/commands/compress.js +15 -15
- package/dist/commands/datapool-upload.d.ts +1 -1
- package/dist/commands/datapool-upload.js +35 -35
- package/dist/commands/get-context.d.ts +1 -1
- package/dist/commands/get-context.js +5 -5
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +33 -29
- package/dist/commands/list-contexts.d.ts +1 -1
- package/dist/commands/list-contexts.js +21 -13
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +16 -16
- package/dist/commands/logout.d.ts +1 -1
- package/dist/commands/logout.js +10 -10
- package/dist/commands/openapi.d.ts +1 -1
- package/dist/commands/openapi.js +112 -94
- package/dist/commands/run.d.ts +2 -2
- package/dist/commands/run.js +37 -34
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +30 -19
- package/dist/commands/services-build-logs.d.ts +2 -2
- package/dist/commands/services-build-logs.js +26 -26
- package/dist/commands/services-build-status.d.ts +1 -1
- package/dist/commands/services-build-status.js +21 -21
- package/dist/commands/services-list.d.ts +1 -1
- package/dist/commands/services-list.js +20 -17
- package/dist/commands/set-context.d.ts +1 -1
- package/dist/commands/set-context.js +18 -16
- package/dist/commands/up.d.ts +1 -1
- package/dist/commands/up.js +50 -48
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/command.js +2 -2
- package/dist/lib/config-dir.js +3 -3
- package/dist/lib/debug.js +1 -1
- package/dist/lib/entrypoint-resolver.js +15 -15
- package/dist/lib/error.js +4 -4
- package/dist/lib/fetch.js +3 -1
- package/dist/lib/init-user-input.d.ts +1 -1
- package/dist/lib/init-user-input.js +28 -26
- package/dist/lib/output.d.ts +2 -2
- package/dist/lib/output.js +3 -3
- package/dist/lib/random-name.js +147 -147
- package/dist/lib/random-string.js +2 -2
- package/dist/lib/repository.js +5 -5
- package/dist/lib/stream-to-blob.d.ts +1 -1
- package/dist/lib/stream-to-blob.js +5 -5
- package/dist/lib/up/get-description.js +8 -8
- package/dist/lib/up/ignore.js +8 -8
- package/dist/lib/up/qhubignore-help.js +10 -10
- package/dist/lib/up/required-files.js +17 -12
- package/dist/lib/up/source.d.ts +2 -2
- package/dist/lib/up/source.js +7 -7
- package/dist/lib/up/zip.js +8 -8
- package/dist/main.js +3 -3
- package/dist/model/command.d.ts +2 -2
- package/dist/model/command.js +9 -9
- package/dist/model/managed-service-config.d.ts +1 -1
- package/dist/model/user-config.js +1 -1
- package/dist/program.d.ts +1 -1
- package/dist/program.js +24 -24
- package/dist/service/auth-service.d.ts +3 -3
- package/dist/service/auth-service.js +11 -9
- package/dist/service/command-service.d.ts +1 -1
- package/dist/service/quantum-hub-service.d.ts +3 -3
- package/dist/service/quantum-hub-service.js +34 -20
- package/dist/service/service-config-service.d.ts +1 -1
- package/dist/service/service-config-service.js +8 -8
- package/dist/service/user-config-service.d.ts +1 -1
- package/dist/service/user-config-service.js +7 -7
- package/dist/service/user-service.d.ts +2 -2
- package/dist/service/user-service.js +4 -4
- package/package.json +8 -2
package/dist/commands/openapi.js
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
import { confirm } from
|
|
2
|
-
import { Command } from
|
|
3
|
-
import fs from
|
|
4
|
-
import path from
|
|
5
|
-
import { spawnSync } from
|
|
6
|
-
import { executeAsyncCommand } from
|
|
7
|
-
import { EntrypointResolver } from
|
|
8
|
-
import { randomString } from
|
|
9
|
-
import { error, spinner, stdout, warn } from
|
|
10
|
-
import { Runtime } from
|
|
11
|
-
import { readServiceConfig } from
|
|
12
|
-
const SUPPORTED_FORMATS = [
|
|
13
|
-
const DEFAULT_OUTPUT_FILE =
|
|
14
|
-
const DEPRECATED_FILE_NAME =
|
|
1
|
+
import { confirm } from "@inquirer/prompts";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { executeAsyncCommand } from "../lib/command.js";
|
|
7
|
+
import { EntrypointResolver } from "../lib/entrypoint-resolver.js";
|
|
8
|
+
import { randomString } from "../lib/random-string.js";
|
|
9
|
+
import { error, spinner, stdout, warn } from "../lib/output.js";
|
|
10
|
+
import { Runtime } from "../model/managed-service-config.js";
|
|
11
|
+
import { readServiceConfig } from "../service/service-config-service.js";
|
|
12
|
+
const SUPPORTED_FORMATS = ["yaml"];
|
|
13
|
+
const DEFAULT_OUTPUT_FILE = "openapi.yaml";
|
|
14
|
+
const DEPRECATED_FILE_NAME = "openapi-spec.yml";
|
|
15
15
|
function getErrorMessage(err) {
|
|
16
|
-
if (typeof err ===
|
|
17
|
-
|
|
16
|
+
if (typeof err === "object" &&
|
|
17
|
+
err !== null &&
|
|
18
|
+
"message" in err &&
|
|
19
|
+
typeof err.message === "string") {
|
|
18
20
|
return err.message;
|
|
19
21
|
}
|
|
20
|
-
if (typeof err ===
|
|
22
|
+
if (typeof err === "string")
|
|
21
23
|
return err;
|
|
22
24
|
if (err instanceof Error)
|
|
23
25
|
return err.message;
|
|
24
|
-
return
|
|
26
|
+
return "An unknown error occurred";
|
|
25
27
|
}
|
|
26
28
|
function checkDeprecatedFiles() {
|
|
27
29
|
const deprecatedFile = path.join(process.cwd(), DEPRECATED_FILE_NAME);
|
|
@@ -40,7 +42,9 @@ async function determineOutputPath(opts) {
|
|
|
40
42
|
fs.mkdirpSync(outputDir);
|
|
41
43
|
}
|
|
42
44
|
catch (err) {
|
|
43
|
-
throw new Error(`Cannot create output directory: ${outputDir}. ${getErrorMessage(err)}`, {
|
|
45
|
+
throw new Error(`Cannot create output directory: ${outputDir}. ${getErrorMessage(err)}`, {
|
|
46
|
+
cause: err,
|
|
47
|
+
});
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
}
|
|
@@ -50,7 +54,7 @@ async function determineOutputPath(opts) {
|
|
|
50
54
|
default: false,
|
|
51
55
|
});
|
|
52
56
|
if (!shouldOverwrite) {
|
|
53
|
-
const newPath = path.join(path.dirname(outputPath), `${path.basename(outputPath,
|
|
57
|
+
const newPath = path.join(path.dirname(outputPath), `${path.basename(outputPath, ".yaml")}-${randomString(5)}.yaml`);
|
|
54
58
|
stdout(`Creating new file: ${path.basename(newPath)}`);
|
|
55
59
|
return newPath;
|
|
56
60
|
}
|
|
@@ -64,7 +68,7 @@ function isProjectSupported(opts) {
|
|
|
64
68
|
}
|
|
65
69
|
catch (err) {
|
|
66
70
|
warn(`Failed to read project configuration: ${getErrorMessage(err)}`);
|
|
67
|
-
warn(
|
|
71
|
+
warn("Ensure you have a valid qhub.json file in your project root.");
|
|
68
72
|
return false;
|
|
69
73
|
}
|
|
70
74
|
const hasExplicitEntrypoint = !!(opts.entrypoint || opts.package || opts.method);
|
|
@@ -73,52 +77,52 @@ function isProjectSupported(opts) {
|
|
|
73
77
|
const isSupported = supportedRuntimes.includes(serviceConfig.runtime);
|
|
74
78
|
if (!isSupported) {
|
|
75
79
|
warn(`Runtime "${serviceConfig.runtime}" is not supported for OpenAPI generation.`);
|
|
76
|
-
warn(
|
|
77
|
-
warn(
|
|
78
|
-
warn(
|
|
79
|
-
warn(
|
|
80
|
-
warn(
|
|
81
|
-
warn(
|
|
82
|
-
warn(
|
|
83
|
-
warn(
|
|
80
|
+
warn("");
|
|
81
|
+
warn("Supported configurations:");
|
|
82
|
+
warn(" \u2022 PYTHON_TEMPLATE: Automatic (requires src/program.py)");
|
|
83
|
+
warn(" \u2022 DOCKER: With explicit entrypoint flags");
|
|
84
|
+
warn("");
|
|
85
|
+
warn("Examples:");
|
|
86
|
+
warn(" qhubctl openapi --entrypoint=myapp.main:handler");
|
|
87
|
+
warn(" qhubctl openapi --package=myapp.core --method=process");
|
|
84
88
|
}
|
|
85
89
|
return isSupported;
|
|
86
90
|
}
|
|
87
|
-
const programFile = path.join(process.cwd(),
|
|
91
|
+
const programFile = path.join(process.cwd(), "src", "program.py");
|
|
88
92
|
const hasProgramFile = fs.existsSync(programFile);
|
|
89
93
|
const isPythonTemplate = serviceConfig.runtime === Runtime.PYTHON_TEMPLATE;
|
|
90
94
|
const isSupported = isPythonTemplate && hasProgramFile;
|
|
91
95
|
if (!isSupported) {
|
|
92
|
-
warn(
|
|
93
|
-
warn(
|
|
96
|
+
warn("Cannot generate OpenAPI description from your code.");
|
|
97
|
+
warn("");
|
|
94
98
|
if (isPythonTemplate && !hasProgramFile) {
|
|
95
|
-
warn(
|
|
96
|
-
warn(
|
|
97
|
-
warn(
|
|
98
|
-
warn(
|
|
99
|
-
warn(
|
|
99
|
+
warn("Missing required file: src/program.py");
|
|
100
|
+
warn("");
|
|
101
|
+
warn("Solutions:");
|
|
102
|
+
warn(" \u2022 Create src/program.py with your main function, or");
|
|
103
|
+
warn(" \u2022 Use explicit entrypoint: --entrypoint=yourmodule.main:function");
|
|
100
104
|
}
|
|
101
105
|
else if (!isPythonTemplate) {
|
|
102
106
|
warn(`Runtime "${serviceConfig.runtime}" requires explicit entrypoint specification.`);
|
|
103
|
-
warn(
|
|
104
|
-
warn(
|
|
105
|
-
warn(
|
|
106
|
-
warn(
|
|
107
|
-
warn(
|
|
108
|
-
warn(
|
|
109
|
-
warn(
|
|
110
|
-
warn(
|
|
107
|
+
warn("");
|
|
108
|
+
warn("Use one of these flags:");
|
|
109
|
+
warn(" --entrypoint=package.module:function");
|
|
110
|
+
warn(" --package=package.module --method=function");
|
|
111
|
+
warn("");
|
|
112
|
+
warn("Examples:");
|
|
113
|
+
warn(" qhubctl openapi --entrypoint=myapp.main:handler");
|
|
114
|
+
warn(" qhubctl openapi --package=myapp.core --method=process");
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
return isSupported;
|
|
114
118
|
}
|
|
115
119
|
function formatPythonError(stderr) {
|
|
116
120
|
const commonErrors = {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
ModuleNotFoundError: "Missing dependency. Install qhub-commons: pip install qhub-commons",
|
|
122
|
+
ImportError: "Cannot import module. Check your entrypoint path and ensure the module exists",
|
|
123
|
+
AttributeError: "Function not found. Check your method name and ensure it exists in the specified module",
|
|
124
|
+
SyntaxError: "Python syntax error in your code. Fix the syntax errors and try again",
|
|
125
|
+
TypeError: "Type error in your function. Ensure proper type annotations for parameters and return values",
|
|
122
126
|
};
|
|
123
127
|
for (const [errorType, helpText] of Object.entries(commonErrors)) {
|
|
124
128
|
if (stderr.includes(errorType))
|
|
@@ -127,36 +131,48 @@ function formatPythonError(stderr) {
|
|
|
127
131
|
return `Failed to generate OpenAPI description: ${stderr}`;
|
|
128
132
|
}
|
|
129
133
|
function validateCommandResult(result) {
|
|
130
|
-
if (typeof result ===
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
if (typeof result === "object" &&
|
|
135
|
+
result !== null &&
|
|
136
|
+
"stdout" in result &&
|
|
137
|
+
"stderr" in result &&
|
|
138
|
+
typeof result.stdout === "string" &&
|
|
139
|
+
typeof result.stderr === "string") {
|
|
134
140
|
const { stdout: out, stderr } = result;
|
|
135
141
|
if (!out.trim()) {
|
|
136
|
-
const detail = stderr.trim()
|
|
142
|
+
const detail = stderr.trim()
|
|
143
|
+
? formatPythonError(stderr)
|
|
144
|
+
: "OpenAPI generation returned empty output. Check your entrypoint and ensure it has proper type annotations.";
|
|
137
145
|
throw new Error(detail);
|
|
138
146
|
}
|
|
139
147
|
return { stdout: out, stderr };
|
|
140
148
|
}
|
|
141
|
-
throw new Error(
|
|
149
|
+
throw new Error("Invalid command result format");
|
|
142
150
|
}
|
|
143
151
|
async function generateFromCode(entrypoint) {
|
|
144
|
-
let title =
|
|
152
|
+
let title = "Service API";
|
|
145
153
|
let description;
|
|
146
154
|
try {
|
|
147
155
|
const serviceConfig = readServiceConfig(process.cwd());
|
|
148
|
-
title = serviceConfig.name ||
|
|
149
|
-
description = serviceConfig.summary ||
|
|
156
|
+
title = serviceConfig.name || "Service API";
|
|
157
|
+
description = serviceConfig.summary || "";
|
|
150
158
|
}
|
|
151
159
|
catch {
|
|
152
160
|
warn(`Could not load qhub.json configuration. Using default title: ${title}`);
|
|
153
161
|
}
|
|
154
|
-
const pySnippet =
|
|
155
|
-
|
|
162
|
+
const pySnippet = "import sys; from qhub.commons.openapi.generator import generate_openapi; " +
|
|
163
|
+
"generate_openapi(entrypoint=sys.argv[1], title=sys.argv[2], description=sys.argv[3] or None)";
|
|
156
164
|
try {
|
|
157
|
-
const result = await executeAsyncCommand(
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
const result = await executeAsyncCommand("uv", [
|
|
166
|
+
"run",
|
|
167
|
+
"--no-sync",
|
|
168
|
+
"--project",
|
|
169
|
+
process.cwd(),
|
|
170
|
+
"python",
|
|
171
|
+
"-c",
|
|
172
|
+
pySnippet,
|
|
173
|
+
entrypoint,
|
|
174
|
+
title,
|
|
175
|
+
description ?? "",
|
|
160
176
|
]);
|
|
161
177
|
const { stdout: out } = validateCommandResult(result);
|
|
162
178
|
return out;
|
|
@@ -173,22 +189,22 @@ Examples:
|
|
|
173
189
|
$ qhubctl openapi --entrypoint=src.program:run
|
|
174
190
|
$ qhubctl openapi --package=src.program --method=run
|
|
175
191
|
$ qhubctl openapi --package=src.program`;
|
|
176
|
-
export const openapiCommand = new Command(
|
|
177
|
-
.description(
|
|
178
|
-
|
|
179
|
-
|
|
192
|
+
export const openapiCommand = new Command("openapi")
|
|
193
|
+
.description("Generates the OpenAPI description for your project based on the parameter and return types of your run() method. " +
|
|
194
|
+
"The output of this command will be used when creating or updating your service. " +
|
|
195
|
+
"Supports PYTHON_TEMPLATE (with src/program.py) and DOCKER runtime (with explicit entrypoint). " +
|
|
180
196
|
'Requires uv and a valid uv project in the workspace. The "qhub-commons" dependency is installed/upgraded automatically as a dependency.')
|
|
181
|
-
.option(
|
|
182
|
-
.option(
|
|
183
|
-
.option(
|
|
184
|
-
.option(
|
|
185
|
-
.option(
|
|
186
|
-
.option(
|
|
187
|
-
.addHelpText(
|
|
197
|
+
.option("-f, --force", "Overwrite the output file if it already exists", false)
|
|
198
|
+
.option("--file <value>", "The file to write the OpenAPI description to")
|
|
199
|
+
.option("--format <value>", `The format to generate the OpenAPI description [possible values: ${SUPPORTED_FORMATS.join(", ")}]`)
|
|
200
|
+
.option("--entrypoint <value>", 'The entrypoint to your program in the format "package.module:function" (default: "src.program:run"). Overrides package and method flags.')
|
|
201
|
+
.option("--package <value>", 'The package/module path to your program (e.g., "src.program" or "my_package.main"). Only used if entrypoint is not provided.')
|
|
202
|
+
.option("--method <value>", 'The method/function name in your program (default: "run"). Only used if entrypoint is not provided.')
|
|
203
|
+
.addHelpText("after", EXAMPLES)
|
|
188
204
|
.action(async (opts) => {
|
|
189
205
|
checkDeprecatedFiles();
|
|
190
206
|
if (opts.format && !SUPPORTED_FORMATS.includes(opts.format)) {
|
|
191
|
-
throw new Error(`Unsupported format: ${opts.format}. Supported formats: ${SUPPORTED_FORMATS.join(
|
|
207
|
+
throw new Error(`Unsupported format: ${opts.format}. Supported formats: ${SUPPORTED_FORMATS.join(", ")}`);
|
|
192
208
|
}
|
|
193
209
|
let entrypoint;
|
|
194
210
|
try {
|
|
@@ -197,48 +213,50 @@ export const openapiCommand = new Command('openapi')
|
|
|
197
213
|
catch (err) {
|
|
198
214
|
throw new Error(`Invalid entrypoint format: ${getErrorMessage(err)}\n\n` +
|
|
199
215
|
'Expected format: "package.module:function"\nExample: "src.program:run"\n\n' +
|
|
200
|
-
|
|
216
|
+
"Use --help for more examples.", { cause: err });
|
|
201
217
|
}
|
|
202
218
|
const outputPath = await determineOutputPath(opts);
|
|
203
219
|
const shouldGenerateFile = isProjectSupported(opts);
|
|
204
220
|
if (!shouldGenerateFile) {
|
|
205
221
|
warn(`Cannot generate OpenAPI description from your code. Supported configurations:\n` +
|
|
206
222
|
' \u2022 PYTHON_TEMPLATE runtime with "./src/program.py" file\n' +
|
|
207
|
-
|
|
223
|
+
" \u2022 DOCKER or PYTHON_TEMPLATE runtimes with explicit entrypoint (--entrypoint, or (--package, --method))");
|
|
208
224
|
return;
|
|
209
225
|
}
|
|
210
226
|
// Ensure qhub-commons is installed (or upgraded) in the project venv
|
|
211
|
-
stdout(
|
|
212
|
-
const addResult = spawnSync(
|
|
227
|
+
stdout("Ensuring qhub-commons is up to date...");
|
|
228
|
+
const addResult = spawnSync("uv", ["add", "--upgrade", "qhub-commons", "--project", process.cwd()], { stdio: "inherit" });
|
|
213
229
|
if (addResult.status !== 0) {
|
|
214
|
-
error(
|
|
215
|
-
|
|
230
|
+
error("Failed to install qhub-commons via uv. " +
|
|
231
|
+
"Make sure uv is installed and the workspace is a valid uv project.");
|
|
216
232
|
}
|
|
217
|
-
const spin = spinner(
|
|
233
|
+
const spin = spinner("Generating OpenAPI description");
|
|
218
234
|
let fileContent;
|
|
219
235
|
try {
|
|
220
236
|
fileContent = await generateFromCode(entrypoint);
|
|
221
237
|
}
|
|
222
238
|
catch (err) {
|
|
223
|
-
spin.fail(
|
|
239
|
+
spin.fail("Failed");
|
|
224
240
|
throw err;
|
|
225
241
|
}
|
|
226
242
|
try {
|
|
227
|
-
fs.writeFileSync(outputPath, fileContent,
|
|
228
|
-
spin.succeed(
|
|
243
|
+
fs.writeFileSync(outputPath, fileContent, "utf8");
|
|
244
|
+
spin.succeed("Done");
|
|
229
245
|
}
|
|
230
246
|
catch (err) {
|
|
231
|
-
spin.fail(
|
|
232
|
-
throw new Error(`Failed to write file ${outputPath}: ${getErrorMessage(err)}`, {
|
|
247
|
+
spin.fail("Failed");
|
|
248
|
+
throw new Error(`Failed to write file ${outputPath}: ${getErrorMessage(err)}`, {
|
|
249
|
+
cause: err,
|
|
250
|
+
});
|
|
233
251
|
}
|
|
234
252
|
const relativePath = path.relative(process.cwd(), outputPath);
|
|
235
|
-
const displayPath = relativePath.startsWith(
|
|
236
|
-
stdout(
|
|
253
|
+
const displayPath = relativePath.startsWith("..") ? outputPath : relativePath;
|
|
254
|
+
stdout("");
|
|
237
255
|
stdout(`OpenAPI description created: ${displayPath}`);
|
|
238
256
|
if (fs.statSync(outputPath).size > 0) {
|
|
239
|
-
stdout(
|
|
240
|
-
stdout(
|
|
241
|
-
stdout(
|
|
242
|
-
stdout(
|
|
257
|
+
stdout("");
|
|
258
|
+
stdout("Next steps:");
|
|
259
|
+
stdout(" \u2022 Review the generated OpenAPI description");
|
|
260
|
+
stdout(" \u2022 Use it to create or update your service");
|
|
243
261
|
}
|
|
244
262
|
});
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import { HubPlatform } from
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { HubPlatform } from "@quantum-hub/qhub-api/platform";
|
|
3
3
|
export interface RunCommandOptions {
|
|
4
4
|
tag: string[];
|
|
5
5
|
storeInput: boolean;
|
package/dist/commands/run.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { confirm } from
|
|
2
|
-
import { Command } from
|
|
3
|
-
import { waitUntil } from
|
|
4
|
-
import fsExtra from
|
|
1
|
+
import { confirm } from "@inquirer/prompts";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { waitUntil } from "async-wait-until";
|
|
4
|
+
import fsExtra from "fs-extra";
|
|
5
5
|
const { readJsonSync } = fsExtra;
|
|
6
|
-
import fs from
|
|
7
|
-
import path from
|
|
8
|
-
import { error, spinner, stdout, warn } from
|
|
9
|
-
import { requireAuth } from
|
|
10
|
-
import { defaultBasePath } from
|
|
11
|
-
import QuantumHubService from
|
|
12
|
-
import { readServiceConfig } from
|
|
13
|
-
import { HubPlatform } from
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { error, spinner, stdout, warn } from "../lib/output.js";
|
|
9
|
+
import { requireAuth } from "../model/command.js";
|
|
10
|
+
import { defaultBasePath } from "../model/user-config.js";
|
|
11
|
+
import QuantumHubService from "../service/quantum-hub-service.js";
|
|
12
|
+
import { readServiceConfig } from "../service/service-config-service.js";
|
|
13
|
+
import { HubPlatform } from "@quantum-hub/qhub-api/platform";
|
|
14
14
|
async function getInputFromFiles(filePaths) {
|
|
15
|
-
const paths = filePaths.split(
|
|
15
|
+
const paths = filePaths.split(",").map((p) => p.trim());
|
|
16
16
|
const mergedInput = {};
|
|
17
17
|
let hasValidFile = false;
|
|
18
18
|
for (const filePath of paths) {
|
|
@@ -22,7 +22,7 @@ async function getInputFromFiles(filePaths) {
|
|
|
22
22
|
}
|
|
23
23
|
if (fs.existsSync(file)) {
|
|
24
24
|
try {
|
|
25
|
-
const content = readJsonSync(file, { encoding:
|
|
25
|
+
const content = readJsonSync(file, { encoding: "utf8" });
|
|
26
26
|
Object.assign(mergedInput, content);
|
|
27
27
|
hasValidFile = true;
|
|
28
28
|
}
|
|
@@ -32,7 +32,7 @@ async function getInputFromFiles(filePaths) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
if (!hasValidFile) {
|
|
35
|
-
const confirmed = await confirm({ message:
|
|
35
|
+
const confirmed = await confirm({ message: "No valid input files found. Run without input?" });
|
|
36
36
|
if (confirmed)
|
|
37
37
|
return {};
|
|
38
38
|
process.exit(0);
|
|
@@ -42,7 +42,7 @@ async function getInputFromFiles(filePaths) {
|
|
|
42
42
|
export function buildCreateJobRequest(serviceId, input, opts) {
|
|
43
43
|
const payload = {
|
|
44
44
|
serviceId,
|
|
45
|
-
tags: [
|
|
45
|
+
tags: ["cli", ...opts.tag],
|
|
46
46
|
input,
|
|
47
47
|
};
|
|
48
48
|
if (opts.storeInput) {
|
|
@@ -59,15 +59,15 @@ Examples:
|
|
|
59
59
|
$ qhubctl run --input-files=./config.json,./overrides.json
|
|
60
60
|
$ qhubctl run --store-input
|
|
61
61
|
$ qhubctl run --tag experiment --tag run-42`;
|
|
62
|
-
export const runCommand = new Command(
|
|
63
|
-
.description(
|
|
64
|
-
.argument(
|
|
65
|
-
.option(
|
|
66
|
-
.option(
|
|
67
|
-
.option(
|
|
68
|
-
.option(
|
|
69
|
-
.option(
|
|
70
|
-
.addHelpText(
|
|
62
|
+
export const runCommand = new Command("run")
|
|
63
|
+
.description("Creates a job execution")
|
|
64
|
+
.argument("[serviceId]", "The ID of the service to run (optional — reads from qhub.json if omitted)")
|
|
65
|
+
.option("-i, --input <value>", "Input as JSON string.")
|
|
66
|
+
.option("--input-files <value>", "Comma-separated paths to files containing input to be merged (default: ./input/data.json,./input/params.json).")
|
|
67
|
+
.option("--store-input", "Persist the input data alongside the job on the server.", false)
|
|
68
|
+
.option("--tag <value>", "Tag to attach to the job. Repeat the flag to attach multiple tags.", (value, previous) => [...previous, value], [])
|
|
69
|
+
.option("--detached", "Executes the job in detached mode, i.e., without waiting for it to finish.", false)
|
|
70
|
+
.addHelpText("after", EXAMPLES)
|
|
71
71
|
.action(async (serviceId, opts) => {
|
|
72
72
|
const userConfig = await requireAuth();
|
|
73
73
|
const hubService = new QuantumHubService(userConfig);
|
|
@@ -81,7 +81,7 @@ export const runCommand = new Command('run')
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
if (!serviceId) {
|
|
84
|
-
error(
|
|
84
|
+
error("Missing service ID. Please provide a service ID as argument or set it in the qhub.json of your project.");
|
|
85
85
|
}
|
|
86
86
|
const service = await hubService.getService(serviceId);
|
|
87
87
|
let input;
|
|
@@ -90,14 +90,17 @@ export const runCommand = new Command('run')
|
|
|
90
90
|
input = JSON.parse(opts.input);
|
|
91
91
|
}
|
|
92
92
|
catch {
|
|
93
|
-
error(
|
|
93
|
+
error("Invalid JSON provided.");
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
else {
|
|
97
|
-
input = await getInputFromFiles(opts.inputFiles ||
|
|
97
|
+
input = await getInputFromFiles(opts.inputFiles || "./input/data.json,./input/params.json");
|
|
98
98
|
}
|
|
99
99
|
let job;
|
|
100
|
-
const payload = buildCreateJobRequest(service.id, input, {
|
|
100
|
+
const payload = buildCreateJobRequest(service.id, input, {
|
|
101
|
+
tag: opts.tag,
|
|
102
|
+
storeInput: opts.storeInput,
|
|
103
|
+
});
|
|
101
104
|
job = await hubService.runJob(payload);
|
|
102
105
|
if (opts.detached) {
|
|
103
106
|
stdout(`Job (${job.id}) created.`);
|
|
@@ -119,24 +122,24 @@ export const runCommand = new Command('run')
|
|
|
119
122
|
// ignore — status printed below
|
|
120
123
|
}
|
|
121
124
|
const basePath = userConfig.endpoint?.basePath || defaultBasePath;
|
|
122
|
-
const dashboardBase = basePath.replace(
|
|
125
|
+
const dashboardBase = basePath.replace("//api.hub.", "//dashboard.hub.");
|
|
123
126
|
const jobDetailsLink = `${dashboardBase}/service-jobs/${job.id}`;
|
|
124
127
|
switch (job.status) {
|
|
125
128
|
case HubPlatform.ServiceJobDto.Status.Cancelled:
|
|
126
|
-
spin.warn(
|
|
129
|
+
spin.warn("Job canceled.");
|
|
127
130
|
stdout(`See details at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007`);
|
|
128
131
|
break;
|
|
129
132
|
case HubPlatform.ServiceJobDto.Status.Failed:
|
|
130
|
-
spin.fail(
|
|
133
|
+
spin.fail("Job failed.");
|
|
131
134
|
stdout(`See details at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007`);
|
|
132
135
|
break;
|
|
133
136
|
case HubPlatform.ServiceJobDto.Status.Succeeded:
|
|
134
|
-
spin.succeed(
|
|
137
|
+
spin.succeed("Job succeeded.");
|
|
135
138
|
stdout(`See result at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007`);
|
|
136
139
|
break;
|
|
137
140
|
default:
|
|
138
141
|
spin.stop();
|
|
139
|
-
stdout(
|
|
142
|
+
stdout("Timeout polling status.");
|
|
140
143
|
stdout(`See status details at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007`);
|
|
141
144
|
}
|
|
142
145
|
});
|
package/dist/commands/serve.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Command } from
|
|
1
|
+
import { Command } from "commander";
|
|
2
2
|
export declare const serveCommand: Command;
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,35 +1,46 @@
|
|
|
1
|
-
import { Command } from
|
|
2
|
-
import { spawnSync } from
|
|
3
|
-
import { error, stdout } from
|
|
4
|
-
const LOG_LEVELS = [
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { error, stdout } from "../lib/output.js";
|
|
4
|
+
const LOG_LEVELS = ["TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL"];
|
|
5
5
|
const EXAMPLES = `
|
|
6
6
|
Examples:
|
|
7
7
|
$ qhubctl serve
|
|
8
8
|
$ qhubctl serve -p 8081
|
|
9
9
|
$ qhubctl serve --workspace /path/to/workspace`;
|
|
10
|
-
export const serveCommand = new Command(
|
|
11
|
-
.description(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.option(
|
|
15
|
-
.option(
|
|
16
|
-
.option(
|
|
17
|
-
.addHelpText(
|
|
10
|
+
export const serveCommand = new Command("serve")
|
|
11
|
+
.description("Runs your project locally via qhub-serve inside the workspace virtual environment, " +
|
|
12
|
+
"exposing the same HTTP endpoints as Kipu Quantum Hub (start execution, check status, " +
|
|
13
|
+
"cancel, retrieve results).")
|
|
14
|
+
.option("-p, --port <number>", "The port on which the local web server accepts requests", "8081")
|
|
15
|
+
.option("--log-level <level>", `Log level for qhub-serve. Possible values: ${LOG_LEVELS.join(", ")}`, "INFO")
|
|
16
|
+
.option("-w, --workspace <path>", "Path to the workspace directory (must be a valid uv project). Defaults to the current directory.")
|
|
17
|
+
.addHelpText("after", EXAMPLES)
|
|
18
18
|
.action(async (opts) => {
|
|
19
19
|
const workspaceDir = opts.workspace ?? process.cwd();
|
|
20
20
|
const port = opts.port;
|
|
21
21
|
const logLevel = opts.logLevel;
|
|
22
22
|
// Step 1: Add (or upgrade) qhub-serve as a dev dependency
|
|
23
|
-
stdout(
|
|
24
|
-
const addResult = spawnSync(
|
|
23
|
+
stdout("Ensuring qhub-serve is up to date...");
|
|
24
|
+
const addResult = spawnSync("uv", ["add", "--dev", "--upgrade", "qhub-serve", "--project", workspaceDir], { stdio: "inherit" });
|
|
25
25
|
if (addResult.status !== 0) {
|
|
26
|
-
error(
|
|
27
|
-
|
|
26
|
+
error("Failed to install qhub-serve via uv. " +
|
|
27
|
+
"Make sure uv is installed and the workspace is a valid uv project.");
|
|
28
28
|
}
|
|
29
29
|
// Step 2: Run qhub-serve inside the workspace venv
|
|
30
|
-
stdout(
|
|
31
|
-
const runResult = spawnSync(
|
|
30
|
+
stdout("\nStarting qhub-serve...");
|
|
31
|
+
const runResult = spawnSync("uv", [
|
|
32
|
+
"run",
|
|
33
|
+
"--project",
|
|
34
|
+
workspaceDir,
|
|
35
|
+
"qhub-serve",
|
|
36
|
+
"--workspace",
|
|
37
|
+
workspaceDir,
|
|
38
|
+
"--port",
|
|
39
|
+
String(port),
|
|
40
|
+
"--log-level",
|
|
41
|
+
logLevel,
|
|
42
|
+
], { stdio: "inherit" });
|
|
32
43
|
if (runResult.status !== 0) {
|
|
33
|
-
error(
|
|
44
|
+
error("qhub-serve exited with a non-zero status code.");
|
|
34
45
|
}
|
|
35
46
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { HubPlatform } from
|
|
2
|
-
import { Command } from
|
|
1
|
+
import { HubPlatform } from "@quantum-hub/qhub-api/platform";
|
|
2
|
+
import { Command } from "commander";
|
|
3
3
|
export declare function formatLogEntry(entry: HubPlatform.LogEntry, opts: {
|
|
4
4
|
isTTY: boolean;
|
|
5
5
|
}): string;
|