@quantum-hub/qhubctl 1.1.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +30 -61
  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 -13
  70. package/dist/commands/serve/index.js +0 -87
  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 -530
@@ -0,0 +1,244 @@
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
+ function getErrorMessage(err) {
16
+ if (typeof err === 'object' && err !== null && 'message' in err &&
17
+ typeof err.message === 'string') {
18
+ return err.message;
19
+ }
20
+ if (typeof err === 'string')
21
+ return err;
22
+ if (err instanceof Error)
23
+ return err.message;
24
+ return 'An unknown error occurred';
25
+ }
26
+ function checkDeprecatedFiles() {
27
+ const deprecatedFile = path.join(process.cwd(), DEPRECATED_FILE_NAME);
28
+ if (fs.existsSync(deprecatedFile)) {
29
+ warn(`Found deprecated file "${DEPRECATED_FILE_NAME}". Consider renaming it to "${DEFAULT_OUTPUT_FILE}".`);
30
+ }
31
+ }
32
+ async function determineOutputPath(opts) {
33
+ const defaultPath = path.join(process.cwd(), DEFAULT_OUTPUT_FILE);
34
+ const outputPath = opts.file ? path.resolve(opts.file) : defaultPath;
35
+ const outputExists = fs.existsSync(outputPath);
36
+ if (opts.file) {
37
+ const outputDir = path.dirname(outputPath);
38
+ if (!fs.existsSync(outputDir)) {
39
+ try {
40
+ fs.mkdirpSync(outputDir);
41
+ }
42
+ catch (err) {
43
+ throw new Error(`Cannot create output directory: ${outputDir}. ${getErrorMessage(err)}`);
44
+ }
45
+ }
46
+ }
47
+ if (outputExists && !opts.force) {
48
+ const shouldOverwrite = await confirm({
49
+ message: `File "${path.basename(outputPath)}" already exists. Overwrite?`,
50
+ default: false,
51
+ });
52
+ if (!shouldOverwrite) {
53
+ const newPath = path.join(path.dirname(outputPath), `${path.basename(outputPath, '.yaml')}-${randomString(5)}.yaml`);
54
+ stdout(`Creating new file: ${path.basename(newPath)}`);
55
+ return newPath;
56
+ }
57
+ }
58
+ return outputPath;
59
+ }
60
+ function isProjectSupported(opts) {
61
+ let serviceConfig;
62
+ try {
63
+ serviceConfig = readServiceConfig(process.cwd());
64
+ }
65
+ catch (err) {
66
+ warn(`Failed to read project configuration: ${getErrorMessage(err)}`);
67
+ warn('Ensure you have a valid qhub.json file in your project root.');
68
+ return false;
69
+ }
70
+ const hasExplicitEntrypoint = !!(opts.entrypoint || opts.package || opts.method);
71
+ const supportedRuntimes = [Runtime.PYTHON_TEMPLATE, Runtime.PYTHON, Runtime.DOCKER];
72
+ if (hasExplicitEntrypoint) {
73
+ const isSupported = supportedRuntimes.includes(serviceConfig.runtime);
74
+ if (!isSupported) {
75
+ warn(`Runtime "${serviceConfig.runtime}" is not supported for OpenAPI generation.`);
76
+ warn('');
77
+ warn('Supported configurations:');
78
+ warn(' \u2022 PYTHON_TEMPLATE: Automatic (requires src/program.py)');
79
+ warn(' \u2022 PYTHON/DOCKER: With explicit entrypoint flags');
80
+ warn('');
81
+ warn('Examples:');
82
+ warn(' qhubctl openapi --entrypoint=myapp.main:handler');
83
+ warn(' qhubctl openapi --package=myapp.core --method=process');
84
+ }
85
+ return isSupported;
86
+ }
87
+ const programFile = path.join(process.cwd(), 'src', 'program.py');
88
+ const hasProgramFile = fs.existsSync(programFile);
89
+ const isPythonTemplate = serviceConfig.runtime === Runtime.PYTHON_TEMPLATE;
90
+ const isSupported = isPythonTemplate && hasProgramFile;
91
+ if (!isSupported) {
92
+ warn('Cannot generate OpenAPI description from your code.');
93
+ warn('');
94
+ if (isPythonTemplate && !hasProgramFile) {
95
+ warn('Missing required file: src/program.py');
96
+ warn('');
97
+ warn('Solutions:');
98
+ warn(' \u2022 Create src/program.py with your main function, or');
99
+ warn(' \u2022 Use explicit entrypoint: --entrypoint=yourmodule.main:function');
100
+ }
101
+ else if (!isPythonTemplate) {
102
+ warn(`Runtime "${serviceConfig.runtime}" requires explicit entrypoint specification.`);
103
+ warn('');
104
+ warn('Use one of these flags:');
105
+ warn(' --entrypoint=package.module:function');
106
+ warn(' --package=package.module --method=function');
107
+ warn('');
108
+ warn('Examples:');
109
+ warn(' qhubctl openapi --entrypoint=myapp.main:handler');
110
+ warn(' qhubctl openapi --package=myapp.core --method=process');
111
+ }
112
+ }
113
+ return isSupported;
114
+ }
115
+ function formatPythonError(stderr) {
116
+ const commonErrors = {
117
+ 'ModuleNotFoundError': 'Missing dependency. Install qhub-commons: pip install qhub-commons',
118
+ 'ImportError': 'Cannot import module. Check your entrypoint path and ensure the module exists',
119
+ 'AttributeError': 'Function not found. Check your method name and ensure it exists in the specified module',
120
+ 'SyntaxError': 'Python syntax error in your code. Fix the syntax errors and try again',
121
+ 'TypeError': 'Type error in your function. Ensure proper type annotations for parameters and return values',
122
+ };
123
+ for (const [errorType, helpText] of Object.entries(commonErrors)) {
124
+ if (stderr.includes(errorType))
125
+ return `${helpText}\n\nDetails: ${stderr}`;
126
+ }
127
+ return `Failed to generate OpenAPI description: ${stderr}`;
128
+ }
129
+ function validateCommandResult(result) {
130
+ if (typeof result === 'object' && result !== null &&
131
+ 'stdout' in result && 'stderr' in result &&
132
+ typeof result.stdout === 'string' &&
133
+ typeof result.stderr === 'string') {
134
+ const { stdout: out, stderr } = result;
135
+ if (!out.trim()) {
136
+ const detail = stderr.trim() ? formatPythonError(stderr) : 'OpenAPI generation returned empty output. Check your entrypoint and ensure it has proper type annotations.';
137
+ throw new Error(detail);
138
+ }
139
+ return { stdout: out, stderr };
140
+ }
141
+ throw new Error('Invalid command result format');
142
+ }
143
+ async function generateFromCode(entrypoint) {
144
+ let title = 'Service API';
145
+ let description;
146
+ try {
147
+ const serviceConfig = readServiceConfig(process.cwd());
148
+ title = serviceConfig.name || 'Service API';
149
+ description = serviceConfig.summary || '';
150
+ }
151
+ catch {
152
+ warn(`Could not load qhub.json configuration. Using default title: ${title}`);
153
+ }
154
+ const safeEntrypoint = entrypoint.replace(/"/g, '\\"');
155
+ const safeTitle = title.replace(/"/g, '\\"');
156
+ const safeDescription = description ? description.replace(/"/g, '\\"') : 'None';
157
+ const pySnippet = `from qhub.commons.openapi.generator import generate_openapi; generate_openapi(entrypoint='${safeEntrypoint}', title='${safeTitle}', description=${safeDescription === 'None' ? 'None' : `'${safeDescription}'`})`;
158
+ const command = `uv run --no-sync --project ${process.cwd()} python -c "${pySnippet}"`;
159
+ try {
160
+ const result = await executeAsyncCommand(command);
161
+ const { stdout: out } = validateCommandResult(result);
162
+ return out;
163
+ }
164
+ catch (err) {
165
+ const errorMessage = getErrorMessage(err);
166
+ throw new Error(`Failed to generate OpenAPI description:\n${errorMessage}`);
167
+ }
168
+ }
169
+ const EXAMPLES = `
170
+ Examples:
171
+ $ qhubctl openapi
172
+ $ qhubctl openapi --file=./openapi.yaml --format=yaml --force
173
+ $ qhubctl openapi --entrypoint=src.program:run
174
+ $ qhubctl openapi --package=src.program --method=run
175
+ $ qhubctl openapi --package=src.program`;
176
+ export const openapiCommand = new Command('openapi')
177
+ .description('Generates the OpenAPI description for your project based on the parameter and return types of your run() method. ' +
178
+ 'The output of this command will be used when creating or updating your service. ' +
179
+ 'Supports PYTHON_TEMPLATE (with src/program.py), PYTHON and DOCKER runtimes (with explicit entrypoint). ' +
180
+ 'Requires uv and a valid uv project in the workspace. The "qhub-commons" dependency is installed/upgraded automatically as a dependency.')
181
+ .option('-f, --force', 'Overwrite the output file if it already exists', false)
182
+ .option('--file <value>', 'The file to write the OpenAPI description to')
183
+ .option('--format <value>', `The format to generate the OpenAPI description [possible values: ${SUPPORTED_FORMATS.join(', ')}]`)
184
+ .option('--entrypoint <value>', 'The entrypoint to your program in the format "package.module:function" (default: "src.program:run"). Overrides package and method flags.')
185
+ .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.')
186
+ .option('--method <value>', 'The method/function name in your program (default: "run"). Only used if entrypoint is not provided.')
187
+ .addHelpText('after', EXAMPLES)
188
+ .action(async (opts) => {
189
+ checkDeprecatedFiles();
190
+ if (opts.format && !SUPPORTED_FORMATS.includes(opts.format)) {
191
+ throw new Error(`Unsupported format: ${opts.format}. Supported formats: ${SUPPORTED_FORMATS.join(', ')}`);
192
+ }
193
+ let entrypoint;
194
+ try {
195
+ entrypoint = EntrypointResolver.resolveEntrypoint(opts);
196
+ }
197
+ catch (err) {
198
+ throw new Error(`Invalid entrypoint format: ${getErrorMessage(err)}\n\n` +
199
+ 'Expected format: "package.module:function"\nExample: "src.program:run"\n\n' +
200
+ 'Use --help for more examples.');
201
+ }
202
+ const outputPath = await determineOutputPath(opts);
203
+ const shouldGenerateFile = isProjectSupported(opts);
204
+ if (!shouldGenerateFile) {
205
+ warn(`Cannot generate OpenAPI description from your code. Supported configurations:\n` +
206
+ ' \u2022 PYTHON_TEMPLATE runtime with "./src/program.py" file\n' +
207
+ ' \u2022 DOCKER, PYTHON, or PYTHON_TEMPLATE runtimes with explicit entrypoint (--entrypoint, or (--package, --method))');
208
+ return;
209
+ }
210
+ // Ensure qhub-commons is installed (or upgraded) in the project venv
211
+ stdout('Ensuring qhub-commons is up to date...');
212
+ const addResult = spawnSync('uv', ['add', '--upgrade', 'qhub-commons', '--project', process.cwd()], { stdio: 'inherit' });
213
+ if (addResult.status !== 0) {
214
+ error('Failed to install qhub-commons via uv. ' +
215
+ 'Make sure uv is installed and the workspace is a valid uv project.');
216
+ }
217
+ const spin = spinner('Generating OpenAPI description');
218
+ let fileContent;
219
+ try {
220
+ fileContent = await generateFromCode(entrypoint);
221
+ }
222
+ catch (err) {
223
+ spin.fail('Failed');
224
+ throw err;
225
+ }
226
+ try {
227
+ fs.writeFileSync(outputPath, fileContent, 'utf8');
228
+ spin.succeed('Done');
229
+ }
230
+ catch (err) {
231
+ spin.fail('Failed');
232
+ throw new Error(`Failed to write file ${outputPath}: ${getErrorMessage(err)}`);
233
+ }
234
+ const relativePath = path.relative(process.cwd(), outputPath);
235
+ const displayPath = relativePath.startsWith('..') ? outputPath : relativePath;
236
+ stdout('');
237
+ stdout(`OpenAPI description created: ${displayPath}`);
238
+ if (fs.statSync(outputPath).size > 0) {
239
+ stdout('');
240
+ stdout('Next steps:');
241
+ stdout(' \u2022 Review the generated OpenAPI description');
242
+ stdout(' \u2022 Use it to create or update your service');
243
+ }
244
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const runCommand: Command;
@@ -0,0 +1,126 @@
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
+ const { readJsonSync } = fsExtra;
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
+ async function getInputFromFiles(filePaths) {
15
+ const paths = filePaths.split(',').map(p => p.trim());
16
+ const mergedInput = {};
17
+ let hasValidFile = false;
18
+ for (const filePath of paths) {
19
+ let file = filePath;
20
+ if (!fs.existsSync(filePath)) {
21
+ file = path.join(process.cwd(), filePath);
22
+ }
23
+ if (fs.existsSync(file)) {
24
+ try {
25
+ const content = readJsonSync(file, { encoding: 'utf8' });
26
+ Object.assign(mergedInput, content);
27
+ hasValidFile = true;
28
+ }
29
+ catch {
30
+ warn(`Failed to parse JSON from ${filePath}. Skipping this file.`);
31
+ }
32
+ }
33
+ }
34
+ if (!hasValidFile) {
35
+ const confirmed = await confirm({ message: 'No valid input files found. Run without input?' });
36
+ if (confirmed)
37
+ return {};
38
+ process.exit(0);
39
+ }
40
+ return mergedInput;
41
+ }
42
+ const EXAMPLES = `
43
+ Examples:
44
+ $ qhubctl run
45
+ $ qhubctl run --detached
46
+ $ qhubctl run -i '{"values": [10,12], "round_up": true}'
47
+ $ qhubctl run --input-files=./input/data.json,./input/params.json
48
+ $ qhubctl run --input-files=./config.json,./overrides.json`;
49
+ export const runCommand = new Command('run')
50
+ .description('Creates a job execution')
51
+ .argument('[serviceId]', 'The ID of the service to run (optional — reads from qhub.json if omitted)')
52
+ .option('-i, --input <value>', 'Input as JSON string.')
53
+ .option('--input-files <value>', 'Comma-separated paths to files containing input to be merged (default: ./input/data.json,./input/params.json).')
54
+ .option('--detached', 'Executes the job in detached mode, i.e., without waiting for it to finish.', false)
55
+ .addHelpText('after', EXAMPLES)
56
+ .action(async (serviceId, opts) => {
57
+ const userConfig = await requireAuth();
58
+ const hubService = new QuantumHubService(userConfig);
59
+ if (!serviceId) {
60
+ try {
61
+ const serviceConfig = readServiceConfig(process.cwd());
62
+ serviceId = serviceConfig.serviceId;
63
+ }
64
+ catch {
65
+ serviceId = undefined;
66
+ }
67
+ }
68
+ if (!serviceId) {
69
+ error('Missing service ID. Please provide a service ID as argument or set it in the qhub.json of your project.');
70
+ }
71
+ const service = await hubService.getService(serviceId);
72
+ let input;
73
+ if (opts.input) {
74
+ try {
75
+ input = JSON.parse(opts.input);
76
+ }
77
+ catch {
78
+ error('Invalid JSON provided.');
79
+ }
80
+ }
81
+ else {
82
+ input = await getInputFromFiles(opts.inputFiles || './input/data.json,./input/params.json');
83
+ }
84
+ let job;
85
+ const payload = { serviceId: service.id, tags: ['cli'], input };
86
+ job = await hubService.runJob(payload);
87
+ if (opts.detached) {
88
+ stdout(`Job (${job.id}) created.`);
89
+ return;
90
+ }
91
+ const spin = spinner(`Running Job (${job.id})`);
92
+ try {
93
+ await waitUntil(async () => {
94
+ job = await hubService.getJobById(job.id);
95
+ return (job.status === HubPlatform.ServiceJobDto.Status.Succeeded ||
96
+ job.status === HubPlatform.ServiceJobDto.Status.Failed ||
97
+ job.status === HubPlatform.ServiceJobDto.Status.Cancelled);
98
+ }, {
99
+ intervalBetweenAttempts: 5000,
100
+ timeout: 10 * 60 * 1000,
101
+ });
102
+ }
103
+ catch {
104
+ // ignore — status printed below
105
+ }
106
+ const basePath = userConfig.endpoint?.basePath || defaultBasePath;
107
+ const jobDetailsLink = `${basePath}/service-jobs/${job.id}`;
108
+ switch (job.status) {
109
+ case HubPlatform.ServiceJobDto.Status.Cancelled:
110
+ spin.warn('Job canceled.');
111
+ stdout(`See details at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007`);
112
+ break;
113
+ case HubPlatform.ServiceJobDto.Status.Failed:
114
+ spin.fail('Job failed.');
115
+ stdout(`See details at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007`);
116
+ break;
117
+ case HubPlatform.ServiceJobDto.Status.Succeeded:
118
+ spin.succeed('Job succeeded.');
119
+ stdout(`See result at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007`);
120
+ break;
121
+ default:
122
+ spin.stop();
123
+ stdout('Timeout polling status.');
124
+ stdout(`See status details at \u001B]8;;${jobDetailsLink}\u0007${jobDetailsLink}\u001B]8;;\u0007`);
125
+ }
126
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const serveCommand: Command;
@@ -0,0 +1,35 @@
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
+ const EXAMPLES = `
6
+ Examples:
7
+ $ qhubctl serve
8
+ $ qhubctl serve -p 8081
9
+ $ qhubctl serve --workspace /path/to/workspace`;
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
+ .action(async (opts) => {
19
+ const workspaceDir = opts.workspace ?? process.cwd();
20
+ const port = opts.port;
21
+ const logLevel = opts.logLevel;
22
+ // Step 1: Add (or upgrade) qhub-serve as a dev dependency
23
+ stdout('Ensuring qhub-serve is up to date...');
24
+ const addResult = spawnSync('uv', ['add', '--dev', '--upgrade', 'qhub-serve', '--project', workspaceDir], { stdio: 'inherit' });
25
+ if (addResult.status !== 0) {
26
+ error('Failed to install qhub-serve via uv. ' +
27
+ 'Make sure uv is installed and the workspace is a valid uv project.');
28
+ }
29
+ // Step 2: Run qhub-serve inside the workspace venv
30
+ stdout('\nStarting qhub-serve...');
31
+ const runResult = spawnSync('uv', ['run', '--project', workspaceDir, 'qhub-serve', '--workspace', workspaceDir, '--port', String(port), '--log-level', logLevel], { stdio: 'inherit' });
32
+ if (runResult.status !== 0) {
33
+ error('qhub-serve exited with a non-zero status code.');
34
+ }
35
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const servicesCommand: Command;
@@ -0,0 +1,48 @@
1
+ import { Command } from 'commander';
2
+ import Table from 'tty-table';
3
+ import { spinner, stdout, warn } from '../lib/output.js';
4
+ import { requireAuth } from '../model/command.js';
5
+ import QuantumHubService from '../service/quantum-hub-service.js';
6
+ const EXAMPLES = `
7
+ 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
+ export const servicesCommand = new Command('services')
14
+ .description('List all services of the current context.')
15
+ .option('--json', 'Output in JSON format', false)
16
+ .option('--name <value>', 'Filter services by name (case-insensitive full match)')
17
+ .option('--all', 'Display all service properties (only available with --json)', false)
18
+ .addHelpText('after', EXAMPLES)
19
+ .action(async (opts) => {
20
+ // suppress "Current context: ..." line when outputting JSON
21
+ const userConfig = await requireAuth({ printContext: !opts.json });
22
+ const quantumHubService = new QuantumHubService(userConfig);
23
+ if (opts.all && !opts.json) {
24
+ warn('The --all flag can only be used with --json output. Continuing without --all.');
25
+ }
26
+ const spin = spinner('Loading services');
27
+ let services = await quantumHubService.getServices();
28
+ spin.succeed('Services loaded');
29
+ if (opts.name) {
30
+ const nameFilter = opts.name.toLowerCase();
31
+ services = services.filter(service => service.name?.toLowerCase() === nameFilter);
32
+ }
33
+ const tableData = services.map(service => ({ id: service.id, name: service.name }));
34
+ if (opts.json) {
35
+ stdout(JSON.stringify(opts.all ? services : tableData, null, 2));
36
+ return;
37
+ }
38
+ const header = [
39
+ { value: "id", alias: "ID", align: "left" },
40
+ { value: "name", alias: "Name", align: "left" },
41
+ ];
42
+ const t = Table(header, tableData);
43
+ const contextName = userConfig.context?.displayName;
44
+ const filterInfo = opts.name ? ` (filtered by name: "${opts.name}")` : '';
45
+ stdout(`Available services for ${contextName}${filterInfo}:`);
46
+ stdout(t.render());
47
+ stdout();
48
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const setContextCommand: Command;
@@ -0,0 +1,52 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { Command } from 'commander';
3
+ import { error, spinner, stdout } from '../lib/output.js';
4
+ import { configDir, requireAuth } from '../model/command.js';
5
+ import QuantumHubService from '../service/quantum-hub-service.js';
6
+ import { writeUserConfig } from '../service/user-config-service.js';
7
+ function getPromptOptions(accounts) {
8
+ return accounts.map(account => ({
9
+ name: account.isOrganization ? `${account.displayName} (Organization)` : account.displayName,
10
+ value: account,
11
+ }));
12
+ }
13
+ const EXAMPLES = `
14
+ Examples:
15
+ $ qhubctl set-context
16
+ $ qhubctl set-context <context-id>`;
17
+ export const setContextCommand = new Command('set-context')
18
+ .description('Set the current context, i.e., the personal or organization account you are currently working with.')
19
+ .argument('[contextId]', 'The ID of the context to switch to')
20
+ .addHelpText('after', EXAMPLES)
21
+ .action(async (contextId) => {
22
+ const userConfig = await requireAuth();
23
+ const quantumHubService = new QuantumHubService(userConfig);
24
+ if (!contextId) {
25
+ const spin = spinner('Loading available contexts');
26
+ const accounts = await quantumHubService.getAccounts();
27
+ spin.succeed('Contexts loaded');
28
+ const response = await select({
29
+ choices: getPromptOptions(accounts),
30
+ message: 'Choose a context',
31
+ default: accounts[0],
32
+ });
33
+ contextId = response.id;
34
+ }
35
+ else {
36
+ const accounts = await quantumHubService.getAccounts();
37
+ const selectedContext = accounts.find(account => account.id === contextId);
38
+ if (!selectedContext) {
39
+ error(`Context with id "${contextId}" not found.`);
40
+ }
41
+ }
42
+ const accounts = await quantumHubService.getAccounts();
43
+ const selectedContext = accounts.find(account => account.id === contextId);
44
+ if (!selectedContext) {
45
+ error(`Context with id "${contextId}" not found.`);
46
+ }
47
+ writeUserConfig(configDir, {
48
+ ...userConfig,
49
+ context: selectedContext,
50
+ });
51
+ stdout(`Switched to context "${selectedContext?.displayName}".`);
52
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const upCommand: Command;