Initial commit: 小羚羚小程序 Axhub Make workspace.

Include xll-miniapp prototype, PRD resources, annotation directory sync, agent skills, and cloud publishing setup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-06-30 15:32:30 +08:00
commit a619938e0c
1227 changed files with 251728 additions and 0 deletions

63
scripts/utils/command-runtime.d.ts vendored Normal file
View File

@@ -0,0 +1,63 @@
export type DecodeOutputOptions = {
platform?: NodeJS.Platform;
};
export type RunCommandOptions = {
command: string;
args?: string[];
cwd?: string;
env?: NodeJS.ProcessEnv;
timeoutMs?: number;
detached?: boolean;
capture?: boolean;
stdio?: any;
};
export type RunCommandResult = {
command: string;
args: string[];
spawnCommand: string;
spawnArgs: string[];
code: number | null;
signal: NodeJS.Signals | null;
stdoutBuffer: Buffer;
stderrBuffer: Buffer;
stdout: string;
stderr: string;
};
export type RunCommandSyncOptions = {
command: string;
args?: string[];
cwd?: string;
env?: NodeJS.ProcessEnv;
timeoutMs?: number;
maxBuffer?: number;
};
export type RunCommandSyncResult = {
command: string;
args: string[];
spawnCommand: string;
spawnArgs: string[];
status: number | null;
signal: NodeJS.Signals | null;
error: Error | null;
stdoutBuffer: Buffer;
stderrBuffer: Buffer;
stdout: string;
stderr: string;
};
export function decodeOutput(value: unknown, options?: DecodeOutputOptions): string;
export function runCommand(options: RunCommandOptions): Promise<RunCommandResult>;
export function runCommandSync(options: RunCommandSyncOptions): RunCommandSyncResult;
export function commandExists(command: string): boolean;
export function getPreferredNpmCommand(): string;
export function getPreferredNpxCommand(): string;
export function getSpawnCommandSpec(command: string, args?: string[], platform?: NodeJS.Platform): {
command: string;
args: string[];
windowsHide: boolean;
};
export function __resetWindowsCodePageCacheForTests(): void;

View File

