mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-28 09:02:12 +08:00
* 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
75 lines
2.5 KiB
JavaScript
75 lines
2.5 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs/promises');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { createContext } = require('/opt/qinglong-cli/dist/internal/runtime/context');
|
|
const { LocalApi } = require('/opt/qinglong-cli/dist/internal/runtime/api');
|
|
assert.equal(process.env.QL_PANEL_INTEGRATION, '1');
|
|
(async () => {
|
|
const api = new LocalApi(createContext({ root: '/ql' }, process.env));
|
|
await fs.writeFile(
|
|
'/ql/data/scripts/image-selection.js',
|
|
'console.log("IMAGE_PARENT="+JSON.stringify(require("node:fs").readFileSync("/proc/"+process.ppid+"/cmdline","utf8").split("\\0")));',
|
|
);
|
|
const task = (
|
|
await api.call('crons', 'POST', {
|
|
name: 'image selection fixture',
|
|
command: 'task image-selection.js',
|
|
schedule: '0 0 1 1 *',
|
|
})
|
|
).data;
|
|
try {
|
|
await api.call('crons/run', 'PUT', [task.id]);
|
|
let log = '';
|
|
for (let i = 0; i < 50; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
log = (await api.call(`crons/${task.id}/log`)).data;
|
|
if (log.includes('IMAGE_PARENT=')) break;
|
|
}
|
|
const parent = JSON.parse(log.match(/IMAGE_PARENT=(.*)/)[1]);
|
|
assert.match(parent[0], /(?:^|\/)node$/);
|
|
assert.equal(parent[1], '/root/bin/task');
|
|
assert.match(
|
|
await fs.readFile('/root/bin/task', 'utf8'),
|
|
/\/opt\/qinglong-cli\/dist\/task.js/,
|
|
);
|
|
const reload = spawnSync('ql', ['reload'], {
|
|
encoding: 'utf8',
|
|
timeout: 30000,
|
|
});
|
|
assert.equal(reload.status, 0, reload.stderr);
|
|
assert.equal(JSON.parse(reload.stdout).code, 200);
|
|
let healthy = false;
|
|
for (let i = 0; i < 50; i++) {
|
|
try {
|
|
healthy =
|
|
(
|
|
await (
|
|
await fetch('http://127.0.0.1:5700/api/system', {
|
|
signal: AbortSignal.timeout(1000),
|
|
})
|
|
).json()
|
|
).code === 200;
|
|
} catch {}
|
|
if (healthy) break;
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
}
|
|
assert.ok(healthy);
|
|
const help = spawnSync('ql', ['--help'], { encoding: 'utf8' });
|
|
assert.equal(help.status, 0);
|
|
assert.match(help.stdout, /ql-compat/);
|
|
console.log(
|
|
JSON.stringify({
|
|
packagedTaskScheduled: true,
|
|
packagedReload: true,
|
|
healthyAfterReload: true,
|
|
selectionRetained: true,
|
|
}),
|
|
);
|
|
} finally {
|
|
await api.call('crons', 'DELETE', [task.id]);
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error.stack);
|
|
process.exitCode = 1;
|
|
});
|