@quantum-hub/qhubctl 1.2.0 → 1.3.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.
Files changed (77) hide show
  1. package/README.md +136 -20
  2. package/dist/client/kqh/models/UpdateWorkflowServiceRequest.js +0 -11
  3. package/dist/commands/compress.d.ts +2 -0
  4. package/dist/commands/compress.js +23 -0
  5. package/dist/commands/datapool-upload.d.ts +2 -0
  6. package/dist/commands/datapool-upload.js +184 -0
  7. package/dist/commands/get-context.d.ts +2 -0
  8. package/dist/commands/get-context.js +12 -0
  9. package/dist/commands/init.d.ts +2 -0
  10. package/dist/commands/init.js +66 -0
  11. package/dist/commands/list-contexts.d.ts +2 -0
  12. package/dist/commands/list-contexts.js +28 -0
  13. package/dist/commands/login.d.ts +2 -0
  14. package/dist/commands/login.js +59 -0
  15. package/dist/commands/logout.d.ts +2 -0
  16. package/dist/commands/logout.js +20 -0
  17. package/dist/commands/openapi.d.ts +2 -0
  18. package/dist/commands/openapi.js +244 -0
  19. package/dist/commands/run.d.ts +2 -0
  20. package/dist/commands/run.js +126 -0
  21. package/dist/commands/serve.d.ts +2 -0
  22. package/dist/commands/serve.js +35 -0
  23. package/dist/commands/services.d.ts +2 -0
  24. package/dist/commands/services.js +48 -0
  25. package/dist/commands/set-context.d.ts +2 -0
  26. package/dist/commands/set-context.js +52 -0
  27. package/dist/commands/up.d.ts +2 -0
  28. package/dist/commands/up.js +161 -0
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/lib/config-dir.d.ts +6 -0
  32. package/dist/lib/config-dir.js +8 -0
  33. package/dist/lib/entrypoint-resolver.js +2 -2
  34. package/dist/lib/output.d.ts +14 -0
  35. package/dist/lib/output.js +22 -0
  36. package/dist/main.d.ts +2 -0
  37. package/dist/main.js +8 -0
  38. package/dist/model/command.d.ts +14 -11
  39. package/dist/model/command.js +44 -40
  40. package/dist/program.d.ts +2 -0
  41. package/dist/program.js +47 -0
  42. package/dist/service/command-service.d.ts +1 -5
  43. package/dist/service/command-service.js +1 -5
  44. package/dist/service/quantum-hub-service.d.ts +1 -3
  45. package/dist/service/quantum-hub-service.js +2 -2
  46. package/package.json +31 -62
  47. package/bin/dev.cmd +0 -3
  48. package/bin/dev.js +0 -17
  49. package/bin/run.cmd +0 -3
  50. package/bin/run.js +0 -12
  51. package/dist/commands/compress/index.d.ts +0 -6
  52. package/dist/commands/compress/index.js +0 -25
  53. package/dist/commands/datapool/upload.d.ts +0 -23
  54. package/dist/commands/datapool/upload.js +0 -260
  55. package/dist/commands/get-context/index.d.ts +0 -6
  56. package/dist/commands/get-context/index.js +0 -10
  57. package/dist/commands/init/index.d.ts +0 -10
  58. package/dist/commands/init/index.js +0 -75
  59. package/dist/commands/list-contexts/index.d.ts +0 -9
  60. package/dist/commands/list-contexts/index.js +0 -30
  61. package/dist/commands/login/index.d.ts +0 -10
  62. package/dist/commands/login/index.js +0 -62
  63. package/dist/commands/logout/index.d.ts +0 -6
  64. package/dist/commands/logout/index.js +0 -18
  65. package/dist/commands/openapi/index.d.ts +0 -26
  66. package/dist/commands/openapi/index.js +0 -307
  67. package/dist/commands/run/index.d.ts +0 -18
  68. package/dist/commands/run/index.js +0 -150
  69. package/dist/commands/serve/index.d.ts +0 -11
  70. package/dist/commands/serve/index.js +0 -51
  71. package/dist/commands/services/index.d.ts +0 -14
  72. package/dist/commands/services/index.js +0 -65
  73. package/dist/commands/set-context/index.d.ts +0 -17
  74. package/dist/commands/set-context/index.js +0 -59
  75. package/dist/commands/up/index.d.ts +0 -19
  76. package/dist/commands/up/index.js +0 -190
  77. package/oclif.manifest.json +0 -548