@@ -0,0 +1,340 @@
import { spawn, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import iconv from 'iconv-lite';
const WINDOWS_CODEPAGE_TIMEOUT_MS = 1200;
let cachedWindowsCodePage = null;
function getPlatform(overridePlatform) {
return overridePlatform || process.platform;
}
function quoteForCmdExec(value) {
if (!value) return '""';
if (!/[\s"&^|<>]/.test(value)) return value;
const escaped = String(value)
.replace(/(\\*)"/g, '$1$1\\"')
.replace(/(\\+)$/g, '$1$1');
return `"${escaped}"`;
}
function buildWindowsCommandLine(command, args) {
return [command, ...args].map((part) => quoteForCmdExec(String(part))).join(' ');
}
function getEnvValue(env, key) {
if (!env) return undefined;
const direct = env[key];
if (typeof direct === 'string' && direct.length > 0) {
return direct;
}
const matchedKey = Object.keys(env).find((candidate) => candidate.toLowerCase() === key.toLowerCase());
if (!matchedKey) return undefined;
const value = env[matchedKey];
return typeof value === 'string' && value.length > 0 ? value : undefined;
}
function getWindowsPathExtList(env) {
const pathExt = getEnvValue(env, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD';
return pathExt
.split(';')
.map((ext) => ext.trim())
.filter(Boolean)
.map((ext) => (ext.startsWith('.') ? ext.toLowerCase() : `.${ext.toLowerCase()}`));
}
function resolveWindowsCommand(command, env) {
if (!command || typeof command !== 'string') return command;
const trimmed = command.trim();
if (!trimmed) return trimmed;
const hasPathSeparator = /[\\/]/.test(trimmed);
const ext = path.extname(trimmed);
const pathExts = ext ? [''] : getWindowsPathExtList(env);
const candidateDirs = hasPathSeparator
? ['']
: (getEnvValue(env, 'PATH') || '')
.split(';')
.map((entry) => entry.trim())
.filter(Boolean);
const baseCandidates = hasPathSeparator ? [trimmed] : candidateDirs.map((dir) => path.join(dir, trimmed));
for (const baseCandidate of baseCandidates) {
const suffixes = ext ? [''] : pathExts;
for (const suffix of suffixes) {
const fullPath = suffix ? `${baseCandidate}${suffix}` : baseCandidate;
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
}
return trimmed;
}
function shouldUseWindowsCmdWrapper(platform, command) {
if (platform !== 'win32') return false;
return /\.(cmd|bat)$/i.test(command) || !/\.(exe|com)$/i.test(command);
}
function getSpawnSpec(command, args, platform = process.platform, env = process.env) {
const resolvedCommand = platform === 'win32' ? resolveWindowsCommand(command, env) : command;
if (!shouldUseWindowsCmdWrapper(platform, resolvedCommand)) {
return {
command: resolvedCommand,
args,
windowsHide: platform === 'win32',
};
}
const commandLine = buildWindowsCommandLine(resolvedCommand, args);
return {
command: 'cmd.exe',
args: ['/d', '/s', '/c', commandLine],
windowsHide: true,
};
}
function mapCodePageToEncoding(codePage) {
if (codePage === 65001) return 'utf8';
if (codePage === 936) return 'gbk';
if (codePage === 54936) return 'gb18030';
return 'gb18030';
}
function parseWindowsCodePage(text) {
if (!text) return null;
const match = String(text).match(/(\d{3,5})/);
if (!match) return null;
const codePage = Number(match[1]);
return Number.isFinite(codePage) ? codePage : null;
}
function readWindowsCodePageSync() {
if (cachedWindowsCodePage !== null) {
return cachedWindowsCodePage;
}
try {
const result = spawnSync('cmd.exe', ['/d', '/s', '/c', 'chcp'], {
windowsHide: true,
encoding: 'utf8',
timeout: WINDOWS_CODEPAGE_TIMEOUT_MS,
});
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
cachedWindowsCodePage = parseWindowsCodePage(output);
} catch {
cachedWindowsCodePage = null;
}
return cachedWindowsCodePage;
}
function toBuffer(value) {
if (!value) return Buffer.alloc(0);
if (Buffer.isBuffer(value)) return value;
if (typeof value === 'string') return Buffer.from(value);
if (value instanceof Uint8Array) return Buffer.from(value);
return Buffer.from(String(value));
}
export function decodeOutput(value, options = {}) {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
const platform = getPlatform(options.platform);
const buffer = toBuffer(value);
if (buffer.length === 0) return '';
try {
const strictUtf8 = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
return strictUtf8;
} catch {
// Fall through to platform fallback decoder.
}
if (platform === 'win32') {
const activeCodePage = readWindowsCodePageSync();
const preferredEncoding = mapCodePageToEncoding(activeCodePage);
try {
return iconv.decode(buffer, preferredEncoding);
} catch {
// Fall through to generic fallback.
}
try {
return iconv.decode(buffer, 'gb18030');
} catch {
// Fall through to latin1 fallback.
}
}
try {
return buffer.toString('utf8');
} catch {
return buffer.toString('latin1');
}
}
export function runCommandSync(options) {
const {
command,
args = [],
cwd,
env,
timeoutMs,
maxBuffer,
} = options;
const platform = process.platform;
const mergedEnv = env ? { ...process.env, ...env } : process.env;
const spawnSpec = getSpawnSpec(command, args, platform, mergedEnv);
const result = spawnSync(spawnSpec.command, spawnSpec.args, {
cwd,
env: mergedEnv,
timeout: timeoutMs,
maxBuffer,
windowsHide: spawnSpec.windowsHide,
encoding: null,
});
const stdoutBuffer = toBuffer(result.stdout);
const stderrBuffer = toBuffer(result.stderr);
return {
command,
args,
spawnCommand: spawnSpec.command,
spawnArgs: spawnSpec.args,
status: typeof result.status === 'number' ? result.status : null,
signal: result.signal || null,
error: result.error || null,
stdoutBuffer,
stderrBuffer,
stdout: decodeOutput(stdoutBuffer, { platform }),
stderr: decodeOutput(stderrBuffer, { platform }),
};
}
export function runCommand(options) {
const {
command,
args = [],
cwd,
env,
timeoutMs,
detached = false,
capture = true,
stdio,
} = options;
return new Promise((resolve, reject) => {
const platform = process.platform;
const mergedEnv = env ? { ...process.env, ...env } : process.env;
const spawnSpec = getSpawnSpec(command, args, platform, mergedEnv);
const child = spawn(spawnSpec.command, spawnSpec.args, {
cwd,
env: mergedEnv,
detached,
stdio: stdio || (capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'),
windowsHide: spawnSpec.windowsHide,
});
const stdoutChunks = [];
const stderrChunks = [];
if (capture && child.stdout) {
child.stdout.on('data', (chunk) => {
stdoutChunks.push(toBuffer(chunk));
});
}
if (capture && child.stderr) {
child.stderr.on('data', (chunk) => {
stderrChunks.push(toBuffer(chunk));
});
}
let timeoutId = null;
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
timeoutId = setTimeout(() => {
child.kill('SIGTERM');
}, timeoutMs);
}
child.once('error', (error) => {
if (timeoutId) clearTimeout(timeoutId);
reject(error);
});
child.once('close', (code, signal) => {
if (timeoutId) clearTimeout(timeoutId);
const stdoutBuffer = Buffer.concat(stdoutChunks);
const stderrBuffer = Buffer.concat(stderrChunks);
resolve({
command,
args,
spawnCommand: spawnSpec.command,
spawnArgs: spawnSpec.args,
code: typeof code === 'number' ? code : null,
signal: signal || null,
stdoutBuffer,
stderrBuffer,
stdout: decodeOutput(stdoutBuffer, { platform }),
stderr: decodeOutput(stderrBuffer, { platform }),
});
});
});
}
export function commandExists(command) {
if (!command || typeof command !== 'string') {
return false;
}
const checker = process.platform === 'win32' ? 'where' : 'which';
const result = runCommandSync({
command: checker,
args: [command],
timeoutMs: 2000,
});
return result.status === 0;
}
export function getPreferredNpmCommand() {
if (process.platform !== 'win32') {
return 'npm';
}
return commandExists('npm.cmd') ? 'npm.cmd' : 'npm';
}
export function getPreferredNpxCommand() {
if (process.platform !== 'win32') {
return 'npx';
}
return commandExists('npx.cmd') ? 'npx.cmd' : 'npx';
}
export function getSpawnCommandSpec(command, args = [], platform = process.platform) {
return getSpawnSpec(command, args, platform);
}
export function __resetWindowsCodePageCacheForTests() {
cachedWindowsCodePage = null;
}

View File

@@ -0,0 +1,41 @@
import ts from 'typescript';
function formatDiagnostic(diagnostic) {
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
if (!diagnostic.file || typeof diagnostic.start !== 'number') {
return message;
}
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
const sourceLine = diagnostic.file.text.split(/\r?\n/u)[line] || '';
return `${diagnostic.file.fileName}:${line + 1}:${character + 1} - ${message}\n${sourceLine}`;
}
export function validateGeneratedTsx(source, filePath = 'generated.tsx') {
const result = ts.transpileModule(source, {
fileName: filePath,
reportDiagnostics: true,
compilerOptions: {
jsx: ts.JsxEmit.ReactJSX,
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
},
});
const diagnostics = (result.diagnostics || []).filter(
(diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error,
);
return {
ok: diagnostics.length === 0,
diagnostics,
formatted: diagnostics.map(formatDiagnostic).join('\n\n'),
};
}
export function assertValidGeneratedTsx(source, filePath = 'generated.tsx') {
const validation = validateGeneratedTsx(source, filePath);
if (!validation.ok) {
throw new Error(`生成的 TSX 语法校验失败:\n${validation.formatted}`);
}
}

26
scripts/utils/serverInfo.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
export type AxhubServerRole = 'runtime' | 'admin';
export interface AxhubServerInfo {
pid: number;
port: number;
host: string;
origin: string;
projectRoot: string;
startedAt: string;
}
export interface ServerInfoPathOptions {
homeDir?: string;
}
export function readServerInfo(projectRoot: string, role: AxhubServerRole, options?: ServerInfoPathOptions): AxhubServerInfo | null;
export function getAdminServerInfoPath(projectRoot?: string, options?: ServerInfoPathOptions): string;
export function getRuntimeServerInfoPath(projectRoot: string): string;
export function writeServerInfo(
projectRoot: string,
role: AxhubServerRole,
info: AxhubServerInfo,
options?: ServerInfoPathOptions,
): AxhubServerInfo;
export function fetchHealth(origin: string, timeoutMs?: number): Promise<unknown | null>;
export function normalizeHealthServerInfo(data: unknown): AxhubServerInfo | null;

View File

@@ -0,0 +1,108 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const MAKE_STATE_DIR = path.join('.axhub', 'make');
const RUNTIME_SERVER_INFO_RELATIVE_PATH = path.join(MAKE_STATE_DIR, '.dev-server-info.json');
const ADMIN_SERVER_INFO_RELATIVE_PATH = path.join(MAKE_STATE_DIR, '.admin-server-info.json');
const MAKE_HOME_DIR_ENV = 'AXHUB_MAKE_HOME_DIR';
function resolveProjectRoot(projectRoot) {
return path.resolve(projectRoot);
}
function getServerInfoPath(projectRoot, role) {
if (role === 'admin') {
return getAdminServerInfoPath();
}
return path.join(resolveProjectRoot(projectRoot), RUNTIME_SERVER_INFO_RELATIVE_PATH);
}
function getGlobalHomeDir(options = {}) {
return options.homeDir || process.env[MAKE_HOME_DIR_ENV] || os.homedir();
}
export function getAdminServerInfoPath(_projectRoot, options = {}) {
return path.join(path.resolve(getGlobalHomeDir(options)), ADMIN_SERVER_INFO_RELATIVE_PATH);
}
export function getRuntimeServerInfoPath(projectRoot) {
return getServerInfoPath(projectRoot, 'runtime');
}
function normalizeServerInfo(data) {
if (!data || typeof data !== 'object') {
return null;
}
if (
typeof data.pid !== 'number'
|| typeof data.port !== 'number'
|| typeof data.host !== 'string'
|| typeof data.origin !== 'string'
|| typeof data.projectRoot !== 'string'
|| typeof data.startedAt !== 'string'
) {
return null;
}
return {
pid: data.pid,
port: data.port,
host: data.host,
origin: data.origin,
projectRoot: resolveProjectRoot(data.projectRoot),
startedAt: data.startedAt,
};
}
export function readServerInfo(projectRoot, role, options = {}) {
const infoPath = role === 'admin'
? getAdminServerInfoPath(projectRoot, options)
: getServerInfoPath(projectRoot, role);
if (!fs.existsSync(infoPath)) {
return null;
}
try {
return normalizeServerInfo(JSON.parse(fs.readFileSync(infoPath, 'utf8')));
} catch {
return null;
}
}
export function writeServerInfo(projectRoot, role, info, options = {}) {
const normalized = {
...info,
projectRoot: resolveProjectRoot(info.projectRoot),
};
const infoPath = role === 'admin'
? getAdminServerInfoPath(projectRoot, options)
: getServerInfoPath(projectRoot, role);
fs.mkdirSync(path.dirname(infoPath), { recursive: true });
fs.writeFileSync(infoPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
return normalized;
}
export async function fetchHealth(origin, timeoutMs = 1000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(new URL('/api/health', origin), {
signal: controller.signal,
headers: { accept: 'application/json' },
});
if (!response.ok) {
return null;
}
return await response.json();
} catch {
return null;
} finally {
clearTimeout(timeout);
}
}
export function normalizeHealthServerInfo(data) {
if (!data || typeof data !== 'object') {
return null;
}
return normalizeServerInfo(data.server ?? data);
}