@tiangong-ai/cli 0.0.49 → 0.0.51
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/AGENTS.md +2 -2
- package/README.md +72 -4
- package/dist/research/orchestration.js +17 -4
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/setup-command.js +52 -0
- package/dist/research/setup-command.js.map +1 -1
- package/dist/research/workspace/acquisition.js +70 -5
- package/dist/research/workspace/acquisition.js.map +1 -1
- package/dist/research/workspace/evidence-ledger.js +0 -2
- package/dist/research/workspace/evidence-ledger.js.map +1 -1
- package/dist/research/workspace/projects.js +203 -76
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.js +5 -3
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/setup-audit-bundle.d.ts +69 -0
- package/dist/research/workspace/setup-audit-bundle.js +1695 -0
- package/dist/research/workspace/setup-audit-bundle.js.map +1 -0
- package/dist/research/workspace/setup-catalog.d.ts +1 -1
- package/dist/research/workspace/setup-catalog.js +176 -9
- package/dist/research/workspace/setup-catalog.js.map +1 -1
- package/dist/research/workspace/setup.js +414 -23
- package/dist/research/workspace/setup.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { chmod, link, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, } from "node:fs/promises";
|
|
4
|
-
import { hostname, homedir, platform } from "node:os";
|
|
5
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { chmod, link, lstat, mkdtemp, mkdir, open, readFile, readdir, realpath, rename, rm, } from "node:fs/promises";
|
|
4
|
+
import { hostname, homedir, platform, tmpdir } from "node:os";
|
|
5
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
7
|
import { CliError } from "../../errors.js";
|
|
8
8
|
import { loadCapabilityDeclarations } from "./capabilities.js";
|
|
@@ -1033,7 +1033,7 @@ async function containsUnmanagedPathCliFallback(root) {
|
|
|
1033
1033
|
};
|
|
1034
1034
|
return inspectDirectory(root, 0);
|
|
1035
1035
|
}
|
|
1036
|
-
async function findAmbientExecutable(environment, executable) {
|
|
1036
|
+
async function findAmbientExecutable(environment, executable, options = {}) {
|
|
1037
1037
|
const pathValue = environment.PATH;
|
|
1038
1038
|
if (!pathValue)
|
|
1039
1039
|
return null;
|
|
@@ -1042,7 +1042,7 @@ async function findAmbientExecutable(environment, executable) {
|
|
|
1042
1042
|
if (!directory)
|
|
1043
1043
|
continue;
|
|
1044
1044
|
for (const suffix of suffixes) {
|
|
1045
|
-
const candidate =
|
|
1045
|
+
const candidate = resolve(directory, `${executable}${suffix}`);
|
|
1046
1046
|
const info = await lstat(candidate).catch(() => undefined);
|
|
1047
1047
|
if (!info || (!info.isFile() && !info.isSymbolicLink()))
|
|
1048
1048
|
continue;
|
|
@@ -1050,8 +1050,12 @@ async function findAmbientExecutable(environment, executable) {
|
|
|
1050
1050
|
if (!resolved)
|
|
1051
1051
|
continue;
|
|
1052
1052
|
const resolvedInfo = await lstat(resolved).catch(() => undefined);
|
|
1053
|
-
if (resolvedInfo?.isFile() && !resolvedInfo.isSymbolicLink())
|
|
1054
|
-
|
|
1053
|
+
if (resolvedInfo?.isFile() && !resolvedInfo.isSymbolicLink()) {
|
|
1054
|
+
const path = options.preserveLeafSymlink
|
|
1055
|
+
? join(await realpath(dirname(candidate)), basename(candidate))
|
|
1056
|
+
: resolved;
|
|
1057
|
+
return { path, ignoredByExactInvocation: true };
|
|
1058
|
+
}
|
|
1055
1059
|
}
|
|
1056
1060
|
}
|
|
1057
1061
|
return null;
|
|
@@ -1446,7 +1450,16 @@ export async function doctorResearchSetup(workspace, options = {}) {
|
|
|
1446
1450
|
: `Run research setup credential set --id ${credential.id} --prompt --workspace ${root}.`,
|
|
1447
1451
|
});
|
|
1448
1452
|
}
|
|
1449
|
-
await
|
|
1453
|
+
const authoringRuntime = await resolveAuthoringRuntime(selected, environment);
|
|
1454
|
+
await appendDependencyChecks(checks, plan, selected, runner, root, environment, authoringRuntime);
|
|
1455
|
+
await appendAuthoringCanaryChecks(checks, {
|
|
1456
|
+
plan,
|
|
1457
|
+
selected,
|
|
1458
|
+
runner,
|
|
1459
|
+
root,
|
|
1460
|
+
environment,
|
|
1461
|
+
runtime: authoringRuntime,
|
|
1462
|
+
});
|
|
1450
1463
|
let capabilityDoctor = null;
|
|
1451
1464
|
const blockingBeforeLive = checks
|
|
1452
1465
|
.map((check) => normalizeSetupDoctorCheck(check, selected))
|
|
@@ -3499,6 +3512,10 @@ function setupDomainReadiness(checks, scope) {
|
|
|
3499
3512
|
return "NOT_REQUIRED";
|
|
3500
3513
|
if (matching.some((check) => check.blocking && check.status === "fail"))
|
|
3501
3514
|
return "BLOCKED";
|
|
3515
|
+
if (scope === "authoring" &&
|
|
3516
|
+
matching.some((check) => check.componentGate && check.status === "fail")) {
|
|
3517
|
+
return "BLOCKED";
|
|
3518
|
+
}
|
|
3502
3519
|
return matching.some((check) => check.status !== "pass") ? "DEGRADED" : "READY";
|
|
3503
3520
|
}
|
|
3504
3521
|
function requireAbsoluteWorkspace(value) {
|
|
@@ -3749,6 +3766,7 @@ function installerEnvironment(source) {
|
|
|
3749
3766
|
"SSL_CERT_FILE",
|
|
3750
3767
|
"SSL_CERT_DIR",
|
|
3751
3768
|
"NODE_EXTRA_CA_CERTS",
|
|
3769
|
+
"NODE_PATH",
|
|
3752
3770
|
"CODEX_HOME",
|
|
3753
3771
|
"CLAUDE_CONFIG_DIR",
|
|
3754
3772
|
"VIRTUAL_ENV",
|
|
@@ -3917,7 +3935,99 @@ async function setupDoctorCheck(checks, id, category, callback, failureMinimumAc
|
|
|
3917
3935
|
});
|
|
3918
3936
|
}
|
|
3919
3937
|
}
|
|
3920
|
-
|
|
3938
|
+
const AUTHORING_PYTHON_PROBES = {
|
|
3939
|
+
"authoring:defusedxml": {
|
|
3940
|
+
distribution: "defusedxml",
|
|
3941
|
+
module: "defusedxml",
|
|
3942
|
+
exactVersion: "0.7.1",
|
|
3943
|
+
},
|
|
3944
|
+
"authoring:lxml": { distribution: "lxml", module: "lxml.etree", exactVersion: null },
|
|
3945
|
+
"authoring:pillow": { distribution: "Pillow", module: "PIL", exactVersion: null },
|
|
3946
|
+
"authoring:python-pptx": {
|
|
3947
|
+
distribution: "python-pptx",
|
|
3948
|
+
module: "pptx",
|
|
3949
|
+
exactVersion: null,
|
|
3950
|
+
},
|
|
3951
|
+
"authoring:markitdown": {
|
|
3952
|
+
distribution: "markitdown",
|
|
3953
|
+
module: "markitdown",
|
|
3954
|
+
exactVersion: "0.1.7",
|
|
3955
|
+
},
|
|
3956
|
+
"authoring:pypdf": { distribution: "pypdf", module: "pypdf", exactVersion: null },
|
|
3957
|
+
"authoring:pdfplumber": {
|
|
3958
|
+
distribution: "pdfplumber",
|
|
3959
|
+
module: "pdfplumber",
|
|
3960
|
+
exactVersion: null,
|
|
3961
|
+
},
|
|
3962
|
+
"authoring:reportlab": {
|
|
3963
|
+
distribution: "reportlab",
|
|
3964
|
+
module: "reportlab",
|
|
3965
|
+
exactVersion: null,
|
|
3966
|
+
},
|
|
3967
|
+
"authoring:pdf2image": {
|
|
3968
|
+
distribution: "pdf2image",
|
|
3969
|
+
module: "pdf2image",
|
|
3970
|
+
exactVersion: null,
|
|
3971
|
+
},
|
|
3972
|
+
"authoring:openpyxl": {
|
|
3973
|
+
distribution: "openpyxl",
|
|
3974
|
+
module: "openpyxl",
|
|
3975
|
+
exactVersion: null,
|
|
3976
|
+
},
|
|
3977
|
+
"authoring:pandas": { distribution: "pandas", module: "pandas", exactVersion: null },
|
|
3978
|
+
};
|
|
3979
|
+
const AUTHORING_NODE_PROBES = {
|
|
3980
|
+
"authoring:node-docx": "docx",
|
|
3981
|
+
"authoring:node-pptxgenjs": "pptxgenjs",
|
|
3982
|
+
};
|
|
3983
|
+
const AUTHORING_COMMAND_PROBES = {
|
|
3984
|
+
"authoring:pandoc": "pandoc",
|
|
3985
|
+
"authoring:libreoffice": "soffice",
|
|
3986
|
+
"authoring:poppler": "pdftoppm",
|
|
3987
|
+
"authoring:zip": "zip",
|
|
3988
|
+
"authoring:unzip": "unzip",
|
|
3989
|
+
};
|
|
3990
|
+
async function resolveAuthoringRuntime(selected, environment) {
|
|
3991
|
+
if (!selected.some((skill) => skill.role === "post-closure-authoring"))
|
|
3992
|
+
return null;
|
|
3993
|
+
const resolveCommand = async (name) => (await findAmbientExecutable(environment, name, {
|
|
3994
|
+
preserveLeafSymlink: name === "python3",
|
|
3995
|
+
}))?.path ?? null;
|
|
3996
|
+
const [python, node, pandoc, soffice, pdftoppm, zip, unzip] = await Promise.all([
|
|
3997
|
+
resolveCommand("python3"),
|
|
3998
|
+
resolveCommand("node"),
|
|
3999
|
+
resolveCommand("pandoc"),
|
|
4000
|
+
resolveCommand("soffice"),
|
|
4001
|
+
resolveCommand("pdftoppm"),
|
|
4002
|
+
resolveCommand("zip"),
|
|
4003
|
+
resolveCommand("unzip"),
|
|
4004
|
+
]);
|
|
4005
|
+
return { python, node, pandoc, soffice, pdftoppm, zip, unzip };
|
|
4006
|
+
}
|
|
4007
|
+
function requireAuthoringRuntimeExecutable(value, minimumAction) {
|
|
4008
|
+
if (!value)
|
|
4009
|
+
throw new Error(minimumAction);
|
|
4010
|
+
return value;
|
|
4011
|
+
}
|
|
4012
|
+
function authoringEnvironment(source, runtime) {
|
|
4013
|
+
const result = installerEnvironment(source);
|
|
4014
|
+
const commandDirectories = [
|
|
4015
|
+
runtime?.soffice,
|
|
4016
|
+
runtime?.pdftoppm,
|
|
4017
|
+
runtime?.pandoc,
|
|
4018
|
+
runtime?.zip,
|
|
4019
|
+
runtime?.unzip,
|
|
4020
|
+
]
|
|
4021
|
+
.filter((value) => Boolean(value))
|
|
4022
|
+
.map(dirname);
|
|
4023
|
+
if (commandDirectories.length) {
|
|
4024
|
+
result.PATH = [...new Set(commandDirectories), result.PATH ?? ""]
|
|
4025
|
+
.filter(Boolean)
|
|
4026
|
+
.join(delimiter);
|
|
4027
|
+
}
|
|
4028
|
+
return result;
|
|
4029
|
+
}
|
|
4030
|
+
async function appendDependencyChecks(checks, plan, selected, runner, root, environment, authoringRuntime) {
|
|
3921
4031
|
const dependencies = [
|
|
3922
4032
|
...new Map(selected
|
|
3923
4033
|
.flatMap((skill) => skill.dependencies)
|
|
@@ -3937,7 +4047,7 @@ async function appendDependencyChecks(checks, plan, selected, runner, root, envi
|
|
|
3937
4047
|
await setupDoctorCheck(checks, `dependency.${dependency.id}`, "dependency", async () => {
|
|
3938
4048
|
if (dependency.id === "python-3.10") {
|
|
3939
4049
|
const result = await runner({
|
|
3940
|
-
command: "python3",
|
|
4050
|
+
command: authoringRuntime?.python ?? "python3",
|
|
3941
4051
|
args: ["--version"],
|
|
3942
4052
|
cwd: root,
|
|
3943
4053
|
environment: installerEnvironment(environment),
|
|
@@ -3971,40 +4081,321 @@ async function appendDependencyChecks(checks, plan, selected, runner, root, envi
|
|
|
3971
4081
|
}
|
|
3972
4082
|
return `${dependency.requirement} is ready.`;
|
|
3973
4083
|
}
|
|
3974
|
-
|
|
4084
|
+
const pythonProbe = AUTHORING_PYTHON_PROBES[dependency.id];
|
|
4085
|
+
if (pythonProbe) {
|
|
4086
|
+
const python = requireAuthoringRuntimeExecutable(authoringRuntime?.python ?? null, dependency.minimumAction);
|
|
3975
4087
|
const result = await runner({
|
|
3976
|
-
command:
|
|
4088
|
+
command: python,
|
|
3977
4089
|
args: [
|
|
3978
4090
|
"-c",
|
|
3979
|
-
|
|
4091
|
+
`import importlib.metadata as m; import ${pythonProbe.module}; print(m.version(${JSON.stringify(pythonProbe.distribution)}))`,
|
|
3980
4092
|
],
|
|
3981
4093
|
cwd: root,
|
|
3982
4094
|
environment: installerEnvironment(environment),
|
|
3983
4095
|
timeoutMs: 15_000,
|
|
3984
4096
|
});
|
|
3985
4097
|
const observed = result.stdout.trim();
|
|
3986
|
-
if (result.exitCode !== 0 ||
|
|
3987
|
-
|
|
4098
|
+
if (result.exitCode !== 0 ||
|
|
4099
|
+
!observed ||
|
|
4100
|
+
(pythonProbe.exactVersion !== null && observed !== pythonProbe.exactVersion)) {
|
|
4101
|
+
throw new Error(`${dependency.requirement} is not active in the bound authoring Python.`);
|
|
3988
4102
|
}
|
|
3989
|
-
return `${dependency.requirement} is active.`;
|
|
4103
|
+
return `${dependency.requirement} is active (version ${observed}).`;
|
|
3990
4104
|
}
|
|
3991
|
-
|
|
4105
|
+
const nodeProbe = AUTHORING_NODE_PROBES[dependency.id];
|
|
4106
|
+
if (nodeProbe) {
|
|
4107
|
+
const node = requireAuthoringRuntimeExecutable(authoringRuntime?.node ?? null, dependency.minimumAction);
|
|
3992
4108
|
const result = await runner({
|
|
3993
|
-
command:
|
|
3994
|
-
args: [
|
|
4109
|
+
command: node,
|
|
4110
|
+
args: [
|
|
4111
|
+
"-e",
|
|
4112
|
+
`const resolved=require.resolve(${JSON.stringify(nodeProbe)}); process.stdout.write(resolved);`,
|
|
4113
|
+
],
|
|
3995
4114
|
cwd: root,
|
|
3996
|
-
environment:
|
|
4115
|
+
environment: authoringEnvironment(environment, authoringRuntime),
|
|
3997
4116
|
timeoutMs: 15_000,
|
|
3998
4117
|
});
|
|
3999
|
-
if (result.exitCode !== 0 ||
|
|
4000
|
-
throw new Error(`${dependency.requirement} is not
|
|
4118
|
+
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
4119
|
+
throw new Error(`${dependency.requirement} is not resolvable by the bound authoring Node.`);
|
|
4001
4120
|
}
|
|
4002
|
-
return `${dependency.requirement} is
|
|
4121
|
+
return `${dependency.requirement} is resolvable.`;
|
|
4122
|
+
}
|
|
4123
|
+
const commandKey = AUTHORING_COMMAND_PROBES[dependency.id];
|
|
4124
|
+
if (commandKey) {
|
|
4125
|
+
const command = requireAuthoringRuntimeExecutable(authoringRuntime?.[commandKey] ?? null, dependency.minimumAction);
|
|
4126
|
+
const result = await runner({
|
|
4127
|
+
command,
|
|
4128
|
+
args: ["pdftoppm", "zip", "unzip"].includes(commandKey) ? ["-v"] : ["--version"],
|
|
4129
|
+
cwd: root,
|
|
4130
|
+
environment: authoringEnvironment(environment, authoringRuntime),
|
|
4131
|
+
timeoutMs: 15_000,
|
|
4132
|
+
});
|
|
4133
|
+
if (result.exitCode !== 0)
|
|
4134
|
+
throw new Error(`${dependency.requirement} is unavailable.`);
|
|
4135
|
+
return `${dependency.requirement} is executable.`;
|
|
4003
4136
|
}
|
|
4004
4137
|
throw new Error(`No automatic dependency check is declared for ${dependency.id}. ${dependency.minimumAction}`);
|
|
4005
4138
|
}, dependency.minimumAction);
|
|
4006
4139
|
}
|
|
4007
4140
|
}
|
|
4141
|
+
async function appendAuthoringCanaryChecks(checks, input) {
|
|
4142
|
+
for (const skill of input.selected.filter((candidate) => candidate.role === "post-closure-authoring" &&
|
|
4143
|
+
["anthropic.docx", "anthropic.pdf", "anthropic.pptx", "anthropic.xlsx"].includes(candidate.id))) {
|
|
4144
|
+
const failedPrerequisites = checks.filter((check) => check.id.startsWith("dependency.") &&
|
|
4145
|
+
check.status === "fail" &&
|
|
4146
|
+
setupCheckComponentIds(check, input.selected).includes(skill.id));
|
|
4147
|
+
if (failedPrerequisites.length) {
|
|
4148
|
+
checks.push({
|
|
4149
|
+
id: `authoring-canary.${skill.id}`,
|
|
4150
|
+
category: "authoring-canary",
|
|
4151
|
+
scope: "authoring",
|
|
4152
|
+
componentIds: [skill.id],
|
|
4153
|
+
status: "fail",
|
|
4154
|
+
detail: `Functional canary was not started because prerequisite checks failed: ${failedPrerequisites.map((check) => check.id).join(", ")}.`,
|
|
4155
|
+
minimumAction: "Resolve every declared runtime and dependency prerequisite, then rerun setup doctor; setup never installs them.",
|
|
4156
|
+
blocking: false,
|
|
4157
|
+
componentGate: true,
|
|
4158
|
+
requiredFor: [`component:${skill.id}`],
|
|
4159
|
+
skippedBecause: failedPrerequisites.map((check) => check.id).join(", "),
|
|
4160
|
+
});
|
|
4161
|
+
continue;
|
|
4162
|
+
}
|
|
4163
|
+
await setupDoctorCheck(checks, `authoring-canary.${skill.id}`, "authoring-canary", async () => {
|
|
4164
|
+
const temporary = await mkdtemp(join(tmpdir(), `tiangong-authoring-${skill.skillName}-`));
|
|
4165
|
+
try {
|
|
4166
|
+
await runAuthoringCanary({ ...input, skill, temporary });
|
|
4167
|
+
return `${skill.id} completed its exact-file synthetic functional canary.`;
|
|
4168
|
+
}
|
|
4169
|
+
finally {
|
|
4170
|
+
await rm(temporary, { recursive: true, force: true });
|
|
4171
|
+
}
|
|
4172
|
+
}, "Inspect the failed pinned helper/package/command step, repair the explicit authoring runtime outside this CLI, and rerun setup doctor.");
|
|
4173
|
+
Object.assign(checks.at(-1), {
|
|
4174
|
+
scope: "authoring",
|
|
4175
|
+
componentIds: [skill.id],
|
|
4176
|
+
blocking: false,
|
|
4177
|
+
componentGate: true,
|
|
4178
|
+
requiredFor: [`component:${skill.id}`],
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
async function runAuthoringCanary(input) {
|
|
4183
|
+
const environment = authoringEnvironment(input.environment, input.runtime);
|
|
4184
|
+
environment.PYTHONDONTWRITEBYTECODE = "1";
|
|
4185
|
+
if (input.skill.id === "anthropic.docx")
|
|
4186
|
+
return runDocxAuthoringCanary(input, environment);
|
|
4187
|
+
if (input.skill.id === "anthropic.pdf")
|
|
4188
|
+
return runPdfAuthoringCanary(input, environment);
|
|
4189
|
+
if (input.skill.id === "anthropic.pptx")
|
|
4190
|
+
return runPptxAuthoringCanary(input, environment);
|
|
4191
|
+
if (input.skill.id === "anthropic.xlsx")
|
|
4192
|
+
return runXlsxAuthoringCanary(input, environment);
|
|
4193
|
+
throw new Error("Unsupported authoring canary.");
|
|
4194
|
+
}
|
|
4195
|
+
async function runDocxAuthoringCanary(input, environment) {
|
|
4196
|
+
const node = requireAuthoringRuntimeExecutable(input.runtime?.node ?? null, "Node is missing.");
|
|
4197
|
+
const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
|
|
4198
|
+
const pandoc = requireAuthoringRuntimeExecutable(input.runtime?.pandoc ?? null, "Pandoc is missing.");
|
|
4199
|
+
const pdftoppm = requireAuthoringRuntimeExecutable(input.runtime?.pdftoppm ?? null, "Poppler is missing.");
|
|
4200
|
+
const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
|
|
4201
|
+
const validator = join(skillDirectory, "scripts", "office", "validate.py");
|
|
4202
|
+
const soffice = join(skillDirectory, "scripts", "office", "soffice.py");
|
|
4203
|
+
await Promise.all([
|
|
4204
|
+
requireRegularCompanionFile(input.root, validator, "DOCX validator"),
|
|
4205
|
+
requireRegularCompanionFile(input.root, soffice, "DOCX LibreOffice helper"),
|
|
4206
|
+
]);
|
|
4207
|
+
const document = join(input.temporary, "docx-canary.docx");
|
|
4208
|
+
await authoringStep(input, environment, "docx.create", node, [
|
|
4209
|
+
"-e",
|
|
4210
|
+
DOCX_CANARY_SCRIPT,
|
|
4211
|
+
document,
|
|
4212
|
+
]);
|
|
4213
|
+
await requireAuthoringArtifact(document, 128);
|
|
4214
|
+
await authoringStep(input, environment, "docx.validate", python, [validator, document]);
|
|
4215
|
+
const text = await authoringStep(input, environment, "docx.extract", pandoc, [
|
|
4216
|
+
"-t",
|
|
4217
|
+
"plain",
|
|
4218
|
+
document,
|
|
4219
|
+
]);
|
|
4220
|
+
if (!text.stdout.includes("TIANGONG_DOCX_CANARY")) {
|
|
4221
|
+
throw new Error("DOCX text extraction did not return the exact canary sentinel.");
|
|
4222
|
+
}
|
|
4223
|
+
await authoringStep(input, environment, "docx.render", python, [soffice, "--headless", "--convert-to", "pdf", "--outdir", input.temporary, document], 60_000);
|
|
4224
|
+
const pdf = join(input.temporary, "docx-canary.pdf");
|
|
4225
|
+
await requireAuthoringArtifact(pdf, 128);
|
|
4226
|
+
const renderPrefix = join(input.temporary, "docx-page");
|
|
4227
|
+
await authoringStep(input, environment, "docx.rasterize", pdftoppm, [
|
|
4228
|
+
"-f",
|
|
4229
|
+
"1",
|
|
4230
|
+
"-singlefile",
|
|
4231
|
+
"-png",
|
|
4232
|
+
pdf,
|
|
4233
|
+
renderPrefix,
|
|
4234
|
+
]);
|
|
4235
|
+
await requireAuthoringArtifact(`${renderPrefix}.png`, 64);
|
|
4236
|
+
}
|
|
4237
|
+
async function runPdfAuthoringCanary(input, environment) {
|
|
4238
|
+
const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
|
|
4239
|
+
const pdftoppm = requireAuthoringRuntimeExecutable(input.runtime?.pdftoppm ?? null, "Poppler is missing.");
|
|
4240
|
+
const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
|
|
4241
|
+
const converter = join(skillDirectory, "scripts", "convert_pdf_to_images.py");
|
|
4242
|
+
const validationImage = join(skillDirectory, "scripts", "create_validation_image.py");
|
|
4243
|
+
await Promise.all([
|
|
4244
|
+
requireRegularCompanionFile(input.root, converter, "PDF image converter"),
|
|
4245
|
+
requireRegularCompanionFile(input.root, validationImage, "PDF validation image helper"),
|
|
4246
|
+
]);
|
|
4247
|
+
const pdf = join(input.temporary, "pdf-canary.pdf");
|
|
4248
|
+
await authoringStep(input, environment, "pdf.create", python, ["-c", PDF_CANARY_SCRIPT, pdf]);
|
|
4249
|
+
await requireAuthoringArtifact(pdf, 128);
|
|
4250
|
+
const parsed = await authoringStep(input, environment, "pdf.parse", python, [
|
|
4251
|
+
"-c",
|
|
4252
|
+
PDF_VERIFY_SCRIPT,
|
|
4253
|
+
pdf,
|
|
4254
|
+
]);
|
|
4255
|
+
if (!parsed.stdout.includes("TIANGONG_PDF_CANARY")) {
|
|
4256
|
+
throw new Error("PDF parser did not return the exact canary sentinel.");
|
|
4257
|
+
}
|
|
4258
|
+
const renderPrefix = join(input.temporary, "pdf-page");
|
|
4259
|
+
await authoringStep(input, environment, "pdf.rasterize", pdftoppm, [
|
|
4260
|
+
"-f",
|
|
4261
|
+
"1",
|
|
4262
|
+
"-singlefile",
|
|
4263
|
+
"-png",
|
|
4264
|
+
pdf,
|
|
4265
|
+
renderPrefix,
|
|
4266
|
+
]);
|
|
4267
|
+
await requireAuthoringArtifact(`${renderPrefix}.png`, 64);
|
|
4268
|
+
const pages = join(input.temporary, "pdf-pages");
|
|
4269
|
+
await mkdir(pages);
|
|
4270
|
+
await authoringStep(input, environment, "pdf.convert-images", python, [converter, pdf, pages]);
|
|
4271
|
+
const page = join(pages, "page_1.png");
|
|
4272
|
+
await requireAuthoringArtifact(page, 64);
|
|
4273
|
+
const fields = join(input.temporary, "pdf-fields.json");
|
|
4274
|
+
await writeJsonAtomic(fields, { form_fields: [] });
|
|
4275
|
+
const annotated = join(input.temporary, "pdf-validation.png");
|
|
4276
|
+
await authoringStep(input, environment, "pdf.validation-image", python, [
|
|
4277
|
+
validationImage,
|
|
4278
|
+
"1",
|
|
4279
|
+
fields,
|
|
4280
|
+
page,
|
|
4281
|
+
annotated,
|
|
4282
|
+
]);
|
|
4283
|
+
await requireAuthoringArtifact(annotated, 64);
|
|
4284
|
+
}
|
|
4285
|
+
async function runPptxAuthoringCanary(input, environment) {
|
|
4286
|
+
const node = requireAuthoringRuntimeExecutable(input.runtime?.node ?? null, "Node is missing.");
|
|
4287
|
+
const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
|
|
4288
|
+
const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
|
|
4289
|
+
const validator = join(skillDirectory, "scripts", "office", "validate.py");
|
|
4290
|
+
const thumbnail = join(skillDirectory, "scripts", "thumbnail.py");
|
|
4291
|
+
await Promise.all([
|
|
4292
|
+
requireRegularCompanionFile(input.root, validator, "PPTX validator"),
|
|
4293
|
+
requireRegularCompanionFile(input.root, thumbnail, "PPTX thumbnail helper"),
|
|
4294
|
+
]);
|
|
4295
|
+
const presentation = join(input.temporary, "pptx-canary.pptx");
|
|
4296
|
+
await authoringStep(input, environment, "pptx.create", node, [
|
|
4297
|
+
"-e",
|
|
4298
|
+
PPTX_CANARY_SCRIPT,
|
|
4299
|
+
presentation,
|
|
4300
|
+
]);
|
|
4301
|
+
await requireAuthoringArtifact(presentation, 128);
|
|
4302
|
+
await authoringStep(input, environment, "pptx.validate", python, [validator, presentation]);
|
|
4303
|
+
const text = await authoringStep(input, environment, "pptx.extract", python, [
|
|
4304
|
+
"-m",
|
|
4305
|
+
"markitdown",
|
|
4306
|
+
presentation,
|
|
4307
|
+
]);
|
|
4308
|
+
if (!text.stdout.includes("TIANGONG_PPTX_CANARY")) {
|
|
4309
|
+
throw new Error("PPTX MarkItDown did not return the exact canary sentinel.");
|
|
4310
|
+
}
|
|
4311
|
+
const thumbnailPrefix = join(input.temporary, "pptx-grid");
|
|
4312
|
+
await authoringStep(input, environment, "pptx.thumbnail", python, [thumbnail, presentation, thumbnailPrefix, "--cols", "1"], 60_000);
|
|
4313
|
+
await requireAuthoringArtifact(`${thumbnailPrefix}.jpg`, 64);
|
|
4314
|
+
}
|
|
4315
|
+
async function runXlsxAuthoringCanary(input, environment) {
|
|
4316
|
+
const python = requireAuthoringRuntimeExecutable(input.runtime?.python ?? null, "Python is missing.");
|
|
4317
|
+
const skillDirectory = await verifiedCompanionSkillDirectory(input.plan, input.skill);
|
|
4318
|
+
const recalc = join(skillDirectory, "scripts", "recalc.py");
|
|
4319
|
+
await requireRegularCompanionFile(input.root, recalc, "XLSX recalculation helper");
|
|
4320
|
+
const workbook = join(input.temporary, "xlsx-canary.xlsx");
|
|
4321
|
+
await authoringStep(input, environment, "xlsx.create", python, [
|
|
4322
|
+
"-c",
|
|
4323
|
+
XLSX_CANARY_SCRIPT,
|
|
4324
|
+
workbook,
|
|
4325
|
+
]);
|
|
4326
|
+
await requireAuthoringArtifact(workbook, 128);
|
|
4327
|
+
const recalculated = await authoringStep(input, environment, "xlsx.recalculate", python, [recalc, workbook, "60"], 75_000);
|
|
4328
|
+
let result;
|
|
4329
|
+
try {
|
|
4330
|
+
result = JSON.parse(recalculated.stdout);
|
|
4331
|
+
}
|
|
4332
|
+
catch {
|
|
4333
|
+
throw new Error("XLSX recalculation helper did not return JSON.");
|
|
4334
|
+
}
|
|
4335
|
+
if (!isObject(result) || result.status !== "success" || result.total_errors !== 0) {
|
|
4336
|
+
throw new Error("XLSX recalculation helper did not complete without formula errors.");
|
|
4337
|
+
}
|
|
4338
|
+
const cached = await authoringStep(input, environment, "xlsx.verify-cache", python, [
|
|
4339
|
+
"-c",
|
|
4340
|
+
XLSX_VERIFY_SCRIPT,
|
|
4341
|
+
workbook,
|
|
4342
|
+
]);
|
|
4343
|
+
if (cached.stdout.trim() !== "5") {
|
|
4344
|
+
throw new Error("XLSX recalculation did not persist the exact cached formula value.");
|
|
4345
|
+
}
|
|
4346
|
+
const markdown = await authoringStep(input, environment, "xlsx.extract", python, [
|
|
4347
|
+
"-m",
|
|
4348
|
+
"markitdown",
|
|
4349
|
+
workbook,
|
|
4350
|
+
]);
|
|
4351
|
+
const normalizedMarkdown = markdown.stdout.replaceAll("\\_", "_");
|
|
4352
|
+
if (!normalizedMarkdown.includes("TIANGONG_XLSX_CANARY") ||
|
|
4353
|
+
!/(?:^|\D)5(?:\D|$)/u.test(normalizedMarkdown)) {
|
|
4354
|
+
throw new Error("XLSX MarkItDown did not return the exact canary sentinel and value.");
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
async function authoringStep(input, environment, step, command, args, timeoutMs = 30_000) {
|
|
4358
|
+
const result = await input.runner({
|
|
4359
|
+
command,
|
|
4360
|
+
args,
|
|
4361
|
+
cwd: input.root,
|
|
4362
|
+
environment,
|
|
4363
|
+
timeoutMs,
|
|
4364
|
+
});
|
|
4365
|
+
if (result.exitCode !== 0) {
|
|
4366
|
+
throw new Error(`Authoring canary step failed (${step}).`);
|
|
4367
|
+
}
|
|
4368
|
+
return result;
|
|
4369
|
+
}
|
|
4370
|
+
async function requireAuthoringArtifact(path, minimumBytes) {
|
|
4371
|
+
const info = await lstat(path).catch(() => undefined);
|
|
4372
|
+
if (!info?.isFile() || info.isSymbolicLink() || info.size < minimumBytes) {
|
|
4373
|
+
throw new Error("Authoring canary did not create its exact expected regular artifact.");
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
const DOCX_CANARY_SCRIPT = String.raw `
|
|
4377
|
+
const fs=require("node:fs"); const {Document,Packer,Paragraph}=require("docx");
|
|
4378
|
+
(async()=>{const out=process.argv[1]; const doc=new Document({sections:[{children:[new Paragraph("TIANGONG_DOCX_CANARY")]}]}); fs.writeFileSync(out,await Packer.toBuffer(doc));})().catch(()=>process.exit(1));`;
|
|
4379
|
+
const PDF_CANARY_SCRIPT = String.raw `
|
|
4380
|
+
import sys
|
|
4381
|
+
from reportlab.pdfgen import canvas
|
|
4382
|
+
c=canvas.Canvas(sys.argv[1]); c.drawString(72,720,"TIANGONG_PDF_CANARY"); c.save()`;
|
|
4383
|
+
const PDF_VERIFY_SCRIPT = String.raw `
|
|
4384
|
+
import sys, pdfplumber
|
|
4385
|
+
from pypdf import PdfReader
|
|
4386
|
+
assert len(PdfReader(sys.argv[1]).pages)==1
|
|
4387
|
+
with pdfplumber.open(sys.argv[1]) as p: print(p.pages[0].extract_text())`;
|
|
4388
|
+
const PPTX_CANARY_SCRIPT = String.raw `
|
|
4389
|
+
const pptxgen=require("pptxgenjs");
|
|
4390
|
+
(async()=>{const p=new pptxgen(); p.addSlide().addText("TIANGONG_PPTX_CANARY",{x:1,y:1,w:6,h:1}); await p.writeFile({fileName:process.argv[1]});})().catch(()=>process.exit(1));`;
|
|
4391
|
+
const XLSX_CANARY_SCRIPT = String.raw `
|
|
4392
|
+
import sys
|
|
4393
|
+
from openpyxl import Workbook
|
|
4394
|
+
w=Workbook(); s=w.active; s["A1"]="TIANGONG_XLSX_CANARY"; s["A2"]=2; s["A3"]=3; s["A4"]="=SUM(A2:A3)"; w.save(sys.argv[1])`;
|
|
4395
|
+
const XLSX_VERIFY_SCRIPT = String.raw `
|
|
4396
|
+
import sys
|
|
4397
|
+
from openpyxl import load_workbook
|
|
4398
|
+
w=load_workbook(sys.argv[1],data_only=True,read_only=True); print(w.active["A4"].value); w.close()`;
|
|
4008
4399
|
async function appendCompanionLiveChecks(checks, input) {
|
|
4009
4400
|
if (input.selected.some((skill) => skill.id === "tiangong.academic-paper-download")) {
|
|
4010
4401
|
await appendSemanticScholarLiveCheck(checks, input);
|