@quantum-hub/qhubctl 1.19.1 → 2.0.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/dist/commands/compress.js +21 -3
- package/dist/commands/openapi.js +4 -4
- package/dist/commands/up.js +7 -0
- package/dist/lib/up/ignore.js +8 -6
- package/dist/lib/up/required-files.d.ts +8 -0
- package/dist/lib/up/required-files.js +48 -0
- package/dist/model/managed-service-config.d.ts +0 -1
- package/dist/model/managed-service-config.js +0 -1
- package/package.json +1 -1
|
@@ -2,18 +2,36 @@ import { Command } from 'commander';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { zipProjectFolder } from '../lib/up/zip.js';
|
|
5
|
-
import {
|
|
5
|
+
import { enforceRequiredFiles } from '../lib/up/required-files.js';
|
|
6
|
+
import { error, spinner, stdout } from '../lib/output.js';
|
|
6
7
|
const EXAMPLES = `
|
|
7
8
|
Examples:
|
|
8
|
-
$ qhubctl compress
|
|
9
|
+
$ qhubctl compress
|
|
10
|
+
|
|
11
|
+
The '.qhubignore' file uses gitignore syntax. To ship only 'src' and the
|
|
12
|
+
Dockerfile, re-include the directory AND its contents:
|
|
13
|
+
|
|
14
|
+
*
|
|
15
|
+
!src
|
|
16
|
+
!src/**
|
|
17
|
+
!Dockerfile
|
|
18
|
+
|
|
19
|
+
These paths are always excluded: .git, node_modules, .venv, __pycache__, service.zip`;
|
|
9
20
|
export const compressCommand = new Command('compress')
|
|
10
|
-
.description("Compresses the current project and creates a ZIP file. Use
|
|
21
|
+
.description("Compresses the current project and creates a ZIP file. Use a '.qhubignore' file (gitignore syntax) to exclude files and directories from the ZIP file.")
|
|
11
22
|
.addHelpText('after', EXAMPLES)
|
|
12
23
|
.action(async () => {
|
|
13
24
|
const spin = spinner('Compressing project');
|
|
14
25
|
const targetDirectory = process.cwd();
|
|
15
26
|
const targetFilename = 'service.zip';
|
|
16
27
|
const targetFilePath = path.join(targetDirectory, '/', targetFilename);
|
|
28
|
+
try {
|
|
29
|
+
await enforceRequiredFiles(targetDirectory);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
spin.fail('Compression aborted');
|
|
33
|
+
error(err.message);
|
|
34
|
+
}
|
|
17
35
|
if (fs.existsSync(targetFilePath)) {
|
|
18
36
|
fs.unlinkSync(targetFilePath);
|
|
19
37
|
}
|
package/dist/commands/openapi.js
CHANGED
|
@@ -68,7 +68,7 @@ function isProjectSupported(opts) {
|
|
|
68
68
|
return false;
|
|
69
69
|
}
|
|
70
70
|
const hasExplicitEntrypoint = !!(opts.entrypoint || opts.package || opts.method);
|
|
71
|
-
const supportedRuntimes = [Runtime.PYTHON_TEMPLATE, Runtime.
|
|
71
|
+
const supportedRuntimes = [Runtime.PYTHON_TEMPLATE, Runtime.DOCKER];
|
|
72
72
|
if (hasExplicitEntrypoint) {
|
|
73
73
|
const isSupported = supportedRuntimes.includes(serviceConfig.runtime);
|
|
74
74
|
if (!isSupported) {
|
|
@@ -76,7 +76,7 @@ function isProjectSupported(opts) {
|
|
|
76
76
|
warn('');
|
|
77
77
|
warn('Supported configurations:');
|
|
78
78
|
warn(' \u2022 PYTHON_TEMPLATE: Automatic (requires src/program.py)');
|
|
79
|
-
warn(' \u2022
|
|
79
|
+
warn(' \u2022 DOCKER: With explicit entrypoint flags');
|
|
80
80
|
warn('');
|
|
81
81
|
warn('Examples:');
|
|
82
82
|
warn(' qhubctl openapi --entrypoint=myapp.main:handler');
|
|
@@ -176,7 +176,7 @@ Examples:
|
|
|
176
176
|
export const openapiCommand = new Command('openapi')
|
|
177
177
|
.description('Generates the OpenAPI description for your project based on the parameter and return types of your run() method. ' +
|
|
178
178
|
'The output of this command will be used when creating or updating your service. ' +
|
|
179
|
-
'Supports PYTHON_TEMPLATE (with src/program.py)
|
|
179
|
+
'Supports PYTHON_TEMPLATE (with src/program.py) and DOCKER runtime (with explicit entrypoint). ' +
|
|
180
180
|
'Requires uv and a valid uv project in the workspace. The "qhub-commons" dependency is installed/upgraded automatically as a dependency.')
|
|
181
181
|
.option('-f, --force', 'Overwrite the output file if it already exists', false)
|
|
182
182
|
.option('--file <value>', 'The file to write the OpenAPI description to')
|
|
@@ -204,7 +204,7 @@ export const openapiCommand = new Command('openapi')
|
|
|
204
204
|
if (!shouldGenerateFile) {
|
|
205
205
|
warn(`Cannot generate OpenAPI description from your code. Supported configurations:\n` +
|
|
206
206
|
' \u2022 PYTHON_TEMPLATE runtime with "./src/program.py" file\n' +
|
|
207
|
-
' \u2022 DOCKER
|
|
207
|
+
' \u2022 DOCKER or PYTHON_TEMPLATE runtimes with explicit entrypoint (--entrypoint, or (--package, --method))');
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
210
210
|
// Ensure qhub-commons is installed (or upgraded) in the project venv
|
package/dist/commands/up.js
CHANGED
|
@@ -7,6 +7,7 @@ import { HubPlatform } from '@quantum-hub/qhub-api/platform';
|
|
|
7
7
|
import { streamToBlob } from '../lib/stream-to-blob.js';
|
|
8
8
|
import { getDescriptionText } from '../lib/up/get-description.js';
|
|
9
9
|
import { zipProjectFolder } from '../lib/up/zip.js';
|
|
10
|
+
import { enforceRequiredFiles } from '../lib/up/required-files.js';
|
|
10
11
|
import { handleError } from '../lib/error.js';
|
|
11
12
|
import { error, spinner, stdout, warn } from '../lib/output.js';
|
|
12
13
|
import { requireAuth } from '../model/command.js';
|
|
@@ -129,6 +130,12 @@ export const upCommand = new Command('up')
|
|
|
129
130
|
if (!createNewService)
|
|
130
131
|
process.exit(0);
|
|
131
132
|
}
|
|
133
|
+
try {
|
|
134
|
+
await enforceRequiredFiles(process.cwd());
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
error(err.message);
|
|
138
|
+
}
|
|
132
139
|
const serviceFiles = {
|
|
133
140
|
openapi: await getOpenapi(),
|
|
134
141
|
descriptionText: await getDescriptionText(process.cwd()),
|
package/dist/lib/up/ignore.js
CHANGED
|
@@ -3,13 +3,15 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
const readFile = promisify(fs.readFile);
|
|
6
|
+
// Always excluded from the upload archive. Added before user patterns so a
|
|
7
|
+
// user can re-include any of them with a negation (e.g. `!node_modules`).
|
|
8
|
+
const DEFAULT_IGNORES = ['.git', 'node_modules', '.venv', '__pycache__', 'service.zip'];
|
|
6
9
|
export async function createIgnoreFileChecker(filePath = process.cwd()) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
const ig = ignore().add(DEFAULT_IGNORES);
|
|
11
|
+
const ignoreFilePath = path.join(filePath, '.qhubignore');
|
|
12
|
+
if (fs.existsSync(ignoreFilePath)) {
|
|
13
|
+
const ignoreFileContents = await readFile(ignoreFilePath, 'utf8');
|
|
14
|
+
ig.add(ignoreFileContents.split('\n'));
|
|
10
15
|
}
|
|
11
|
-
const ignoreFileContents = await readFile(ignoreFilePath, 'utf8');
|
|
12
|
-
const ig = ignore().add(ignoreFileContents.split('\n'));
|
|
13
|
-
ig.add('service.zip');
|
|
14
16
|
return (fullPath) => ig.ignores(fullPath);
|
|
15
17
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface RequiredFilesResult {
|
|
2
|
+
skipped: boolean;
|
|
3
|
+
runtimeMissing: boolean;
|
|
4
|
+
missing: string[];
|
|
5
|
+
excluded: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function evaluateRequiredFiles(workingDirectory: string): Promise<RequiredFilesResult>;
|
|
8
|
+
export declare function enforceRequiredFiles(workingDirectory: string): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Runtime } from '../../model/managed-service-config.js';
|
|
4
|
+
import { readServiceConfig } from '../../service/service-config-service.js';
|
|
5
|
+
import { createIgnoreFileChecker } from './ignore.js';
|
|
6
|
+
import { warn } from '../output.js';
|
|
7
|
+
const REQUIRED_FILES = {
|
|
8
|
+
[Runtime.DOCKER]: ['Dockerfile'],
|
|
9
|
+
[Runtime.PYTHON_TEMPLATE]: ['requirements.txt', 'src/program.py'],
|
|
10
|
+
};
|
|
11
|
+
export async function evaluateRequiredFiles(workingDirectory) {
|
|
12
|
+
const result = { skipped: false, runtimeMissing: false, missing: [], excluded: [] };
|
|
13
|
+
if (!fs.existsSync(path.join(workingDirectory, 'qhub.json'))) {
|
|
14
|
+
result.skipped = true;
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
const { runtime } = readServiceConfig(workingDirectory);
|
|
18
|
+
if (!runtime) {
|
|
19
|
+
result.runtimeMissing = true;
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
const required = REQUIRED_FILES[runtime] ?? [];
|
|
23
|
+
const isIgnored = await createIgnoreFileChecker(workingDirectory);
|
|
24
|
+
for (const file of required) {
|
|
25
|
+
const onDisk = fs.existsSync(path.join(workingDirectory, file));
|
|
26
|
+
if (onDisk && isIgnored(file)) {
|
|
27
|
+
result.excluded.push(file);
|
|
28
|
+
}
|
|
29
|
+
else if (!onDisk) {
|
|
30
|
+
result.missing.push(file);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
export async function enforceRequiredFiles(workingDirectory) {
|
|
36
|
+
const { runtimeMissing, missing, excluded } = await evaluateRequiredFiles(workingDirectory);
|
|
37
|
+
if (runtimeMissing) {
|
|
38
|
+
warn('No "runtime" set in qhub.json; skipping required-file validation.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
warn(`Required file(s) missing for the configured runtime: ${missing.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
if (excluded.length > 0) {
|
|
45
|
+
throw new Error(`Required file(s) are present but excluded by .qhubignore: ${excluded.join(', ')}. ` +
|
|
46
|
+
`Re-include them with a negation (e.g. "!Dockerfile").`);
|
|
47
|
+
}
|
|
48
|
+
}
|