源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseDocument, stringify } from "yaml";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, "../..");
|
||||
const snapshotSchemaPath = resolve(
|
||||
repoRoot,
|
||||
"packages/workbench/snapshot/src/schema.json"
|
||||
);
|
||||
const openApiPath = resolve(
|
||||
repoRoot,
|
||||
"services/tuttid/api/openapi/tuttid.v1.yaml"
|
||||
);
|
||||
const checkOnly = process.argv.includes("--check");
|
||||
|
||||
const schemaNames = [
|
||||
"WorkbenchSize",
|
||||
"WorkbenchSafeArea",
|
||||
"WorkbenchLayoutConstraints",
|
||||
"WorkbenchLayoutBasis",
|
||||
"WorkbenchLayoutPreset",
|
||||
"WorkbenchNormalizedFrame",
|
||||
"WorkbenchLockedLayout",
|
||||
"WorkbenchFrame",
|
||||
"WorkbenchSnapshotNode",
|
||||
"WorkbenchSnapshotSpace",
|
||||
"WorkbenchSnapshot"
|
||||
];
|
||||
|
||||
const sourceSchema = JSON.parse(readFileSync(snapshotSchemaPath, "utf8"));
|
||||
const openApiSource = readExistingText(openApiPath);
|
||||
const syncedOpenApiSource = replaceWorkbenchSchemas(
|
||||
openApiSource,
|
||||
buildOpenApiWorkbenchSchemas(sourceSchema)
|
||||
);
|
||||
|
||||
if (checkOnly) {
|
||||
if (openApiSource !== syncedOpenApiSource) {
|
||||
throw new Error(
|
||||
"OpenAPI workbench schemas are out of sync with packages/workbench/snapshot/src/schema.json. Run pnpm sync:workbench-openapi-schema."
|
||||
);
|
||||
}
|
||||
} else if (openApiSource !== syncedOpenApiSource) {
|
||||
writeFileSync(openApiPath, syncedOpenApiSource, "utf8");
|
||||
}
|
||||
|
||||
function buildOpenApiWorkbenchSchemas(schema) {
|
||||
const defs = schema.$defs ?? {};
|
||||
|
||||
return {
|
||||
WorkbenchSize: convertJsonSchemaToOpenApi(defs.size),
|
||||
WorkbenchSafeArea: convertJsonSchemaToOpenApi(defs.safeArea),
|
||||
WorkbenchLayoutConstraints: convertJsonSchemaToOpenApi(
|
||||
defs.layoutConstraints
|
||||
),
|
||||
WorkbenchLayoutBasis: convertJsonSchemaToOpenApi(defs.layoutBasis),
|
||||
WorkbenchLayoutPreset: convertJsonSchemaToOpenApi(defs.layoutPreset),
|
||||
WorkbenchNormalizedFrame: convertJsonSchemaToOpenApi(defs.normalizedFrame),
|
||||
WorkbenchLockedLayout: convertJsonSchemaToOpenApi(defs.lockedLayout),
|
||||
WorkbenchFrame: convertJsonSchemaToOpenApi(defs.frame),
|
||||
WorkbenchSnapshotNode: convertJsonSchemaToOpenApi(defs.node),
|
||||
WorkbenchSnapshotSpace: convertJsonSchemaToOpenApi(defs.space),
|
||||
WorkbenchSnapshot: convertJsonSchemaToOpenApi(
|
||||
omitKeys(schema, ["$schema", "$id", "title", "$defs"])
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function convertJsonSchemaToOpenApi(value) {
|
||||
if (value === true) {
|
||||
return { nullable: true };
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(convertJsonSchemaToOpenApi);
|
||||
}
|
||||
if (!isRecord(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (isNullableAnyOfRef(value)) {
|
||||
return {
|
||||
allOf: [convertJsonSchemaToOpenApi(value.anyOf[0])],
|
||||
nullable: true
|
||||
};
|
||||
}
|
||||
|
||||
const output = {};
|
||||
|
||||
for (const [key, entryValue] of Object.entries(value)) {
|
||||
if (key === "$ref") {
|
||||
output.$ref = rewriteRef(entryValue);
|
||||
continue;
|
||||
}
|
||||
if (key === "$defs" || key === "$schema" || key === "$id") {
|
||||
continue;
|
||||
}
|
||||
if (key === "const") {
|
||||
output.type = Number.isInteger(entryValue)
|
||||
? "integer"
|
||||
: typeof entryValue;
|
||||
output.enum = [entryValue];
|
||||
continue;
|
||||
}
|
||||
if (key === "exclusiveMinimum" && typeof entryValue === "number") {
|
||||
output.minimum = entryValue;
|
||||
output.exclusiveMinimum = true;
|
||||
continue;
|
||||
}
|
||||
if (key === "uniqueItems") {
|
||||
output.uniqueItems = entryValue;
|
||||
continue;
|
||||
}
|
||||
if (key === "type" && Array.isArray(entryValue)) {
|
||||
const nonNullTypes = entryValue.filter((item) => item !== "null");
|
||||
if (nonNullTypes.length === 1 && entryValue.includes("null")) {
|
||||
output.type = nonNullTypes[0];
|
||||
output.nullable = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
output[key] = convertJsonSchemaToOpenApi(entryValue);
|
||||
}
|
||||
|
||||
if (
|
||||
output.type === "object" &&
|
||||
output.additionalProperties === undefined &&
|
||||
output.properties === undefined
|
||||
) {
|
||||
output.additionalProperties = true;
|
||||
}
|
||||
if (Array.isArray(output.enum) && output.type === undefined) {
|
||||
const enumTypes = new Set(output.enum.map((item) => typeof item));
|
||||
if (enumTypes.size === 1) {
|
||||
output.type = [...enumTypes][0];
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function replaceWorkbenchSchemas(openApiSource, schemas) {
|
||||
const document = parseDocument(openApiSource, { keepSourceTokens: true });
|
||||
const schemasNode = document.getIn(["components", "schemas"], true);
|
||||
|
||||
if (!schemasNode?.items) {
|
||||
throw new Error(`Unable to locate components.schemas in ${openApiPath}`);
|
||||
}
|
||||
|
||||
const replacements = schemaNames
|
||||
.map((schemaName) =>
|
||||
locateSchemaReplacement(openApiSource, schemasNode, schemaName, schemas)
|
||||
)
|
||||
.filter(Boolean);
|
||||
const missingSchemaNames = schemaNames.filter(
|
||||
(schemaName) =>
|
||||
!schemasNode.items.some((pair) => pair.key?.value === schemaName)
|
||||
);
|
||||
if (missingSchemaNames.length > 0) {
|
||||
const firstWorkbenchSchema = replacements.reduce(
|
||||
(first, replacement) =>
|
||||
replacement.start < first.start ? replacement : first,
|
||||
replacements[0]
|
||||
);
|
||||
if (!firstWorkbenchSchema) {
|
||||
throw new Error(`Unable to locate Workbench schemas in ${openApiPath}`);
|
||||
}
|
||||
replacements.push({
|
||||
start: firstWorkbenchSchema.start,
|
||||
end: firstWorkbenchSchema.start,
|
||||
content: missingSchemaNames
|
||||
.map((schemaName) =>
|
||||
renderIndentedSchema(schemaName, schemas[schemaName])
|
||||
)
|
||||
.join("")
|
||||
});
|
||||
}
|
||||
|
||||
let output = openApiSource;
|
||||
for (const replacement of replacements.sort(
|
||||
(left, right) => right.start - left.start
|
||||
)) {
|
||||
output =
|
||||
output.slice(0, replacement.start) +
|
||||
replacement.content +
|
||||
output.slice(replacement.end);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function locateSchemaReplacement(
|
||||
openApiSource,
|
||||
schemasNode,
|
||||
schemaName,
|
||||
schemas
|
||||
) {
|
||||
const pairIndex = schemasNode.items.findIndex(
|
||||
(pair) => pair.key?.value === schemaName
|
||||
);
|
||||
const pair = schemasNode.items[pairIndex];
|
||||
if (!pair) {
|
||||
return null;
|
||||
}
|
||||
if (!pair.key?.range || !pair.value?.range) {
|
||||
throw new Error(`Unable to read ${schemaName} in ${openApiPath}`);
|
||||
}
|
||||
|
||||
const nextPair = schemasNode.items[pairIndex + 1];
|
||||
const start = lineStartBefore(openApiSource, pair.key.range[0]);
|
||||
const end = nextPair?.key?.range
|
||||
? lineStartBefore(openApiSource, nextPair.key.range[0])
|
||||
: pair.value.range[2];
|
||||
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
content: renderIndentedSchema(schemaName, schemas[schemaName])
|
||||
};
|
||||
}
|
||||
|
||||
function renderIndentedSchema(schemaName, schema) {
|
||||
return stringify(
|
||||
{
|
||||
[schemaName]: schema
|
||||
},
|
||||
{
|
||||
lineWidth: 0,
|
||||
singleQuote: false
|
||||
}
|
||||
)
|
||||
.trimEnd()
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n")
|
||||
.concat("\n");
|
||||
}
|
||||
|
||||
function lineStartBefore(source, index) {
|
||||
return source.lastIndexOf("\n", index) + 1;
|
||||
}
|
||||
|
||||
function rewriteRef(value) {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(`Expected string $ref, got ${typeof value}`);
|
||||
}
|
||||
|
||||
return value
|
||||
.replace("#/$defs/frame", "#/components/schemas/WorkbenchFrame")
|
||||
.replace("#/$defs/size", "#/components/schemas/WorkbenchSize")
|
||||
.replace("#/$defs/safeArea", "#/components/schemas/WorkbenchSafeArea")
|
||||
.replace(
|
||||
"#/$defs/layoutConstraints",
|
||||
"#/components/schemas/WorkbenchLayoutConstraints"
|
||||
)
|
||||
.replace("#/$defs/layoutBasis", "#/components/schemas/WorkbenchLayoutBasis")
|
||||
.replace(
|
||||
"#/$defs/layoutPreset",
|
||||
"#/components/schemas/WorkbenchLayoutPreset"
|
||||
)
|
||||
.replace(
|
||||
"#/$defs/normalizedFrame",
|
||||
"#/components/schemas/WorkbenchNormalizedFrame"
|
||||
)
|
||||
.replace(
|
||||
"#/$defs/lockedLayout",
|
||||
"#/components/schemas/WorkbenchLockedLayout"
|
||||
)
|
||||
.replace("#/$defs/node", "#/components/schemas/WorkbenchSnapshotNode")
|
||||
.replace("#/$defs/space", "#/components/schemas/WorkbenchSnapshotSpace");
|
||||
}
|
||||
|
||||
function isNullableAnyOfRef(value) {
|
||||
return (
|
||||
Array.isArray(value.anyOf) &&
|
||||
value.anyOf.length === 2 &&
|
||||
isRecord(value.anyOf[0]) &&
|
||||
typeof value.anyOf[0].$ref === "string" &&
|
||||
isRecord(value.anyOf[1]) &&
|
||||
value.anyOf[1].type === "null"
|
||||
);
|
||||
}
|
||||
|
||||
function omitKeys(value, keys) {
|
||||
const ignored = new Set(keys);
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).filter(([key]) => !ignored.has(key))
|
||||
);
|
||||
}
|
||||
|
||||
function readExistingText(path) {
|
||||
if (!existsSync(path)) {
|
||||
return "";
|
||||
}
|
||||
return readFileSync(path, "utf8");
|
||||
}
|
||||
|
||||
function isRecord(value) {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user