183 lines
4.6 KiB
TypeScript
183 lines
4.6 KiB
TypeScript
import {
|
|
CLI_EXPERIMENTAL_FEATURES,
|
|
isExperimentalFeatureEnabledByDefault,
|
|
type CliReleaseChannel,
|
|
} from '../../src/experimental-features';
|
|
|
|
export type SkillReleaseChannel = CliReleaseChannel;
|
|
|
|
export type SkillFeatureFlag =
|
|
(typeof CLI_EXPERIMENTAL_FEATURES)[keyof typeof CLI_EXPERIMENTAL_FEATURES];
|
|
|
|
export type SkillBuildContext = {
|
|
channel: SkillReleaseChannel;
|
|
experimentalFeatures: Readonly<Record<SkillFeatureFlag, boolean>>;
|
|
};
|
|
|
|
export type FeatureScoped = {
|
|
features?: SkillFeatureFlag[];
|
|
};
|
|
|
|
export type CommandSnippet = FeatureScoped & {
|
|
code: string;
|
|
description?: string;
|
|
};
|
|
|
|
export type ReferenceSection = FeatureScoped & {
|
|
body?: string[];
|
|
commands?: CommandSnippet[];
|
|
title: string;
|
|
};
|
|
|
|
export type ReferenceDocument = {
|
|
intro?: string[];
|
|
slug: string;
|
|
title: string;
|
|
sections: ReferenceSection[];
|
|
};
|
|
|
|
const TOP_LEVEL_COMMANDS = new Set([
|
|
'artifacts',
|
|
'config',
|
|
'dev',
|
|
'execute',
|
|
'link',
|
|
'listen',
|
|
'local-tools',
|
|
'login',
|
|
'logout',
|
|
'proxy',
|
|
'run',
|
|
'search',
|
|
'tools',
|
|
'whoami',
|
|
]);
|
|
|
|
export const resolveSkillBuildContext = (
|
|
channel: SkillReleaseChannel,
|
|
overrides?: Partial<Record<SkillFeatureFlag, boolean>>
|
|
): SkillBuildContext => ({
|
|
channel,
|
|
experimentalFeatures: {
|
|
[CLI_EXPERIMENTAL_FEATURES.LISTEN]: isExperimentalFeatureEnabledByDefault(
|
|
CLI_EXPERIMENTAL_FEATURES.LISTEN,
|
|
channel
|
|
),
|
|
[CLI_EXPERIMENTAL_FEATURES.LOCAL_TOOLS]: isExperimentalFeatureEnabledByDefault(
|
|
CLI_EXPERIMENTAL_FEATURES.LOCAL_TOOLS,
|
|
channel
|
|
),
|
|
[CLI_EXPERIMENTAL_FEATURES.MULTI_ACCOUNT]: isExperimentalFeatureEnabledByDefault(
|
|
CLI_EXPERIMENTAL_FEATURES.MULTI_ACCOUNT,
|
|
channel
|
|
),
|
|
...overrides,
|
|
},
|
|
});
|
|
|
|
export const isEnabledForBuild = (build: SkillBuildContext, value?: FeatureScoped) =>
|
|
!value?.features || value.features.every(feature => build.experimentalFeatures[feature]);
|
|
|
|
export const renderCommandSnippet = (snippet: CommandSnippet) => {
|
|
const lines: string[] = [];
|
|
|
|
if (snippet.description) {
|
|
lines.push(snippet.description + ':');
|
|
}
|
|
|
|
lines.push('```bash', snippet.code, '```');
|
|
return lines.join('\n');
|
|
};
|
|
|
|
export const renderReferenceDocument = (document: ReferenceDocument, build: SkillBuildContext) => {
|
|
const lines = [`# ${document.title}`];
|
|
|
|
for (const introLine of document.intro ?? []) {
|
|
lines.push('', introLine);
|
|
}
|
|
|
|
for (const section of document.sections.filter(section => isEnabledForBuild(build, section))) {
|
|
lines.push('', `## ${section.title}`);
|
|
|
|
for (const bodyLine of section.body ?? []) {
|
|
lines.push('', bodyLine);
|
|
}
|
|
|
|
for (const command of (section.commands ?? []).filter(command =>
|
|
isEnabledForBuild(build, command)
|
|
)) {
|
|
lines.push('', renderCommandSnippet(command));
|
|
}
|
|
}
|
|
|
|
return lines.join('\n') + '\n';
|
|
};
|
|
|
|
export const validateCommandSnippet = (snippet: CommandSnippet, documentSlug: string) => {
|
|
const errors: string[] = [];
|
|
const lines = snippet.code.split('\n');
|
|
let inQuotedCommand = false;
|
|
let expectingContinuation = false;
|
|
|
|
for (const rawLine of lines) {
|
|
const line = rawLine.trim();
|
|
if (line.length === 0 || line.startsWith('#')) {
|
|
continue;
|
|
}
|
|
|
|
if (inQuotedCommand) {
|
|
const quoteCount = (line.match(/'/g) ?? []).length;
|
|
if (line === "'" || quoteCount % 2 === 1) {
|
|
inQuotedCommand = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (expectingContinuation) {
|
|
expectingContinuation = line.endsWith('\\');
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
line.startsWith('cat ') ||
|
|
line.startsWith('echo ') ||
|
|
line.startsWith('source ') ||
|
|
line.startsWith('export ')
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (!line.startsWith('composio ')) {
|
|
errors.push(`[${documentSlug}] command must start with "composio": ${line}`);
|
|
continue;
|
|
}
|
|
|
|
const normalized = line.endsWith('\\') ? line.slice(0, -1).trim() : line;
|
|
const parts = normalized.split(/\s+/);
|
|
const topLevel = parts[1];
|
|
if (!TOP_LEVEL_COMMANDS.has(topLevel)) {
|
|
errors.push(`[${documentSlug}] unknown top-level command "${topLevel}" in: ${normalized}`);
|
|
}
|
|
|
|
expectingContinuation = line.endsWith('\\');
|
|
const singleQuoteCount = (line.match(/'/g) ?? []).length;
|
|
if (singleQuoteCount % 2 === 1) {
|
|
inQuotedCommand = true;
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
};
|
|
|
|
export const validateReferenceDocument = (document: ReferenceDocument) => {
|
|
const errors: string[] = [];
|
|
|
|
for (const section of document.sections) {
|
|
for (const command of section.commands ?? []) {
|
|
errors.push(...validateCommandSnippet(command, document.slug));
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
};
|