@quantum-hub/qhubctl 1.2.0 → 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.
- package/README.md +136 -20
- package/dist/client/kqh/models/UpdateWorkflowServiceRequest.js +0 -11
- package/dist/commands/compress.d.ts +2 -0
- package/dist/commands/compress.js +23 -0
- package/dist/commands/datapool-upload.d.ts +2 -0
- package/dist/commands/datapool-upload.js +184 -0
- package/dist/commands/get-context.d.ts +2 -0
- package/dist/commands/get-context.js +12 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +66 -0
- package/dist/commands/list-contexts.d.ts +2 -0
- package/dist/commands/list-contexts.js +28 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +59 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +20 -0
- package/dist/commands/openapi.d.ts +2 -0
- package/dist/commands/openapi.js +244 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +126 -0
- package/dist/commands/serve.d.ts +2 -0
- package/dist/commands/serve.js +35 -0
- package/dist/commands/services.d.ts +2 -0
- package/dist/commands/services.js +48 -0
- package/dist/commands/set-context.d.ts +2 -0
- package/dist/commands/set-context.js +52 -0
- package/dist/commands/up.d.ts +2 -0
- package/dist/commands/up.js +161 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/config-dir.d.ts +6 -0
- package/dist/lib/config-dir.js +8 -0
- package/dist/lib/entrypoint-resolver.js +2 -2
- package/dist/lib/output.d.ts +14 -0
- package/dist/lib/output.js +22 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +8 -0
- package/dist/model/command.d.ts +14 -11
- package/dist/model/command.js +44 -40
- package/dist/program.d.ts +2 -0
- package/dist/program.js +47 -0
- package/dist/service/command-service.d.ts +1 -5
- package/dist/service/command-service.js +1 -5
- package/dist/service/quantum-hub-service.d.ts +1 -3
- package/dist/service/quantum-hub-service.js +2 -2
- package/package.json +30 -61
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -17
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -12
- package/dist/commands/compress/index.d.ts +0 -6
- package/dist/commands/compress/index.js +0 -25
- package/dist/commands/datapool/upload.d.ts +0 -23
- package/dist/commands/datapool/upload.js +0 -260
- package/dist/commands/get-context/index.d.ts +0 -6
- package/dist/commands/get-context/index.js +0 -10
- package/dist/commands/init/index.d.ts +0 -10
- package/dist/commands/init/index.js +0 -75
- package/dist/commands/list-contexts/index.d.ts +0 -9
- package/dist/commands/list-contexts/index.js +0 -30
- package/dist/commands/login/index.d.ts +0 -10
- package/dist/commands/login/index.js +0 -62
- package/dist/commands/logout/index.d.ts +0 -6
- package/dist/commands/logout/index.js +0 -18
- package/dist/commands/openapi/index.d.ts +0 -26
- package/dist/commands/openapi/index.js +0 -307
- package/dist/commands/run/index.d.ts +0 -18
- package/dist/commands/run/index.js +0 -150
- package/dist/commands/serve/index.d.ts +0 -11
- package/dist/commands/serve/index.js +0 -51
- package/dist/commands/services/index.d.ts +0 -14
- package/dist/commands/services/index.js +0 -65
- package/dist/commands/set-context/index.d.ts +0 -17
- package/dist/commands/set-context/index.js +0 -59
- package/dist/commands/up/index.d.ts +0 -19
- package/dist/commands/up/index.js +0 -190
- package/oclif.manifest.json +0 -548
package/README.md
CHANGED
|
@@ -1,40 +1,156 @@
|
|
|
1
|
-
#
|
|
1
|
+
# qhubctl
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
We have installation instructions to guide you through the initial setup in our
|
|
5
|
-
[quickstart](https://docs.hub.kipu-quantum.com/quickstart.html) guide.
|
|
3
|
+
`qhubctl`, is a Command Line Interface (CLI) which lets you interact with the Kipu Quantum Hub from your terminal.
|
|
6
4
|
|
|
7
|
-
Detailed information about the supported commands can be found in our
|
|
8
|
-
[CLI reference](https://docs.hub.kipu-quantum.com/cli-reference.html).
|
|
5
|
+
Detailed information about the supported commands can be found in our [CLI reference](https://docs.hub.kipu-quantum.com/cli-reference.html).
|
|
9
6
|
|
|
10
7
|
## Installation
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
[Node version manager](https://github.com/nvm-sh/nvm) or a [Node installer](https://nodejs.org/en/download).
|
|
14
|
-
|
|
15
|
-
**Make sure you install Node.js version 18 or higher.**
|
|
16
|
-
|
|
17
|
-
Then install the qhubctl globally using npm:
|
|
9
|
+
Node.js 18 or higher is required.
|
|
10
|
+
Install it via a [Node version manager](https://github.com/nvm-sh/nvm) or a [Node installer](https://nodejs.org/en/download).
|
|
18
11
|
|
|
19
12
|
```bash
|
|
20
13
|
npm install -g @quantum-hub/qhubctl
|
|
21
14
|
```
|
|
22
15
|
|
|
23
|
-
|
|
16
|
+
> **Troubleshooting:** If you see `EACCES permissions` errors, use [nvm](https://github.com/nvm-sh/nvm) or see the [npm docs](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally).
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
24
19
|
|
|
25
|
-
```
|
|
20
|
+
```bash
|
|
26
21
|
qhubctl --help
|
|
22
|
+
qhubctl <command> --help
|
|
27
23
|
```
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
## Development
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
### Build
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
npm run build
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
Compiles TypeScript from `src/` to `dist/`.
|
|
35
|
+
The CLI entry point is `dist/main.js`.
|
|
36
|
+
|
|
37
|
+
### Run locally
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node dist/main.js <command>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Watch mode
|
|
44
|
+
|
|
45
|
+
For active development, run the TypeScript compiler in watch mode so every file save triggers an incremental rebuild:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`qhubctl` will reflect your latest changes on the next invocation without any manual rebuild step.
|
|
52
|
+
|
|
53
|
+
### Linking globally with npm link
|
|
54
|
+
|
|
55
|
+
`npm link` registers a global `qhubctl` symlink that points permanently to `dist/main.js` in your local checkout. Run it once after the initial setup:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm run build
|
|
59
|
+
npm link
|
|
60
|
+
qhubctl <command> # now resolves to your local build
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The symlink persists across rebuilds and watch-mode recompiles — you never need to run `npm link` again unless you delete and re-clone the repo or change the `"bin"` entry in `package.json`.
|
|
64
|
+
|
|
65
|
+
> **Note:** `npm link` and a globally installed published version of qhubctl are mutually exclusive — they occupy the same path. The last one to run wins. Use `which qhubctl` or `qhubctl --version` to confirm which is currently active.
|
|
66
|
+
|
|
67
|
+
To switch back to the published version from npm:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm unlink -g @quantum-hub/qhubctl # removes the local symlink
|
|
71
|
+
npm install -g @quantum-hub/qhubctl # installs the published version
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
To go back to your local dev version:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm link
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Run against staging
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node dist/main.js login --base-path https://api.hub.34.90.225.20.nip.io -t <personal access token>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Debug output
|
|
87
|
+
|
|
88
|
+
Set `DEBUG=*` to print all debug output:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
DEBUG=* node dist/main.js <command>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
On Windows, set the variable separately:
|
|
95
|
+
|
|
96
|
+
```cmd
|
|
97
|
+
set DEBUG=*
|
|
98
|
+
node dist\main.js <command>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Tests
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npm test # run tests + lint
|
|
105
|
+
npm run test:coverage # run tests with lcov coverage report
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Lint
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm run lint
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Generate docs
|
|
115
|
+
|
|
116
|
+
Produces a Markdown CLI reference at `docs/cli-reference.md` from the compiled program:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm run build
|
|
120
|
+
npm run docs:generate
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Regenerating the API client
|
|
124
|
+
|
|
125
|
+
The generated clients in `src/client/` are produced from the platform OpenAPI specs using [openapi-generator-cli](https://github.com/OpenAPITools/openapi-generator-cli).
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm install -g @openapitools/openapi-generator-cli
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Against production:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
openapi-generator-cli generate -g typescript-fetch \
|
|
135
|
+
-i https://api.hub.kipu-quantum.com/qc-catalog/docs \
|
|
136
|
+
-o src/client/kqh
|
|
137
|
+
|
|
138
|
+
openapi-generator-cli generate -g typescript-fetch \
|
|
139
|
+
-i https://api.hub.kipu-quantum.com/user-service/docs \
|
|
140
|
+
-o src/client/users
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Against staging:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
openapi-generator-cli generate -g typescript-fetch \
|
|
147
|
+
-i https://api.hub.34.90.225.20.nip.io/qc-catalog/docs \
|
|
148
|
+
-o src/client/kqh
|
|
149
|
+
|
|
150
|
+
openapi-generator-cli generate -g typescript-fetch \
|
|
151
|
+
-i https://api.hub.34.90.225.20.nip.io/user-service/docs \
|
|
152
|
+
-o src/client/users
|
|
153
|
+
```
|
|
38
154
|
|
|
39
155
|
## License
|
|
40
156
|
|
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
/* tslint:disable */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
-
/**
|
|
4
|
-
* Quantum Hub Platform API
|
|
5
|
-
* Part of the OpenAPI specification for the Quantum Hub Platform.
|
|
6
|
-
*
|
|
7
|
-
* The version of the OpenAPI document: v1
|
|
8
|
-
* Contact: info@anaqor.io
|
|
9
|
-
*
|
|
10
|
-
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
|
11
|
-
* https://openapi-generator.tech
|
|
12
|
-
* Do not edit the class manually.
|
|
13
|
-
*/
|
|
14
3
|
/**
|
|
15
4
|
* Check if a given object implements the UpdateWorkflowServiceRequest interface.
|
|
16
5
|
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { zipProjectFolder } from '../lib/up/zip.js';
|
|
5
|
+
import { spinner, stdout } from '../lib/output.js';
|
|
6
|
+
const EXAMPLES = `
|
|
7
|
+
Examples:
|
|
8
|
+
$ qhubctl compress`;
|
|
9
|
+
export const compressCommand = new Command('compress')
|
|
10
|
+
.description("Compresses the current project and creates a ZIP file. Use the '.dockerignore' file to exclude files and directories from the ZIP file.")
|
|
11
|
+
.addHelpText('after', EXAMPLES)
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const spin = spinner('Compressing project');
|
|
14
|
+
const targetDirectory = process.cwd();
|
|
15
|
+
const targetFilename = 'service.zip';
|
|
16
|
+
const targetFilePath = path.join(targetDirectory, '/', targetFilename);
|
|
17
|
+
if (fs.existsSync(targetFilePath)) {
|
|
18
|
+
fs.unlinkSync(targetFilePath);
|
|
19
|
+
}
|
|
20
|
+
await zipProjectFolder('./', targetDirectory, targetFilename);
|
|
21
|
+
spin.succeed('Project compressed');
|
|
22
|
+
stdout(`\nFile created at: ${targetFilePath}`);
|
|
23
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { input, select } from '@inquirer/prompts';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spinner, stdout } from '../lib/output.js';
|
|
6
|
+
import { requireAuth } from '../model/command.js';
|
|
7
|
+
import { HubPlatformClient, HubPlatformError } from '@quantum-hub/qhub-api/platform';
|
|
8
|
+
function extractError(err) {
|
|
9
|
+
const bodyObj = err.body;
|
|
10
|
+
if (bodyObj.errorMessage)
|
|
11
|
+
return bodyObj.errorMessage;
|
|
12
|
+
if (bodyObj.detail)
|
|
13
|
+
return bodyObj.detail;
|
|
14
|
+
if (bodyObj.message)
|
|
15
|
+
return bodyObj.message;
|
|
16
|
+
return JSON.stringify(err.body);
|
|
17
|
+
}
|
|
18
|
+
function printHttpSpecificError(datapoolId, statusCode, errorMessage) {
|
|
19
|
+
if (statusCode === 404)
|
|
20
|
+
throw new Error(`Data pool not found: ${datapoolId}`);
|
|
21
|
+
if (statusCode === 403)
|
|
22
|
+
throw new Error(`Access denied. You don't have permission to upload to data pool: ${datapoolId}`);
|
|
23
|
+
if (statusCode === 400)
|
|
24
|
+
throw new Error(`Bad request. Please check the data pool ID and file format. Details: ${errorMessage}`);
|
|
25
|
+
if (statusCode === 401)
|
|
26
|
+
throw new Error(`Authentication failed. Please login again using 'qhubctl login'`);
|
|
27
|
+
if (statusCode === 413)
|
|
28
|
+
throw new Error(`File too large. The file exceeds the maximum allowed size for data pool uploads`);
|
|
29
|
+
if (statusCode === 415)
|
|
30
|
+
throw new Error(`Unsupported file type. Please check if the file format is supported`);
|
|
31
|
+
throw new Error(`Failed to upload file (HTTP ${statusCode}): ${errorMessage}`);
|
|
32
|
+
}
|
|
33
|
+
function handleUploadError(err, datapoolId) {
|
|
34
|
+
if (err instanceof HubPlatformError) {
|
|
35
|
+
const statusCode = err.statusCode ?? 500;
|
|
36
|
+
let errorMessage = 'Unknown API error occurred';
|
|
37
|
+
if (err.body) {
|
|
38
|
+
if (typeof err.body === 'string')
|
|
39
|
+
errorMessage = err.body;
|
|
40
|
+
else if (typeof err.body === 'object')
|
|
41
|
+
errorMessage = extractError(err);
|
|
42
|
+
else if (err.message)
|
|
43
|
+
errorMessage = err.message;
|
|
44
|
+
}
|
|
45
|
+
else if (err.message) {
|
|
46
|
+
errorMessage = err.message;
|
|
47
|
+
}
|
|
48
|
+
printHttpSpecificError(datapoolId || 'unknown', statusCode, errorMessage);
|
|
49
|
+
}
|
|
50
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
51
|
+
throw new Error(`Unexpected Error: ${msg}`);
|
|
52
|
+
}
|
|
53
|
+
async function promptForDatapoolAction(client) {
|
|
54
|
+
try {
|
|
55
|
+
const dataPools = await client.dataPools.getDataPools();
|
|
56
|
+
if (dataPools.length === 0) {
|
|
57
|
+
stdout('No data pools found. Creating a new one...');
|
|
58
|
+
return 'create';
|
|
59
|
+
}
|
|
60
|
+
return await select({
|
|
61
|
+
message: 'What would you like to do?',
|
|
62
|
+
choices: [
|
|
63
|
+
{ name: 'Create a new data pool', value: 'create' },
|
|
64
|
+
{ name: 'Select an existing data pool', value: 'select' },
|
|
65
|
+
],
|
|
66
|
+
default: 'create',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return 'create';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function createDatapool(client, suggestedName) {
|
|
74
|
+
try {
|
|
75
|
+
const name = await input({
|
|
76
|
+
message: 'Enter a name for the new data pool:',
|
|
77
|
+
default: `Data Pool for ${suggestedName}`,
|
|
78
|
+
});
|
|
79
|
+
const datapool = await client.dataPools.createDataPool({ name });
|
|
80
|
+
stdout(`\u2713 Created new data pool: ${datapool.name} (${datapool.id})`);
|
|
81
|
+
return datapool.id;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (err instanceof HubPlatformError) {
|
|
85
|
+
const errorMessage = err.body || err.message || 'Unknown API error occurred';
|
|
86
|
+
throw new Error(`Failed to create data pool (HTTP ${err.statusCode}): ${JSON.stringify(errorMessage)}`);
|
|
87
|
+
}
|
|
88
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
89
|
+
throw new Error(`Failed to create data pool: ${msg}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function selectExistingDatapool(client) {
|
|
93
|
+
try {
|
|
94
|
+
const dataPools = await client.dataPools.getDataPools();
|
|
95
|
+
if (dataPools.length === 0) {
|
|
96
|
+
stdout('No existing data pools found. Creating a new one...');
|
|
97
|
+
return createDatapool(client, 'upload');
|
|
98
|
+
}
|
|
99
|
+
stdout('\nAvailable data pools:');
|
|
100
|
+
dataPools.forEach((pool, index) => {
|
|
101
|
+
stdout(`${index + 1}. ${pool.name} (${pool.id})`);
|
|
102
|
+
});
|
|
103
|
+
const selection = await input({
|
|
104
|
+
message: `Select a data pool (1-${dataPools.length}) or enter the ID directly:`,
|
|
105
|
+
});
|
|
106
|
+
const index = parseInt(selection, 10);
|
|
107
|
+
if (!isNaN(index) && index >= 1 && index <= dataPools.length) {
|
|
108
|
+
return dataPools[index - 1].id;
|
|
109
|
+
}
|
|
110
|
+
return selection;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new Error('Failed to retrieve data pools');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function ensureDatapoolExists(client, datapoolId, filePaths) {
|
|
117
|
+
if (!datapoolId) {
|
|
118
|
+
stdout('No data pool ID provided. You can create a new data pool or select an existing one.');
|
|
119
|
+
const action = await promptForDatapoolAction(client);
|
|
120
|
+
return action === 'create'
|
|
121
|
+
? createDatapool(client, filePaths[0])
|
|
122
|
+
: selectExistingDatapool(client);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
await client.dataPools.getDataPool({ id: datapoolId });
|
|
126
|
+
return datapoolId;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err instanceof HubPlatformError && err.statusCode === 404) {
|
|
130
|
+
stdout(`Data pool ${datapoolId} not found. Would you like to create a new data pool instead?`);
|
|
131
|
+
const action = await promptForDatapoolAction(client);
|
|
132
|
+
return action === 'create'
|
|
133
|
+
? createDatapool(client, filePaths[0])
|
|
134
|
+
: selectExistingDatapool(client);
|
|
135
|
+
}
|
|
136
|
+
handleUploadError(err, datapoolId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const EXAMPLES = `
|
|
140
|
+
Examples:
|
|
141
|
+
$ qhubctl datapool upload --file ./data/dataset.csv
|
|
142
|
+
$ qhubctl datapool upload --file ./data/dataset.csv --file ./data/metadata.json
|
|
143
|
+
$ qhubctl datapool upload -f ./data/dataset.csv -f ./data/metadata.json --datapool-id 550e8400-e29b-41d4-a716-446655440000`;
|
|
144
|
+
export const datapoolUploadCommand = new Command('upload')
|
|
145
|
+
.description('Upload files to a data pool')
|
|
146
|
+
.requiredOption('-f, --file <value...>', 'Path to a file to upload (can be specified multiple times)')
|
|
147
|
+
.option('-d, --datapool-id <value>', 'ID of the data pool to upload the files to (optional - will prompt to create a new one if not provided)')
|
|
148
|
+
.addHelpText('after', EXAMPLES)
|
|
149
|
+
.action(async (opts) => {
|
|
150
|
+
const userConfig = await requireAuth();
|
|
151
|
+
const baseUrl = userConfig.endpoint?.basePath
|
|
152
|
+
? `${userConfig.endpoint.basePath}/qc-catalog`
|
|
153
|
+
: undefined;
|
|
154
|
+
const apiKey = userConfig.auth?.value;
|
|
155
|
+
const organizationId = userConfig.context?.isOrganization ? userConfig.context.id : undefined;
|
|
156
|
+
const client = new HubPlatformClient({ baseUrl, apiKey, organizationId });
|
|
157
|
+
const filePaths = opts.file;
|
|
158
|
+
for (const filePath of filePaths) {
|
|
159
|
+
if (!fs.existsSync(filePath))
|
|
160
|
+
throw new Error(`File not found: ${filePath}`);
|
|
161
|
+
try {
|
|
162
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new Error(`Cannot read file: ${filePath}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const datapoolId = await ensureDatapoolExists(client, opts.datapoolId, filePaths);
|
|
169
|
+
for (const filePath of filePaths) {
|
|
170
|
+
const fileName = path.basename(filePath);
|
|
171
|
+
const spin = spinner(`Uploading ${fileName} to data pool ${datapoolId}`);
|
|
172
|
+
try {
|
|
173
|
+
const fileStream = fs.createReadStream(filePath);
|
|
174
|
+
const result = await client.dataPools.addDataPoolFile({ id: datapoolId, file: fileStream });
|
|
175
|
+
spin.succeed('\u2713');
|
|
176
|
+
stdout(`Successfully uploaded ${fileName} to data pool ${datapoolId}`);
|
|
177
|
+
stdout(`File ID: ${result.id}`);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
spin.fail('\u2717');
|
|
181
|
+
handleUploadError(err, datapoolId);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { requireAuth } from '../model/command.js';
|
|
3
|
+
const EXAMPLES = `
|
|
4
|
+
Examples:
|
|
5
|
+
$ qhubctl get-context`;
|
|
6
|
+
export const getContextCommand = new Command('get-context')
|
|
7
|
+
.description('Get the current context, i.e., the personal or organization account you are currently working with.')
|
|
8
|
+
.addHelpText('after', EXAMPLES)
|
|
9
|
+
.action(async () => {
|
|
10
|
+
// requireAuth prints "Current context: ..." as a side-effect
|
|
11
|
+
await requireAuth();
|
|
12
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_STARTER, promptUserInput } from '../lib/init-user-input.js';
|
|
5
|
+
import { randomName } from '../lib/random-name.js';
|
|
6
|
+
import { downloadRepository, extractStarter } from '../lib/repository.js';
|
|
7
|
+
import { error, stdout } from '../lib/output.js';
|
|
8
|
+
import { Runtime } from '../model/managed-service-config.js';
|
|
9
|
+
import { writeServiceConfig } from '../service/service-config-service.js';
|
|
10
|
+
const EXAMPLES = `
|
|
11
|
+
Examples:
|
|
12
|
+
$ qhubctl init`;
|
|
13
|
+
export const initCommand = new Command('init')
|
|
14
|
+
.description('Bootstrap project to create a service.')
|
|
15
|
+
.option('--name <value>', 'The name of the service')
|
|
16
|
+
.option('--non-interactive', 'Run it in non-interactive mode', false)
|
|
17
|
+
.addHelpText('after', EXAMPLES)
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
const nonInteractive = opts.nonInteractive;
|
|
20
|
+
let name = opts.name ?? randomName();
|
|
21
|
+
const input = nonInteractive ? {
|
|
22
|
+
cpu: 1,
|
|
23
|
+
descriptionFile: 'README.md',
|
|
24
|
+
gpu: undefined,
|
|
25
|
+
memory: 1,
|
|
26
|
+
name: name,
|
|
27
|
+
starter: DEFAULT_STARTER,
|
|
28
|
+
} : await promptUserInput(name);
|
|
29
|
+
name = input.name || name;
|
|
30
|
+
const targetDirectory = input.starter ? path.join(process.cwd(), name) : path.join(process.cwd());
|
|
31
|
+
if (input.starter && fs.existsSync(targetDirectory)) {
|
|
32
|
+
error(`Destination ${targetDirectory} already exists. Please choose another name.`);
|
|
33
|
+
}
|
|
34
|
+
const serviceConfig = {
|
|
35
|
+
name,
|
|
36
|
+
descriptionFile: input.descriptionFile ?? 'README.md',
|
|
37
|
+
resources: {
|
|
38
|
+
cpu: input.cpu,
|
|
39
|
+
memory: input.memory,
|
|
40
|
+
},
|
|
41
|
+
runtime: input.starter ? input.starter.runtime : Runtime.PYTHON_TEMPLATE,
|
|
42
|
+
};
|
|
43
|
+
if (input.gpu) {
|
|
44
|
+
serviceConfig.resources.gpu = {
|
|
45
|
+
count: 1,
|
|
46
|
+
type: input.gpu,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (input.starter) {
|
|
50
|
+
fs.mkdirSync(targetDirectory);
|
|
51
|
+
}
|
|
52
|
+
writeServiceConfig(targetDirectory, serviceConfig);
|
|
53
|
+
if (input.starter) {
|
|
54
|
+
const zip = await downloadRepository(input.starter.url);
|
|
55
|
+
const starterName = path.basename(input.starter.url, '.zip');
|
|
56
|
+
extractStarter(zip, starterName, targetDirectory);
|
|
57
|
+
}
|
|
58
|
+
stdout('\u{1F389} Initialized project. Happy hacking!');
|
|
59
|
+
if (input.starter) {
|
|
60
|
+
stdout('\n Next steps:');
|
|
61
|
+
stdout(` \u{2022} cd ${name}`);
|
|
62
|
+
stdout(' \u{2022} qhubctl up (deploys your code as a service to Kipu Quantum Hub)');
|
|
63
|
+
stdout(' \u{2022} qhubctl run (executes your service using the data from the input directory)');
|
|
64
|
+
}
|
|
65
|
+
stdout('');
|
|
66
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import Table from 'tty-table';
|
|
3
|
+
import { spinner, stdout } 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 list-contexts`;
|
|
9
|
+
export const listContextsCommand = new Command('list-contexts')
|
|
10
|
+
.description('Retrieves the available contexts, i.e., the personal or organizational accounts available to you to work with.')
|
|
11
|
+
.addHelpText('after', EXAMPLES)
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const userConfig = await requireAuth();
|
|
14
|
+
const quantumHubService = new QuantumHubService(userConfig);
|
|
15
|
+
const spin = spinner('Loading available contexts');
|
|
16
|
+
const accounts = await quantumHubService.getAccounts();
|
|
17
|
+
spin.succeed('Contexts loaded');
|
|
18
|
+
const tableData = accounts
|
|
19
|
+
.map(account => ({ displayName: account.displayName, id: account.id, isOrganization: account.isOrganization }));
|
|
20
|
+
const header = [
|
|
21
|
+
{ value: "displayName", alias: "Name", align: "left" },
|
|
22
|
+
{ value: "id", alias: "ID", align: "left" },
|
|
23
|
+
{ value: "isOrganization", alias: "Account", width: 14, formatter: (value) => value ? "Organization" : "Personal" },
|
|
24
|
+
];
|
|
25
|
+
const t = Table(header, tableData);
|
|
26
|
+
stdout(t.render());
|
|
27
|
+
stdout();
|
|
28
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { password } from '@inquirer/prompts';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { spinner } from '../lib/output.js';
|
|
4
|
+
import { configDir, getUserConfig, showCustomBasePathWarning } from '../model/command.js';
|
|
5
|
+
import { AuthType, defaultBasePath } from '../model/user-config.js';
|
|
6
|
+
import AuthService from '../service/auth-service.js';
|
|
7
|
+
import { writeUserConfig } from '../service/user-config-service.js';
|
|
8
|
+
const EXAMPLES = `
|
|
9
|
+
Examples:
|
|
10
|
+
$ qhubctl login -t <personal access token>
|
|
11
|
+
$ qhubctl login -t <personal access token> --base-path ${defaultBasePath}`;
|
|
12
|
+
export const loginCommand = new Command('login')
|
|
13
|
+
.description('Login with your credentials')
|
|
14
|
+
.option('-t, --token <value>', 'Your personal access token')
|
|
15
|
+
.option('--base-path <value>', 'Custom base path for authentication (development)')
|
|
16
|
+
.addHelpText('after', EXAMPLES)
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
let { token } = opts;
|
|
19
|
+
const { basePath } = opts;
|
|
20
|
+
if (!token) {
|
|
21
|
+
token = await password({ message: 'Enter or paste your personal access token?', mask: true });
|
|
22
|
+
}
|
|
23
|
+
if (!token) {
|
|
24
|
+
throw new Error('Invalid credentials. Please try again.');
|
|
25
|
+
}
|
|
26
|
+
if (basePath) {
|
|
27
|
+
showCustomBasePathWarning();
|
|
28
|
+
}
|
|
29
|
+
const spin = spinner('Login...');
|
|
30
|
+
const userConfig = getUserConfig();
|
|
31
|
+
const tempUserConfig = basePath ? {
|
|
32
|
+
...userConfig,
|
|
33
|
+
endpoint: {
|
|
34
|
+
basePath,
|
|
35
|
+
defaultHeaders: userConfig.endpoint?.defaultHeaders || {}
|
|
36
|
+
}
|
|
37
|
+
} : userConfig;
|
|
38
|
+
const user = await new AuthService(tempUserConfig).authenticate(token);
|
|
39
|
+
const newUserConfig = {
|
|
40
|
+
...userConfig,
|
|
41
|
+
auth: {
|
|
42
|
+
type: AuthType.API_KEY,
|
|
43
|
+
value: token,
|
|
44
|
+
},
|
|
45
|
+
context: {
|
|
46
|
+
id: user.id,
|
|
47
|
+
displayName: `${user.firstname} ${user.lastname}`,
|
|
48
|
+
isOrganization: false,
|
|
49
|
+
},
|
|
50
|
+
...(basePath && {
|
|
51
|
+
endpoint: {
|
|
52
|
+
basePath,
|
|
53
|
+
defaultHeaders: userConfig.endpoint?.defaultHeaders || {}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
};
|
|
57
|
+
writeUserConfig(configDir, newUserConfig);
|
|
58
|
+
spin.succeed('Login successful');
|
|
59
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { spinner } from '../lib/output.js';
|
|
3
|
+
import { configDir } from '../model/command.js';
|
|
4
|
+
import { writeUserConfig } from '../service/user-config-service.js';
|
|
5
|
+
import { getUserConfig } from '../model/command.js';
|
|
6
|
+
const EXAMPLES = `
|
|
7
|
+
Examples:
|
|
8
|
+
$ qhubctl logout`;
|
|
9
|
+
export const logoutCommand = new Command('logout')
|
|
10
|
+
.description('Logout')
|
|
11
|
+
.addHelpText('after', EXAMPLES)
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const spin = spinner('Logout');
|
|
14
|
+
writeUserConfig(configDir, {
|
|
15
|
+
...getUserConfig(),
|
|
16
|
+
auth: undefined,
|
|
17
|
+
context: undefined,
|
|
18
|
+
});
|
|
19
|
+
spin.succeed('Logout successful');
|
|
20
|
+
});
|