@@ -1,307 +0,0 @@
1
- import { confirm } from "@inquirer/prompts";
2
- import { Flags, ux } from '@oclif/core';
3
- import fs from "fs-extra";
4
- import path from "node:path";
5
- import { executeAsyncCommand } from "../../lib/command.js";
6
- import { EntrypointResolver } from "../../lib/entrypoint-resolver.js";
7
- import { fetchOrThrow } from "../../lib/fetch.js";
8
- import { randomString } from "../../lib/random-string.js";
9
- import { AbstractCommand } from '../../model/command.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_TEMPLATE_URL = 'https://docs.hub.kipu-quantum.com/files/openapi-service.yaml';
14
- const DEFAULT_OUTPUT_FILE = 'openapi.yaml';
15
- const DEPRECATED_FILE_NAME = 'openapi-spec.yml';
16
- export default class Openapi extends AbstractCommand {
17
- static description = 'Generates the OpenAPI description for your project based on the parameter and return types of your run() method. ' +
18
- 'The output of this command will be used when creating or updating your service. ' +
19
- 'Supports PYTHON_TEMPLATE (with src/program.py), PYTHON and DOCKER runtimes (with explicit entrypoint). ' +
20
- 'The Python dependency "qhub-commons" must be installed in your activated Python environment to use this command.';
21
- static examples = [
22
- '$ qhubctl openapi',
23
- '$ qhubctl openapi --file=./openapi.yaml --format=yaml --force',
24
- '$ qhubctl openapi --entrypoint=src.program:run',
25
- '$ qhubctl openapi --package=src.program --method=run',
26
- '$ qhubctl openapi --package=src.program',
27
- ];
28
- static flags = {
29
- force: Flags.boolean({
30
- char: 'f',
31
- description: 'Overwrite the output file if it already exists',
32
- required: false,
33
- }),
34
- file: Flags.string({
35
- description: 'The file to write the OpenAPI description to',
36
- required: false,
37
- }),
38
- format: Flags.string({
39
- description: `The format to generate the OpenAPI description [possible values: ${SUPPORTED_FORMATS.join(', ')}]`,
40
- required: false,
41
- options: [...SUPPORTED_FORMATS]
42
- }),
43
- entrypoint: Flags.string({
44
- description: 'The entrypoint to your program in the format "package.module:function" (default: "src.program:run"). Overrides package and method flags.',
45
- required: false,
46
- }),
47
- package: Flags.string({
48
- description: 'The package/module path to your program (e.g., "src.program" or "my_package.main"). Only used if entrypoint is not provided.',
49
- required: false,
50
- }),
51
- method: Flags.string({
52
- description: 'The method/function name in your program (default: "run"). Only used if entrypoint is not provided.',
53
- required: false,
54
- }),
55
- };
56
- async run() {
57
- const { flags } = await this.parse(Openapi);
58
- this.checkDeprecatedFiles();
59
- // Validate format if provided
60
- if (flags.format && !SUPPORTED_FORMATS.includes(flags.format)) {
61
- throw new Error(`Unsupported format: ${flags.format}. Supported formats: ${SUPPORTED_FORMATS.join(', ')}`);
62
- }
63
- // Validate and resolve entrypoint (throws error if invalid)
64
- let entrypoint;
65
- try {
66
- entrypoint = EntrypointResolver.resolveEntrypoint(flags);
67
- }
68
- catch (error) {
69
- const errorMessage = this.getErrorMessage(error);
70
- throw new Error(`Invalid entrypoint format: ${errorMessage}\n\nExpected format: "package.module:function"\nExample: "src.program:run"\n\nUse --help for more examples.`);
71
- }
72
- const outputPath = await this.determineOutputPath(flags);
73
- const shouldGenerateFile = this.isProjectSupported(flags);
74
- if (!shouldGenerateFile) {
75
- ux.warn(`Cannot generate OpenAPI description from your code. Supported configurations:
76
- • PYTHON_TEMPLATE runtime with "./src/program.py" file
77
- • DOCKER, PYTHON, or PYTHON_TEMPLATE runtimes with explicit entrypoint (--entrypoint, or (--package, --method))`);
78
- return;
79
- }
80
- ux.action.start('Generating OpenAPI description');
81
- let fileContent;
82
- if (shouldGenerateFile) {
83
- fileContent = await this.generateFromCode(entrypoint);
84
- }
85
- else {
86
- fileContent = await this.fetchDefaultTemplate();
87
- }
88
- try {
89
- fs.writeFileSync(outputPath, fileContent, 'utf8');
90
- ux.action.stop('Done');
91
- }
92
- catch (error) {
93
- ux.action.stop('Failed');
94
- const errorMessage = this.getErrorMessage(error);
95
- throw new Error(`Failed to write file ${outputPath}: ${errorMessage}`);
96
- }
97
- this.displaySuccess(outputPath);
98
- }
99
- getErrorMessage(error) {
100
- if (this.isErrorWithMessage(error)) {
101
- return error.message;
102
- }
103
- if (typeof error === 'string') {
104
- return error;
105
- }
106
- if (error instanceof Error) {
107
- return error.message;
108
- }
109
- return 'An unknown error occurred';
110
- }
111
- isErrorWithMessage(error) {
112
- return (typeof error === 'object' &&
113
- error !== null &&
114
- 'message' in error &&
115
- typeof error.message === 'string');
116
- }
117
- checkDeprecatedFiles() {
118
- const deprecatedFile = path.join(process.cwd(), DEPRECATED_FILE_NAME);
119
- if (fs.existsSync(deprecatedFile)) {
120
- ux.warn(`Found deprecated file "${DEPRECATED_FILE_NAME}". Consider renaming it to "${DEFAULT_OUTPUT_FILE}".`);
121
- }
122
- }
123
- async determineOutputPath(flags) {
124
- const defaultPath = path.join(process.cwd(), DEFAULT_OUTPUT_FILE);
125
- const outputPath = flags.file ? path.resolve(flags.file) : defaultPath;
126
- const outputExists = fs.existsSync(outputPath);
127
- // Create output directory if it doesn't exist
128
- if (flags.file) {
129
- const outputDir = path.dirname(outputPath);
130
- if (!fs.existsSync(outputDir)) {
131
- try {
132
- fs.mkdirpSync(outputDir);
133
- }
134
- catch (error) {
135
- const errorMessage = this.getErrorMessage(error);
136
- throw new Error(`Cannot create output directory: ${outputDir}. ${errorMessage}`);
137
- }
138
- }
139
- }
140
- if (outputExists && !flags.force) {
141
- const shouldOverwrite = await confirm({
142
- message: `File "${path.basename(outputPath)}" already exists. Overwrite?`,
143
- default: false
144
- });
145
- if (!shouldOverwrite) {
146
- const newPath = path.join(path.dirname(outputPath), `${path.basename(outputPath, '.yaml')}-${randomString(5)}.yaml`);
147
- ux.stdout(`Creating new file: ${path.basename(newPath)}`);
148
- return newPath;
149
- }
150
- }
151
- return outputPath;
152
- }
153
- async generateFromCode(entrypoint) {
154
- // Load service config to get title and description
155
- let serviceConfig;
156
- let title = "Service API";
157
- let description;
158
- try {
159
- serviceConfig = readServiceConfig(process.cwd());
160
- title = serviceConfig.name || "Service API";
161
- description = serviceConfig.summary || '';
162
- }
163
- catch {
164
- // If we can't read the config, use defaults but continue
165
- ux.warn(`Could not load qhub.json configuration. Using default title: ${title}`);
166
- }
167
- // Escape parameters to prevent command injection
168
- const safeEntrypoint = entrypoint.replace(/"/g, '\\"');
169
- const safeTitle = title.replace(/"/g, '\\"');
170
- const safeDescription = description ? description.replace(/"/g, '\\"') : 'None';
171
- // Build Python command with title and description parameters
172
- const command = `python -c "from qhub.commons.openapi.generator import generate_openapi; generate_openapi(entrypoint='${safeEntrypoint}', title='${safeTitle}', description=${safeDescription === 'None' ? 'None' : `'${safeDescription}'`})"`;
173
- try {
174
- const result = await executeAsyncCommand(command);
175
- const { stdout } = this.validateCommandResult(result);
176
- return stdout;
177
- }
178
- catch (error) {
179
- const errorMessage = this.getErrorMessage(error);
180
- if (errorMessage.includes('ModuleNotFoundError')) {
181
- throw new Error(`Failed to generate OpenAPI description:\n${errorMessage}\n\nMaybe "qhub-commons" is not installed. Try to install it with 'pip install qhub-commons".`);
182
- }
183
- throw new Error(`Failed to generate OpenAPI description:\n${errorMessage}`);
184
- }
185
- }
186
- validateCommandResult(result) {
187
- if (typeof result === 'object' &&
188
- result !== null &&
189
- 'stdout' in result &&
190
- 'stderr' in result &&
191
- typeof result.stdout === 'string' &&
192
- typeof result.stderr === 'string') {
193
- const { stdout, stderr } = result;
194
- if (stderr) {
195
- throw new Error(this.formatPythonError(stderr));
196
- }
197
- if (!stdout.trim()) {
198
- throw new Error('OpenAPI generation returned empty output. Check your entrypoint and ensure it has proper type annotations.');
199
- }
200
- return {
201
- stdout, stderr
202
- };
203
- }
204
- throw new Error('Invalid command result format');
205
- }
206
- formatPythonError(stderr) {
207
- const commonErrors = {
208
- 'ModuleNotFoundError': 'Missing dependency. Install qhub-commons: pip install qhub-commons',
209
- 'ImportError': 'Cannot import module. Check your entrypoint path and ensure the module exists',
210
- 'AttributeError': 'Function not found. Check your method name and ensure it exists in the specified module',
211
- 'SyntaxError': 'Python syntax error in your code. Fix the syntax errors and try again',
212
- 'TypeError': 'Type error in your function. Ensure proper type annotations for parameters and return values'
213
- };
214
- for (const [errorType, helpText] of Object.entries(commonErrors)) {
215
- if (stderr.includes(errorType)) {
216
- return `${helpText}\n\nDetails: ${stderr}`;
217
- }
218
- }
219
- return `Failed to generate OpenAPI description: ${stderr}`;
220
- }
221
- async fetchDefaultTemplate() {
222
- try {
223
- const response = await fetchOrThrow(DEFAULT_TEMPLATE_URL, { method: 'GET' });
224
- return await response.text();
225
- }
226
- catch (error) {
227
- const errorMessage = this.getErrorMessage(error);
228
- throw new Error(`Failed to fetch default OpenAPI template from ${DEFAULT_TEMPLATE_URL}: ${errorMessage}`);
229
- }
230
- }
231
- displaySuccess(outputPath) {
232
- const relativePath = path.relative(process.cwd(), outputPath);
233
- const displayPath = relativePath.startsWith('..') ? outputPath : relativePath;
234
- ux.stdout('');
235
- ux.stdout(`OpenAPI description created: ${displayPath}`);
236
- // Provide next steps
237
- if (fs.statSync(outputPath).size > 0) {
238
- ux.stdout('');
239
- ux.stdout('Next steps:');
240
- ux.stdout(' • Review the generated OpenAPI description');
241
- ux.stdout(' • Use it to create or update your service');
242
- }
243
- }
244
- isProjectSupported(flags) {
245
- let serviceConfig;
246
- try {
247
- serviceConfig = readServiceConfig(process.cwd());
248
- }
249
- catch (error) {
250
- const errorMessage = this.getErrorMessage(error);
251
- ux.warn(`Failed to read project configuration: ${errorMessage}`);
252
- ux.warn('Ensure you have a valid qhub.json file in your project root.');
253
- return false;
254
- }
255
- const hasExplicitEntrypoint = !!(flags.entrypoint || flags.package || flags.method);
256
- const supportedRuntimes = [Runtime.PYTHON_TEMPLATE, Runtime.PYTHON, Runtime.DOCKER];
257
- if (hasExplicitEntrypoint) {
258
- const isSupported = supportedRuntimes.includes(serviceConfig.runtime);
259
- if (!isSupported) {
260
- this.showRuntimeNotSupportedWarning(serviceConfig.runtime);
261
- }
262
- return isSupported;
263
- }
264
- // Default behavior: only PYTHON_TEMPLATE with program.py file
265
- const programFile = path.join(process.cwd(), 'src', 'program.py');
266
- const hasProgramFile = fs.existsSync(programFile);
267
- const isPythonTemplate = serviceConfig.runtime === Runtime.PYTHON_TEMPLATE;
268
- const isSupported = isPythonTemplate && hasProgramFile;
269
- if (!isSupported) {
270
- this.showConfigurationHelp(serviceConfig.runtime, isPythonTemplate, hasProgramFile);
271
- }
272
- return isSupported;
273
- }
274
- showRuntimeNotSupportedWarning(currentRuntime) {
275
- ux.warn(`Runtime "${currentRuntime}" is not supported for OpenAPI generation.`);
276
- ux.warn('');
277
- ux.warn('Supported configurations:');
278
- ux.warn(' • PYTHON_TEMPLATE: Automatic (requires src/program.py)');
279
- ux.warn(' • PYTHON/DOCKER: With explicit entrypoint flags');
280
- ux.warn('');
281
- ux.warn('Examples:');
282
- ux.warn(' qhubctl openapi --entrypoint=myapp.main:handler');
283
- ux.warn(' qhubctl openapi --package=myapp.core --method=process');
284
- }
285
- showConfigurationHelp(runtime, isPythonTemplate, hasProgramFile) {
286
- ux.warn('Cannot generate OpenAPI description from your code.');
287
- ux.warn('');
288
- if (isPythonTemplate && !hasProgramFile) {
289
- ux.warn('Missing required file: src/program.py');
290
- ux.warn('');
291
- ux.warn('Solutions:');
292
- ux.warn(' • Create src/program.py with your main function, or');
293
- ux.warn(' • Use explicit entrypoint: --entrypoint=yourmodule.main:function');
294
- }
295
- else if (!isPythonTemplate) {
296
- ux.warn(`Runtime "${runtime}" requires explicit entrypoint specification.`);
297
- ux.warn('');
298
- ux.warn('Use one of these flags:');
299
- ux.warn(' --entrypoint=package.module:function');
300
- ux.warn(' --package=package.module --method=function');
301
- ux.warn('');
302
- ux.warn('Examples:');
303
- ux.warn(' qhubctl openapi --entrypoint=myapp.main:handler');
304
- ux.warn(' qhubctl openapi --package=myapp.core --method=process');
305
- }
306
- }
307
- }
@@ -1,18 +0,0 @@
1
- import { AuthenticatedCommand } from '../../model/command.js';
2
- import QuantumHubService from '../../service/quantum-hub-service.js';
3
- export default class Run extends AuthenticatedCommand {
4
- static readonly args: {
5
- serviceId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
- };
7
- static readonly description = "Creates a job execution";
8
- static readonly examples: string[];
9
- static readonly flags: {
10
- input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
- 'input-files': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
- detached: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
- };
14
- hubService: QuantumHubService;
15
- getInputFromFiles(filePaths: string): Promise<Record<string, never> | undefined>;
16
- init(): Promise<void>;
17
- run(): Promise<void>;
18
- }
@@ -1,150 +0,0 @@
1
- import { Args, Flags, ux } from '@oclif/core';
2
- import { waitUntil } from 'async-wait-until';
3
- import fsExtra from 'fs-extra';
4
- const { readJsonSync } = fsExtra;
5
- import inquirer from 'inquirer';
6
- import fs from 'node:fs';
7
- import path from 'node:path';
8
- import { AuthenticatedCommand } from '../../model/command.js';
9
- import { defaultBasePath } from '../../model/user-config.js';
10
- import QuantumHubService from '../../service/quantum-hub-service.js';
11
- import { readServiceConfig } from '../../service/service-config-service.js';
12
- import { HubPlatform } from "@quantum-hub/qhub-api/platform";
13
- export default class Run extends AuthenticatedCommand {
14
- static args = {
15
- serviceId: Args.string(),
16
- };
17
- static description = 'Creates a job execution';
18
- static examples = [
19
- '$ qhubctl run',
20
- '$ qhubctl run --detached',
21
- '$ qhubctl run -i \'{"values": [10,12], "round_up": true}\'',
22
- '$ qhubctl run --input-files=./input/data.json,./input/params.json',
23
- '$ qhubctl run --input-files=./config.json,./overrides.json',
24
- ];
25
- static flags = {
26
- input: Flags.string({ char: 'i', description: 'Input as JSON string.', required: false }),
27
- 'input-files': Flags.string({
28
- aliases: ['input-files'],
29
- description: 'Comma-separated paths to files containing input to be merged (default: ./input/data.json,./input/params.json).',
30
- required: false,
31
- }),
32
- detached: Flags.boolean({
33
- description: 'Executes the job in detached mode, i.e., without waiting for it to finish.',
34
- required: false,
35
- }),
36
- };
37
- hubService;
38
- async getInputFromFiles(filePaths) {
39
- const paths = filePaths.split(',').map(p => p.trim());
40
- const mergedInput = {};
41
- let hasValidFile = false;
42
- for (const filePath of paths) {
43
- let file = filePath;
44
- // if the file path is not absolute, try to find it in the current working directory
45
- if (!fs.existsSync(filePath)) {
46
- file = path.join(process.cwd(), filePath);
47
- }
48
- if (fs.existsSync(file)) {
49
- try {
50
- const content = readJsonSync(file, { encoding: 'utf8' });
51
- Object.assign(mergedInput, content);
52
- hasValidFile = true;
53
- }
54
- catch {
55
- ux.warn(`Failed to parse JSON from ${filePath}. Skipping this file.`);
56
- }
57
- }
58
- }
59
- if (!hasValidFile) {
60
- const response = await inquirer.prompt({
61
- message: `No valid input files found. Run without input?`,
62
- name: 'confirm',
63
- type: 'confirm',
64
- });
65
- if (response.confirm)
66
- return {};
67
- this.exit();
68
- }
69
- return mergedInput;
70
- }
71
- async init() {
72
- await super.init();
73
- this.hubService = new QuantumHubService(this, this.config, this.userConfig);
74
- }
75
- async run() {
76
- const { args } = await this.parse(Run);
77
- const { flags } = await this.parse(Run);
78
- let { serviceId } = args;
79
- // if no service id is provided, try to read it from the qhub.json
80
- if (!serviceId) {
81
- try {
82
- const serviceConfig = readServiceConfig(process.cwd());
83
- serviceId = serviceConfig.serviceId;
84
- }
85
- catch {
86
- serviceId = undefined;
87
- }
88
- }
89
- if (!serviceId) {
90
- ux.error('Missing service ID. Please provide a service ID as argument or set it in the qhub.json of your project.');
91
- }
92
- const service = await this.hubService.getService(serviceId);
93
- let input;
94
- if (flags.input) {
95
- try {
96
- input = JSON.parse(flags.input);
97
- }
98
- catch {
99
- ux.error('Invalid JSON provided.');
100
- }
101
- }
102
- else {
103
- input = await this.getInputFromFiles(flags['input-files'] || './input/data.json,./input/params.json');
104
- }
105
- let job;
106
- const payload = { serviceId: service.id, tags: ['cli'], input };
107
- job = await this.hubService.runJob(payload);
108
- if (flags.detached) {
109
- ux.stdout(`Job (${job.id}) created.`);
110
- this.exit();
111
- }
112
- ux.action.start(`Running Job (${job.id})`);
113
- try {
114
- await waitUntil(async () => {
115
- job = await this.hubService.getJobById(job.id);
116
- return job.status === HubPlatform.ServiceJobDto.Status.Succeeded || job.status === HubPlatform.ServiceJobDto.Status.Failed || job.status === HubPlatform.ServiceJobDto.Status.Cancelled;
117
- }, {
118
- intervalBetweenAttempts: 5000, // every 5 seconds
119
- timeout: 10 * 60 * 1000, // 10 minute timeout
120
- });
121
- }
122
- catch {
123
- // ignore wait until next attempt
124
- }
125
- const basePath = this.userConfig.endpoint?.basePath || defaultBasePath;
126
- const jobDetailsLink = `${basePath}/service-jobs/${job.id}`;
127
- switch (job.status) {
128
- case HubPlatform.ServiceJobDto.Status.Cancelled: {
129
- ux.action.stop('Job canceled.');
130
- ux.stdout(`See details at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007\``);
131
- break;
132
- }
133
- case HubPlatform.ServiceJobDto.Status.Failed: {
134
- ux.action.stop('Job failed.');
135
- ux.stdout(`See details at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007\``);
136
- break;
137
- }
138
- case HubPlatform.ServiceJobDto.Status.Succeeded: {
139
- ux.action.stop('Job succeeded.');
140
- ux.stdout(`See result at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007\``);
141
- break;
142
- }
143
- default: {
144
- ux.action.stop('Timeout polling status.');
145
- ux.stdout(`See status details at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007\``);
146
- }
147
- }
148
- this.exit();
149
- }
150
- }
@@ -1,11 +0,0 @@
1
- import { AbstractCommand } from '../../model/command.js';
2
- export default class Serve extends AbstractCommand {
3
- static description: string;
4
- static examples: string[];
5
- static flags: {
6
- port: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
7
- 'log-level': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
- workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
- };
10
- run(): Promise<void>;
11
- }
@@ -1,51 +0,0 @@
1
- import { Flags, ux } from '@oclif/core';
2
- import { spawnSync } from 'node:child_process';
3
- import { AbstractCommand } from '../../model/command.js';
4
- export default class Serve extends AbstractCommand {
5
- static description = 'Runs your project locally via qhub-serve inside the workspace virtual environment, ' +
6
- 'exposing the same HTTP endpoints as Kipu Quantum Hub (start execution, check status, ' +
7
- 'cancel, retrieve results).';
8
- static examples = [
9
- '$ qhubctl serve',
10
- '$ qhubctl serve -p 8081',
11
- '$ qhubctl serve --workspace /path/to/workspace',
12
- ];
13
- static flags = {
14
- port: Flags.integer({
15
- char: 'p',
16
- description: 'The port on which the local web server accepts requests',
17
- required: false,
18
- default: 8081,
19
- }),
20
- 'log-level': Flags.string({
21
- description: 'Log level for qhub-serve. Possible values: TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL',
22
- required: false,
23
- default: 'INFO',
24
- options: ['TRACE', 'DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR', 'CRITICAL'],
25
- }),
26
- workspace: Flags.string({
27
- char: 'w',
28
- description: 'Path to the workspace directory (must be a valid uv project). Defaults to the current directory.',
29
- required: false,
30
- }),
31
- };
32
- async run() {
33
- const { flags } = await this.parse(Serve);
34
- const workspaceDir = flags.workspace ?? process.cwd();
35
- const port = flags.port;
36
- const logLevel = flags['log-level'];
37
- // Step 1: Add (or upgrade) qhub-serve as a dev dependency
38
- ux.stdout('Ensuring qhub-serve is up to date...');
39
- const addResult = spawnSync('uv', ['add', '--dev', '--upgrade', 'qhub-serve', '--project', workspaceDir], { stdio: 'inherit' });
40
- if (addResult.status !== 0) {
41
- this.error('Failed to install qhub-serve via uv. ' +
42
- 'Make sure uv is installed and the workspace is a valid uv project.');
43
- }
44
- // Step 2: Run qhub-serve inside the workspace venv
45
- ux.stdout('\nStarting qhub-serve...');
46
- const runResult = spawnSync('uv', ['run', '--project', workspaceDir, 'qhub-serve', '--workspace', workspaceDir, '--port', String(port), '--log-level', logLevel], { stdio: 'inherit' });
47
- if (runResult.status !== 0) {
48
- this.error('qhub-serve exited with a non-zero status code.');
49
- }
50
- }
51
- }
@@ -1,14 +0,0 @@
1
- import { AuthenticatedCommand } from '../../model/command.js';
2
- import QuantumHubService from '../../service/quantum-hub-service.js';
3
- export default class Services extends AuthenticatedCommand {
4
- static readonly description = "List all services of the current context.";
5
- static readonly examples: string[];
6
- static readonly flags: {
7
- json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
- name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
- all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
- };
11
- quantumHubService: QuantumHubService;
12
- init(): Promise<void>;
13
- run(): Promise<void>;
14
- }
@@ -1,65 +0,0 @@
1
- import { Flags, ux } from '@oclif/core';
2
- import Table from "tty-table";
3
- import { AuthenticatedCommand } from '../../model/command.js';
4
- import QuantumHubService from '../../service/quantum-hub-service.js';
5
- export default class Services extends AuthenticatedCommand {
6
- static description = 'List all services of the current context.';
7
- static examples = [
8
- '$ qhubctl services',
9
- '$ qhubctl services --json',
10
- '$ qhubctl services --name "my-service"',
11
- '$ qhubctl services --name "my-service" --json',
12
- '$ qhubctl services --json --all',
13
- ];
14
- static flags = {
15
- json: Flags.boolean({
16
- description: 'output in JSON format',
17
- default: false,
18
- }),
19
- name: Flags.string({
20
- description: 'filter services by name (case-insensitive full match)',
21
- required: false,
22
- }),
23
- all: Flags.boolean({
24
- description: 'display all service properties (only available with --json)',
25
- default: false,
26
- }),
27
- };
28
- quantumHubService;
29
- async init() {
30
- const { flags } = await this.parse(Services);
31
- await super.init(!flags.json);
32
- this.quantumHubService = new QuantumHubService(this, this.config, this.userConfig);
33
- }
34
- async run() {
35
- const { flags } = await this.parse(Services);
36
- // Validate flag combinations
37
- if (flags.all && !flags.json) {
38
- ux.warn('The --all flag can only be used with --json output. Continuing without --all.');
39
- }
40
- ux.action.start('Loading services');
41
- let services = await this.quantumHubService.getServices();
42
- ux.action.stop('Done');
43
- // Filter by name if provided
44
- if (flags.name) {
45
- const nameFilter = flags.name.toLowerCase();
46
- services = services.filter(service => service.name?.toLowerCase() === nameFilter);
47
- }
48
- const tableData = services
49
- .map(service => ({ id: service.id, name: service.name }));
50
- if (flags.json) {
51
- ux.stdout(JSON.stringify(flags.all ? services : tableData, null, 2));
52
- return;
53
- }
54
- const header = [
55
- { value: "id", alias: "ID", align: "left" },
56
- { value: "name", alias: "Name", align: "left" },
57
- ];
58
- const t = Table(header, tableData);
59
- const contextName = this.userConfig.context?.displayName;
60
- const filterInfo = flags.name ? ` (filtered by name: "${flags.name}")` : '';
61
- ux.stdout(`Available services for ${contextName}${filterInfo}:`);
62
- ux.stdout(t.render());
63
- ux.stdout();
64
- }
65
- }
@@ -1,17 +0,0 @@
1
- import Account from '../../model/account.js';
2
- import { AuthenticatedCommand } from '../../model/command.js';
3
- import QuantumHubService from '../../service/quantum-hub-service.js';
4
- export default class SetContext extends AuthenticatedCommand {
5
- static args: {
6
- contextId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
- };
8
- static description: string;
9
- static examples: string[];
10
- quantumHubService: QuantumHubService;
11
- getPromptOptions(accounts: Account[]): {
12
- name: string;
13
- value: Account;
14
- }[];
15
- init(): Promise<void>;
16
- run(): Promise<void>;
17
- }