Files
qinglong/cli/scripts/verify-package.cjs
T
whyour 801a71d740 feat(cli): cover OpenAPI with a remote npm CLI and internal panel tools (#3074)
* feat(cli): add unified Commander CLI for QingLong 2.x

* fix(cli): publish via npm and address security review feedback

* ci(cli): package npm artifacts and remove evaluation collateral

* test(cli): use a fixed shell fixture for log retention

* refactor(cli): separate remote npm client from panel tools

* feat(cli): cover active panel OpenAPI resources

* docs(cli): unify authentication and skill guidance

* refactor(cli): isolate internal commands and generate Commander help

* refactor(cli): organize remote and internal modules by responsibility

* ci(cli): publish verified npm archives from master

* fix(cli): publish under the whyour npm scope

* ci: use npm trusted publishing for both packages

* docs: introduce the published CLI on the project homepage

* fix(cli): preserve server log truncation and correct login hints

* fix(cli): accept dashboard record request bodies

* fix(cli): preserve stdin for local task execution

* fix(cli): resolve task executables after changing directory

* fix(cli): preserve shell function tasks and sanitize test failures

* fix(cli): preserve shell hook state and resolve workdir after hooks

* fix(cli): preserve cleanup across shared shell task timeouts

* fix(cli): isolate shell control descriptors and reap timed-out descendants
2026-09-25 23:24:41 +08:00

125 lines
4.2 KiB
JavaScript

const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync, spawnSync } = require('node:child_process');
const assert = require('node:assert/strict');
const root = path.resolve(__dirname, '..');
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-package-'));
const run = (program, args, options = {}) =>
execFileSync(program, args, {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 60000,
...options,
});
try {
const cache = path.join(temp, 'cache');
const manifest = JSON.parse(
run(
'npm',
[
'pack',
'--json',
'--ignore-scripts',
'--cache',
cache,
'--pack-destination',
temp,
],
{ cwd: root },
),
)[0];
for (const name of ['README.md', 'README.en.md', 'LICENSE', 'dist/npm/commander-LICENSE'])
assert.ok(manifest.files.some((file) => file.path === name), name);
assert.ok(manifest.files.every((file) => /^(dist\/npm\/|skills\/qinglong-cli\/|README(?:\.en)?\.md$|LICENSE$|package\.json$)/.test(file.path)));
assert.ok(
manifest.files.some((file) => file.path === 'skills/qinglong-cli/SKILL.md'),
);
assert.ok(
!manifest.files.some(
(file) => file.path.startsWith('src/') || file.path.startsWith('test/'),
),
);
const prefix = path.join(temp, 'install');
run(
'npm',
[
'install',
'--offline',
'--ignore-scripts',
'--no-audit',
'--no-fund',
'--cache',
cache,
'--prefix',
prefix,
path.join(temp, manifest.filename),
],
{ cwd: temp },
);
const installed = path.join(prefix, 'node_modules/@whyour/qinglong-cli');
const metadata = JSON.parse(
fs.readFileSync(path.join(installed, 'package.json'), 'utf8'),
);
assert.equal(Object.keys(metadata.dependencies || {}).length, 0);
assert.equal(metadata.bin['ql-dev-cli'], undefined);
assert.ok(!manifest.files.some(file => /^dist\/developer(?:\/|\.)/.test(file.path)));
assert.deepEqual(metadata.bin, { ql: 'dist/npm/ql.js' });
for (const reference of ['panel.md', 'openapi.md'])
assert.ok(fs.existsSync(path.join(installed, 'skills/qinglong-cli/references', reference)));
assert.equal(
fs.readFileSync(
path.join(installed, 'skills/qinglong-cli/SKILL.md'),
'utf8',
),
fs.readFileSync(path.join(root, 'skills/qinglong-cli/SKILL.md'), 'utf8'),
);
const entries = [];
for (const [name, target] of Object.entries(metadata.bin)) {
const entry = path.join(prefix, 'node_modules/.bin', name);
assert.equal(
fs.realpathSync(entry),
fs.realpathSync(path.join(installed, target)),
);
const output = run(entry, ['--help'], {
cwd: temp,
env: { ...process.env, QL_DIR: '', QL_DATA_DIR: '' },
});
assert.match(output, /Usage:|用法|usage/i);
entries.push(name);
}
assert.ok(!manifest.files.some(file => file.path.includes('qinglong-local')));
for (const args of [['reload'], ['resetpwd', 'fixture'], ['local', 'check'], ['task', 'exec', 'fixture.js'], ['task', 'fixture.js'], ['repo', 'fixture'], ['dev', 'release']]) {
const result = spawnSync(process.execPath, [path.join(installed, metadata.bin.ql), ...args, '--json'], {
cwd: temp, encoding: 'utf8', timeout: 10000,
env: { PATH: process.env.PATH, QL_CLI_CONFIG: path.join(temp, 'missing.json'), QL_DIR: '/nonexistent-panel' },
});
assert.equal(result.status, 2, JSON.stringify(args) + result.stderr);
assert.equal(result.stdout, '');
assert.equal(JSON.parse(result.stderr).code, 2);
}
// CI uploads the exact archive whose isolated installation passed above.
if (process.env.QL_CLI_PACKAGE_OUTPUT) {
const output = path.resolve(process.env.QL_CLI_PACKAGE_OUTPUT);
fs.mkdirSync(output, { recursive: true });
fs.copyFileSync(path.join(temp, manifest.filename), path.join(output, manifest.filename));
}
process.stdout.write(
JSON.stringify(
{
entries,
packedBytes: manifest.size,
unpackedBytes: manifest.unpackedSize,
runtimeDependencies: 0,
standaloneInstall: true,
},
null,
2,
) + '\n',
);
} finally {
fs.rmSync(temp, { recursive: true, force: true });
}