diff --git a/packages/polymech/package.json b/packages/polymech/package.json index 69cb750..4e95e90 100644 --- a/packages/polymech/package.json +++ b/packages/polymech/package.json @@ -4,10 +4,15 @@ "type": "module", "scripts": { "dev": "tsc -p . --watch", - "build": "tsc -p . " + "build": "tsc -p . ", + "build:cli": "tsc -p tsconfig.cli.json" + }, + "bin": { + "pm-astro": "./dist/bin.js" }, "exports": { ".": "./dist/index.js", + "./app/*": "./src/app/*", "./plugins/*": "./plugins/*", "./base/*": "./dist/base/*", "./model/*": "./dist/model/*", @@ -30,6 +35,7 @@ "@polymech/i18n": "file:../../../polymech-mono/packages/i18n", "@polymech/kbot-d": "file:../../../polymech-mono/packages/kbot", "@polymech/log": "file:../../../polymech-mono/packages/log", + "@types/yargs": "^17.0.35", "astro": "^5.13.2", "exifreader": "^4.31.1", "find-up": "^7.0.0", @@ -43,6 +49,7 @@ "mdast-util-to-string": "^4.0.0", "node-xlsx": "^0.24.0", "p-map": "^7.0.3", + "quicktype-core": "^23.2.6", "react-jsx-parser": "^2.4.0", "reading-time": "^1.5.0", "rehype-stringify": "^10.0.1", @@ -54,4 +61,4 @@ "unist-util-visit": "^5.0.0", "yargs": "^18.0.0" } -} +} \ No newline at end of file diff --git a/packages/polymech/ref/main.ts b/packages/polymech/ref/main.ts new file mode 100644 index 0000000..8e1b20e --- /dev/null +++ b/packages/polymech/ref/main.ts @@ -0,0 +1,111 @@ +#!/usr/bin/env node +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' +import { toYargs } from '@polymech/commons' +import { createLogger } from '@polymech/log' + +import { OptionsSchema, schemas, types } from './zod_schema.js' +import { IKBotTask } from '@polymech/ai-tools' + +import helpCommand from './commands/help.js' +import { examples } from './commands/examples.js' +import { init } from './commands/init.js' +import { build } from './commands/build.js' +import { fetch } from './commands/fetch.js' +import { run } from './commands/run.js' + +import { transcribeCommand, TranscribeOptionsSchema } from './commands/transcribe.js' +import { imageCommand, ImageOptionsSchema } from './commands/images.js' +import { ttsCommand, TTSOptionsSchema } from './commands/tts.js' + +export const logger: any = createLogger('llm-tools') + +const modify = async (argv: IKBotTask) => await run(argv as IKBotTask) + +const yargOptions: any = { + onKey: ((_yargs, key, options) => { + switch (key) { + case 'prompt': + { + return _yargs.positional(key, options) + } + case 'include': + { + return _yargs.option(key, {...options, alias: key[0].toLowerCase()}) + } + } + }) +} + +yargs(hideBin(process.argv)) + .command( + 'init', + 'Initialize KBot configuration', + (yargs) => toYargs(yargs, OptionsSchema(), yargOptions), + init + ) + .command( + 'modify [prompt]', + 'Modify an existing project', + (yargs) => toYargs(yargs, OptionsSchema(), yargOptions), + modify + ) + .command( + 'image [prompt]', + 'Create or edit an image', + (yargs) => toYargs(yargs, ImageOptionsSchema(), yargOptions), + imageCommand + ) + .command( + 'transcribe', + 'Transcribe audio files', + (yargs) => toYargs(yargs, TranscribeOptionsSchema(), yargOptions), + transcribeCommand + ) + .command( + 'tts', + 'Convert text to speech using ElevenLabs', + (yargs) => toYargs(yargs, TTSOptionsSchema(), yargOptions), + ttsCommand + ) + .command( + 'types', + 'Generate types', + (yargs) => { }, + (argv) => types() + ) + .command( + 'schemas', + 'Generate schemas', + (yargs) => { }, + (argv) => schemas() + ) + .command( + 'build', + 'Build kbot essentials', + (yargs) => { }, + (argv) => build() + ) + .command( + 'fetch', + "Fetch models, to $HOME/.kbot/", + (yargs) => { }, + (argv) => fetch() + ) + .command( + 'help-md', + 'Generate markdown help', + (yargs) => { }, + helpCommand + ) + .command( + 'examples', + 'Show examples', + (yargs) => { }, + examples + ) + .command(['modify [prompt]', '$0'], 'Default command modify', + (yargs) => toYargs(yargs, OptionsSchema(), yargOptions), modify) + .help() + //.wrap(yargs.terminalWidth() - 20) + .parse() \ No newline at end of file diff --git a/packages/polymech/ref/resize.ts b/packages/polymech/ref/resize.ts new file mode 100644 index 0000000..127127c --- /dev/null +++ b/packages/polymech/ref/resize.ts @@ -0,0 +1,224 @@ +import * as CLI from 'yargs' +import { logger } from '../index.js' +import { + resize, + ResizeOptionsSchema +} from '../lib/media/images/resize.js' +import { cli } from '../cli.js' +import { + sanitize, + defaults +} from '../_cli.js' +import { toYargs } from '@polymech/commons' + +import { + IOptions, + IResizeOptions +} from '../types.js' + +export const defaultOptions = (yargs: CLI.Argv) => { + return toYargs(yargs, ResizeOptionsSchema(), { + onKey: ((yargs, key, options) => { + switch (key) { + case 'src': + return yargs.positional(key, {...options, demandOption: true}) + default: + return yargs.option(key, options) + } + }) + }).option('gui', { + boolean: true, + default: false, + describe: 'Launch interactive graphical user interface' + }) +} + +export const command = 'resize'; +export const desc = 'Resizes files'; +export const builder = defaultOptions; + +export async function handler(argv: CLI.Arguments) { + defaults() + const options = sanitize(argv) as IOptions & IResizeOptions + logger.settings.minLevel = options.logLevel as any + + // Check if GUI mode is requested + if (options.gui) { + const { renderSchemaUI } = await import('../lib/ui/electron.js') + const { ResizeOptionsSchema } = await import('../lib/media/images/resize.js') + + // Generate schema for UI + const schema = ResizeOptionsSchema() + + // Convert Zod schema to JSON Schema (enhanced) + const jsonSchema = { + type: 'object', + title: 'Resize Image Options', + properties: { + src: { + type: 'string', + title: 'Source Files', + description: 'Source file(s) or directory to resize (supports glob patterns)', + examples: ['image.jpg', '*.png', './photos/', 'images/**/*.jpg'] + }, + dst: { + type: 'string', + title: 'Destination', + description: 'Output directory or file pattern for resized images' + }, + width: { + type: 'integer', + title: 'Width (pixels)', + description: 'Target width in pixels', + minimum: 1, + maximum: 10000 + }, + height: { + type: 'integer', + title: 'Height (pixels)', + description: 'Target height in pixels', + minimum: 1, + maximum: 10000 + }, + percent: { + type: 'number', + title: 'Resize Percentage', + description: 'Resize by percentage (e.g., 50 for 50%)', + minimum: 1, + maximum: 1000 + }, + square: { + type: 'boolean', + title: 'Square Output', + description: 'Fit image within specified width to create 1:1 aspect ratio', + default: false + }, + fillColor: { + type: 'string', + title: 'Background Fill Color', + description: 'Color to use for background when creating square images', + default: 'white', + examples: ['white', 'black', 'transparent', '#FF0000'] + }, + fit: { + type: 'string', + title: 'Resize Fit Mode', + description: 'How the image should be resized to fit the dimensions', + enum: ['cover', 'contain', 'fill', 'inside', 'outside'], + default: 'cover' + }, + withoutEnlargement: { + type: 'boolean', + title: 'Prevent Enlargement', + description: 'Do not enlarge images that are smaller than target size', + default: false + }, + quality: { + type: 'integer', + title: 'Quality', + description: 'Output quality for JPEG images (1-100)', + minimum: 1, + maximum: 100, + default: 90 + } + }, + required: ['src'], + anyOf: [ + { + required: ['width'] + }, + { + required: ['height'] + }, + { + required: ['percent'] + } + ] + } + + const uiSchema = { + 'ui:order': ['src', 'dst', 'width', 'height', 'percent', 'fit', 'square', 'fillColor', 'withoutEnlargement', 'quality'], + src: { + 'ui:placeholder': 'e.g., *.jpg or ./photos/', + 'ui:help': 'Supports files, directories, and glob patterns', + 'ui:widget': 'textarea', + 'ui:options': { + rows: 2 + } + }, + dst: { + 'ui:placeholder': 'e.g., ./resized/ or output.jpg', + 'ui:help': 'Output directory or file pattern' + }, + width: { + 'ui:help': 'Leave empty to maintain aspect ratio' + }, + height: { + 'ui:help': 'Leave empty to maintain aspect ratio' + }, + percent: { + 'ui:help': 'Alternative to width/height - resize by percentage' + }, + fit: { + 'ui:help': 'How to fit image when both width and height are specified' + }, + fillColor: { + 'ui:help': 'Used when square mode creates padding', + 'ui:placeholder': 'white, black, transparent, #FF0000' + }, + square: { + 'ui:help': 'Creates square images by padding with fillColor' + }, + withoutEnlargement: { + 'ui:help': 'Prevents making small images larger' + }, + quality: { + 'ui:help': 'Only applies to JPEG output files' + } + } + + try { + // Extract CLI options that were provided + const cliOptions: any = {}; + if (options.src) cliOptions.src = options.src; + if (options.dst) cliOptions.dst = options.dst; + if (options.width) cliOptions.width = options.width; + if (options.height) cliOptions.height = options.height; + if (options.percent) cliOptions.percent = options.percent; + if (options.square !== undefined) cliOptions.square = options.square; + if (options.fillColor) cliOptions.fillColor = options.fillColor; + if (options.fit) cliOptions.fit = options.fit; + if (options.withoutEnlargement !== undefined) cliOptions.withoutEnlargement = options.withoutEnlargement; + + const result = await renderSchemaUI({ + schema: jsonSchema, + uiSchema, + title: 'Resize Images', + formData: { + fillColor: 'white', + square: false, + fit: 'cover', + withoutEnlargement: false, + quality: 90 + }, + cliOptions + }) + + if (result && result.data) { + // Merge UI result with CLI options + const uiOptions = { ...options, ...result.data } + await resize(uiOptions) + } else { + logger.info('Operation cancelled by user') + } + } catch (error) { + logger.error('GUI Error:', error) + logger.info('Falling back to CLI mode...') + await resize(options) + } + } else { + await resize(options) + } +} + +cli.command(command, desc, builder, handler) diff --git a/packages/polymech/ref/zod_schema.ts b/packages/polymech/ref/zod_schema.ts new file mode 100644 index 0000000..f4942e8 --- /dev/null +++ b/packages/polymech/ref/zod_schema.ts @@ -0,0 +1,345 @@ +import { z } from 'zod' +import * as path from 'node:path' +import chalk from 'chalk' +import env from 'env-var' +import { generate_interfaces, ZodMetaMap, resolve, write } from '@polymech/commons' +import { sync as exists } from '@polymech/fs/exists' +import { sync as writeFS } from '@polymech/fs/write' +import { sync as readFS } from '@polymech/fs/read' +import { isArray, isFunction, isString } from '@polymech/core/primitives' +import { zodResponseFormat } from "openai/helpers/zod" + +import { API_PREFIX, LOGGING_DIRECTORY, PREFERENCES_FILE_NAME } from './constants.js' + +export const get_var = (key: string = '') => env.get(key).asString() || env.get(key.replace(/-/g, '_')).asString() || env.get(key.replace(/_/g, '-')).asString() +export const HOME = (sub = '') => path.join(process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'] || '', sub) +export const PREFERENCES_DEFAULT = (key: string = 'KBOT_PREFERENCES') => get_var(key) || path.join(HOME(`.${API_PREFIX}`), PREFERENCES_FILE_NAME) +import { jsonSchemaToZod } from "json-schema-to-zod" +import { Filters } from './filters.js' +import { models_dist } from './models/index.js' +import { defaultTemplate } from './tools.js' + +export const E_Filters = z.enum(Object.keys(Filters) as any) + +export const E_RouterTypeSchema = z.enum(['openrouter', 'openai', 'deepseek', 'huggingface', 'ollama', 'fireworks', 'gemini', 'xai']) +export type E_RouterType = z.infer + +export const E_Mode = { + COMPLETION: 'completion', + TOOLS: 'tools', + ASSISTANT: 'assistant', + RESPONSES: 'responses', + CUSTOM: 'custom' +} as const + +export const EType = z.enum([ + E_Mode.COMPLETION, + E_Mode.TOOLS, + E_Mode.ASSISTANT, + E_Mode.RESPONSES, + E_Mode.CUSTOM +]) + +// Define the new enum for append modes +export const E_AppendMode = z.enum(['concat', 'merge', 'replace']) +export type E_AppendModeType = z.infer + + +// Define the new enum for wrap modes +export const E_WrapMode = z.enum(['meta', 'none']) +export type E_WrapModeType = z.infer + +// Define the new enum for glob extensions (presets) +export const E_GlobExtension = z.enum(['match-cpp']) // Add more presets here later if needed +export type E_GlobExtensionType = z.infer + +export type OptionsSchemaMeta = Record + +export { fetchOpenRouterModels, listModelsAsStrings as listOpenRouterModelsAsStrings } from './models/openrouter.js' +export { fetchOpenAIModels, listModelsAsStrings as listOpenAIModelsAsStrings } from './models/openai.js' + +let schemaMap + +export const OptionsSchema = (opts?: any): any => { + + schemaMap = ZodMetaMap.create() + schemaMap.add( + 'path', + z.string() + .min(1) + .default('.') + .describe('Target directory') + , { 'ui:widget': 'file' }) + .add( + 'prompt', + z.string() + .describe('The prompt. Supports file paths and environment variables.') + .optional() + ) + .add( + 'output', + z.string() + .optional() + .describe('Optional output path for modified files (Tool mode only)') + ) + .add( + 'dst', + z.string() + .optional() + .describe('Optional destination path for the result, will substitute ${MODEL_NAME} and ${ROUTER} in the path. Optional, used for "completion" mode') + ) + .add( + 'append', + E_AppendMode + .optional() + .describe('How to handle output if --dst file already exists: "concat" (append) or "merge" (try to merge structures if possible, otherwise append). Only used if --dst is specified.') + ) + .add( + 'wrap', + E_WrapMode + .default('none') + .describe('Specify how to wrap the output, "meta (file name, absolute path, cwd)" or "none".') + ) + .add( + 'each', + z.string() + .optional() + .describe('Iterate over items, supported: GLOB | Path to JSON File | array of strings (comma separated). To test different models, use --each="gpt-3.5-turbo,gpt-4o", the actual string will exposed as variable `ITEM`, eg: --dst="${ITEM}-output.md"') + ) + .add( + 'disable', + z.array(z.string()) + .default([]) + .describe(`Disable tools categories, eg: --disable=${defaultTemplate.tools.join(',')}`) + ) + .add( + 'disableTools', + z.array(z.string()) + .optional() + .default([]) + .describe('List of specific tools to disable') + ) + .add( + 'tools', + z.union( + [ + z.array(z.string()), + z.string() + ]).optional() + .default(defaultTemplate.tools) + .describe(`List of tools to use. Can be built-in tool names or paths to custom tool files. Default: ${defaultTemplate.tools.join(',')}`) + .transform((val) => Array.isArray(val) ? val : val.split(',')) + ) + .add( + 'include', + z.array(z.string()) + .optional() + .describe('Comma separated glob patterns or paths, eg --include=src/*.tsx,src/*.ts --include=package.json') + ) + .add( + 'exclude', + z.array(z.string()) + .optional() + .describe('Comma separated glob patterns or paths, eg --exclude=src/*.tsx,src/*.ts --exclude=package.json') + ) + .add( + 'globExtension', + z.union([E_GlobExtension, z.string()]) // Allow preset enum or custom string + .optional() + .describe( + 'Specify a glob extension behavior. Available presets: ' + E_GlobExtension.options.join(', ') + + '. Also accepts a custom glob pattern with variables like ${SRC_DIR}, ${SRC_NAME}, ${SRC_EXT}. ' + + 'E.g., \"match-cpp\" or \"${SRC_DIR}/${SRC_NAME}*.cpp\"' + ) + ) + .add( + 'api_key', + z.string() + .optional() + .describe('Explicit API key to use') + ) + .add( + 'model', + z.string() + .optional() + .describe(`AI model to use for processing. Available models:\n${models_dist().join('\n')}`) + ) + .add( + 'router', + z.string() + .default('openrouter') + .describe('Router to use: openai, openrouter or deepseek') + ) + .add( + 'mode', + EType + .default(E_Mode.TOOLS) + .describe(`Chat completion mode:\n\t completion, tools, assistant. + ${chalk.green.bold('completion')}: no support for tools, please use --dst parameter to save the output. + ${chalk.green.bold('tools')}: allows for tools to be used, eg 'save to ./output.md'. Not all models support this mode. + ${chalk.green.bold('responses')}: allows for responses to be used, eg 'save to ./output.md'. Not all models support this mode. + ${chalk.green.bold('assistant')}: : allows documents (PDF, DOCX, ...) to be added but dont support tools. Use --dst to save the output. Supported files : + ${chalk.green.bold('custom')}: custom mode + `) + ) + .add( + 'logLevel', + z.number() + .default(4) + .describe('Logging level for the application') + ) + .add( + 'profile', + z.string() + .optional() + .describe('Path to profile for variables. Supports environment variables.') + ) + .add( + 'baseURL', + z.string() + .optional() + .describe('Base URL for the API, set via --router or directly') + ) + .add( + 'config', + z.string() + .optional() + .describe('Path to JSON configuration file (API keys). Supports environment variables.') + ) + .add( + 'dump', + z.string() + .optional() + .describe('Create a script') + ) + .add( + 'preferences', + z.string() + .default(PREFERENCES_DEFAULT()) + .describe('Path to preferences file, eg: location, your email address, gender, etc. Supports environment variables.') + ) + .add( + 'logs', + z.string() + .default(LOGGING_DIRECTORY) + .describe('Logging directory') + ).add( + 'stream', + z.boolean() + .default(false) + .describe('Enable streaming (verbose LLM output)') + ) + .add( + 'alt', + z.boolean() + .default(false) + .describe('Use alternate tokenizer & instead of $') + ) + .add( + 'env', + z.string() + .default('default') + .describe('Environment (in profile)') + ) + .add( + 'variables', + z.record(z.string(), z.string()) + .optional() + .default({}) + ) + .add( + 'filters', + z.union([ + z.string(), + z.array(E_Filters), + z.array(z.string()), + z.array(z.function()) + ]) + .optional() + .default('') + .describe(`List of filters to apply to the output. + Used only in completion mode and a given output file specified with --dst. + It unwraps by default any code or data in Markdown. + Choices:\n\t${Object.keys(Filters)}\n`) + .transform((val) => { + if (isArray(val) && val.length && isFunction(val[0])) { + return val + } + let filters = isString(val) ? val.split(',') : val + filters = filters.map((f: any) => Filters[f]).filter(Boolean) + return filters + }) + ) + .add( + 'query', + z.string() + .nullable() + .optional() + .default(null) + .describe('JSONPath query to be used to transform input objects') + ) + .add( + 'dry', + z.union([ + z.boolean(), + z.string().transform((val) => val.toLowerCase() === 'true') + ]) + .optional() + .default(false) + .describe('Dry run - only write out parameters without making API calls') + ) + .add( + 'format', + z.union([ + z.string().transform((val) => { + try { + // Check if the string is a file path + if (exists(val) && val.endsWith('.json')) { + const content = readFS(val); + const schema = JSON.parse(content.toString()); + const zodSchemaStr = jsonSchemaToZod(schema); + // Evaluate the string to get the actual Zod schema + const zodSchema = eval(`(${zodSchemaStr})`); + return zodResponseFormat(zodSchema, "format"); + } else { + // Try parsing as JSON schema first + try { + const schema = JSON.parse(val); + const zodSchemaStr = jsonSchemaToZod(schema); + const zodSchema = eval(`(${zodSchemaStr})`); + return zodResponseFormat(zodSchema, "format"); + } catch { + // If not JSON, try evaluating as Zod schema directly + const zodSchema = eval(`(${val})`); + return zodResponseFormat(zodSchema, "format"); + } + } + } catch (e) { + console.error(`Error parsing format: ${e}`) + return null; + } + }), + z.any().transform((val) => { + // If it's already a Zod schema, use it directly + if (val && typeof val === 'object' && 'parse' in val) { + return zodResponseFormat(val, "format"); + } + return null; + }) + ]) + .optional() + .describe('Format for structured outputs. Can be a Zod schema, a Zod schema string, a JSON schema string, or a path to a JSON file.') + ); + return schemaMap.root() + .passthrough() + .describe('IKBotOptions') +} +export const types = () => { + generate_interfaces([OptionsSchema()], 'src/zod_types.ts') + generate_interfaces([OptionsSchema()], path.resolve(resolve('../ai-tools/src/types_kbot.ts'))) + schemas() +} +export const schemas = () => { + write([OptionsSchema()], 'schema.json', 'kbot', {}) + writeFS('schema_ui.json', schemaMap.getUISchema()) +} \ No newline at end of file diff --git a/packages/polymech/src/app/index.ts b/packages/polymech/src/app/index.ts new file mode 100644 index 0000000..a3753c8 --- /dev/null +++ b/packages/polymech/src/app/index.ts @@ -0,0 +1,3 @@ +export * from './config.js' +export * from './config-loader.js' + diff --git a/packages/polymech/src/bin.ts b/packages/polymech/src/bin.ts new file mode 100644 index 0000000..83e82f4 --- /dev/null +++ b/packages/polymech/src/bin.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { cli } from './cli.js'; +import './commands/build-config.js'; +import './commands/build.js'; + +cli.parse(); diff --git a/packages/polymech/src/cli.ts b/packages/polymech/src/cli.ts new file mode 100644 index 0000000..cfba8b4 --- /dev/null +++ b/packages/polymech/src/cli.ts @@ -0,0 +1,8 @@ +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +export const cli = yargs(hideBin(process.argv)) + .scriptName('pm-astro') + .usage('$0 [args]') + .demandCommand(1, 'You need at least one command before moving on') + .help(); diff --git a/packages/polymech/scripts/generate-app-config.ts b/packages/polymech/src/commands/build-config.ts similarity index 53% rename from packages/polymech/scripts/generate-app-config.ts rename to packages/polymech/src/commands/build-config.ts index fe8c9eb..6cc24ce 100644 --- a/packages/polymech/scripts/generate-app-config.ts +++ b/packages/polymech/src/commands/build-config.ts @@ -1,14 +1,43 @@ - +import * as CLI from 'yargs' import fs from 'fs'; import path from 'path'; import { quicktype, InputData, jsonInputForTargetLanguage } from 'quicktype-core'; +import { cli } from '../cli.js'; -const CONFIG_PATH = path.resolve('./app-config.json'); -const OUTPUT_SCHEMA_PATH = path.resolve('./src/app/config.schema.ts'); -const OUTPUT_DTS_PATH = path.resolve('./src/app/config.d.ts'); +export const command = 'build:config [src]'; +export const desc = 'Generate config types and schema from JSON'; +export const builder = (yargs: CLI.Argv) => { + return yargs + .positional('src', { + describe: 'Input JSON config path', + type: 'string', + default: './app-config.json', + alias: 'config' + }) + .option('dest-types', { + alias: 'd', + type: 'string', + describe: 'Output d.ts path', + default: './src/app/config.d.ts' + }) + .option('dest-schema', { + alias: 'z', + type: 'string', + describe: 'Output Zod schema path', + default: './src/app/config.schema.ts' + }); +}; + +export async function handler(argv: CLI.Arguments) { + const CONFIG_PATH = path.resolve(argv.src as string); + const OUTPUT_DTS_PATH = path.resolve(argv.destTypes as string); + const OUTPUT_SCHEMA_PATH = path.resolve(argv.destSchema as string); -async function main() { console.log(`Reading config from ${CONFIG_PATH}...`); + if (!fs.existsSync(CONFIG_PATH)) { + console.error(`Config file not found at ${CONFIG_PATH}`); + process.exit(1); + } const configContent = fs.readFileSync(CONFIG_PATH, 'utf8'); // 1. Generate TypeScript Definitions (d.ts) FIRST @@ -28,11 +57,12 @@ async function main() { lang: "ts", rendererOptions: { "just-types": "true", - "acronym-style": "original" + "acronym-style": "original" // Keep naming consistent } }); const tsCode = tsResult.lines.join('\n'); + fs.mkdirSync(path.dirname(OUTPUT_DTS_PATH), { recursive: true }); fs.writeFileSync(OUTPUT_DTS_PATH, tsCode); console.log(`Wrote TypeScript definitions to ${OUTPUT_DTS_PATH}`); @@ -46,6 +76,9 @@ async function main() { const relDts = path.relative(process.cwd(), OUTPUT_DTS_PATH); const relSchema = path.relative(process.cwd(), OUTPUT_SCHEMA_PATH); + // Ensure output dir exists + fs.mkdirSync(path.dirname(OUTPUT_SCHEMA_PATH), { recursive: true }); + execSync(`npx ts-to-zod "${relDts}" "${relSchema}"`, { stdio: 'inherit', cwd: process.cwd() }); // Append export type AppConfig @@ -58,7 +91,4 @@ async function main() { } } -main().catch(err => { - console.error('Error fetching/generating config:', err); - process.exit(1); -}); +cli.command(command, desc, builder, handler); diff --git a/packages/polymech/src/commands/build.ts b/packages/polymech/src/commands/build.ts new file mode 100644 index 0000000..efe0394 --- /dev/null +++ b/packages/polymech/src/commands/build.ts @@ -0,0 +1,38 @@ +import { spawnSync } from 'child_process'; +import * as CLI from 'yargs'; +import { cli } from '../cli.js'; +import { handler as configHandler, builder as configBuilder } from './build-config.js'; + +export const command = 'build [src]'; +export const desc = 'Generate config and build Astro app'; + +export const builder = (yargs: CLI.Argv) => { + return configBuilder(yargs) + .strict(false); +}; + +export async function handler(argv: CLI.Arguments) { + // 1. Config Generation + // We execute the config generation logic in-process + await configHandler(argv); + + // 2. Astro Build + // We forward arguments to Astro. + // Finding where 'build' command is in the args list. + // Note: process.argv = [node, bin, 'build', ...flags] + const buildIndex = process.argv.indexOf('build'); + const forwardedArgs = buildIndex !== -1 ? process.argv.slice(buildIndex + 1) : []; + + console.log('[pm-astro] Running astro build...'); + // Execute astro build using npx to resolve local binary + const result = spawnSync('npx', ['astro', 'build', ...forwardedArgs], { + stdio: 'inherit', + shell: true + }); + + if (result.status !== 0) { + process.exit(result.status || 1); + } +} + +cli.command(command, desc, builder, handler); diff --git a/packages/polymech/src/index.ts b/packages/polymech/src/index.ts index d0fbd45..14cda49 100644 --- a/packages/polymech/src/index.ts +++ b/packages/polymech/src/index.ts @@ -5,7 +5,10 @@ export const foo2 = 2 // export { default as Test } from './components/test.astro' // Export collection utilities -export * from './base/collections' +export * from './base/collections.js' +export * from './app/index.js' + + diff --git a/packages/polymech/src/pages/[locale]/store/[...path].astro b/packages/polymech/src/pages/[locale]/store/[...path].astro index b4d85e8..4cd72f7 100644 --- a/packages/polymech/src/pages/[locale]/store/[...path].astro +++ b/packages/polymech/src/pages/[locale]/store/[...path].astro @@ -1,21 +1,18 @@ --- -import StoreLayout from '@/layouts/StoreLayout.astro' -import { getCollection } from 'astro:content' -import { LANGUAGES_PROD } from "config/config.js" +import StoreLayout from "@/layouts/StoreLayout.astro"; +import { getCollection } from "astro:content"; +import { LANGUAGES_PROD } from "config/config.js"; import StoreEntries from "@/components/store/StoreEntries.astro"; import BaseLayout from "@/layouts/BaseLayout.astro"; import Wrapper from "@/components/containers/Wrapper.astro"; import Translate from "@polymech/astro-base/components/i18n.astro"; -import { translate } from "@/base/i18n.js" -import { I18N_SOURCE_LANGUAGE } from "config/config.js" -import { slugify } from "@/base/strings.js" +import { slugify } from "@/base/strings.js"; + +export async function getStaticPaths() { + const view = "store"; + const allProducts = await getCollection(view); + const all: unknown[] = []; -export async function getStaticPaths() -{ - const view = 'store' - const allProducts = await getCollection(view) - const all: unknown[] = [] - LANGUAGES_PROD.forEach((lang) => { // Add individual product routes allProducts.forEach((product) => { @@ -29,26 +26,28 @@ export async function getStaticPaths() locale: lang, path: product.id, view, - type: 'product' - } - }) - }) - + type: "product", + }, + }); + }); + // Add folder routes for categories - const folders = new Set() - allProducts.forEach(product => { - const segments = product.id.split('/').filter(segment => segment !== '') + const folders = new Set(); + allProducts.forEach((product) => { + const segments = product.id + .split("/") + .filter((segment) => segment !== ""); if (segments.length > 1) { // Add all possible folder paths (e.g., products, products/sheetpress, etc.) for (let i = 1; i < segments.length; i++) { - const folderPath = segments.slice(0, i).join('/') - folders.add(folderPath) + const folderPath = segments.slice(0, i).join("/"); + folders.add(folderPath); } } - }) - + }); + // Add folder paths - Array.from(folders).forEach(folder => { + Array.from(folders).forEach((folder) => { all.push({ params: { locale: lang, @@ -56,160 +55,221 @@ export async function getStaticPaths() }, props: { folderPath: folder, - products: allProducts.filter(product => { - const productFolder = product.id.split('/').slice(0, -1).join('/') - return productFolder === folder + products: allProducts.filter((product) => { + const productFolder = product.id + .split("/") + .slice(0, -1) + .join("/"); + return productFolder === folder; }), locale: lang, view, - type: 'folder' - } - }) - }) - + type: "folder", + }, + }); + }); + // Add special case for store/ (root store) - shows all products all.push({ params: { locale: lang, - path: '', + path: "", }, props: { - folderPath: '', + folderPath: "", products: allProducts, // All products locale: lang, view, - type: 'folder', - isRootStore: true - } - }) - + type: "folder", + isRootStore: true, + }, + }); + // Add special case for store/products/ - shows all products all.push({ params: { locale: lang, - path: 'products', + path: "products", }, props: { - folderPath: 'products', + folderPath: "products", products: allProducts, // All products locale: lang, view, - type: 'folder', - isProductsFolder: true - } - }) - }) - - return all + type: "folder", + isProductsFolder: true, + }, + }); + }); + + return all; } -const { page, folderPath, products, locale, type, isRootStore, isProductsFolder, ...rest } = Astro.props as any +const { + page, + folderPath, + products, + locale, + type, + isRootStore, + isProductsFolder, + ...rest +} = Astro.props as any; --- -{type === 'folder' ? ( - (() => { - // Handle folder view - show all products in category - let categoryTitle: string - const categoryDescription = 'Browse our complete collection of products' - if (isRootStore) { - // Root store - all products - categoryTitle = 'All Products' - } else if (isProductsFolder) { - // Products folder - all products - categoryTitle = 'Products' - } else { - // Regular category folder - const categoryName = folderPath.split('/').pop() || folderPath - categoryTitle = categoryName - } - categoryTitle = categoryTitle.charAt(0).toUpperCase() + categoryTitle.slice(1) - - // Group products by category for all products views - let groupedProducts: any = {} - if (isRootStore || isProductsFolder) { - // Use the same grouping logic as homepage - simplified for now - groupedProducts = products.reduce((acc, item: any) => { - const id = item.id.split("/")[1] - const key = slugify(id) - .split('-') - .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(' ') - if (!acc[key]) { - acc[key] = [] +{ + type === "folder" + ? (() => { + // Handle folder view - show all products in category + let categoryTitle: string; + const categoryDescription = + "Browse our complete collection of products"; + if (isRootStore) { + // Root store - all products + categoryTitle = "All Products"; + } else if (isProductsFolder) { + // Products folder - all products + categoryTitle = "Products"; + } else { + // Regular category folder + const categoryName = + folderPath.split("/").pop() || folderPath; + categoryTitle = categoryName; } - acc[key].push(item) - return acc - }, {}) - } else { - // For category views, just sort products by title - groupedProducts = { - [categoryTitle]: products.sort((a: any, b: any) => - a.data.title.localeCompare(b.data.title) - ) - } - } - const store = `/${locale}/store/`; - return ( - - -
-

- {categoryTitle} -

-

- {categoryDescription} -

- {Object.keys(groupedProducts).length > 0 ? ( - Object.keys(groupedProducts).map((categoryKey) => ( -
-
- {groupedProducts[categoryKey].map((product: any) => ( - - ))} -
-
- )) - ) : ( -
-
- - - -
-

- No products in this category -

-

- This category appears to be empty. + categoryTitle = + categoryTitle.charAt(0).toUpperCase() + + categoryTitle.slice(1); + + // Group products by category for all products views + let groupedProducts: any = {}; + if (isRootStore || isProductsFolder) { + // Use the same grouping logic as homepage - simplified for now + groupedProducts = products.reduce((acc, item: any) => { + const id = item.id.split("/")[1]; + const key = slugify(id) + .split("-") + .map( + (word) => + word.charAt(0).toUpperCase() + + word.slice(1).toLowerCase(), + ) + .join(" "); + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(item); + return acc; + }, {}); + } else { + // For category views, just sort products by title + groupedProducts = { + [categoryTitle]: products.sort((a: any, b: any) => + a.data.title.localeCompare(b.data.title), + ), + }; + } + const store = `/${locale}/store/`; + return ( + + +

+

+ {categoryTitle} +

+

+ {categoryDescription}

-
- )} - -
-
-
- ) - })() -) : ( - (() => { - const { data } = page - return - })() -)} + {Object.keys(groupedProducts).length > 0 ? ( + Object.keys(groupedProducts).map( + (categoryKey) => ( +
+
+ {groupedProducts[ + categoryKey + ].map((product: any) => ( + + ))} +
+
+ ), + ) + ) : ( +
+
+ + + +
+

+ + No products in this category + +

+

+ + This category appears to be + empty. + +

+
+ )} + + + + + ); + })() + : (() => { + const { data } = page; + return ; + })() +} diff --git a/packages/polymech/temp-config.d.ts b/packages/polymech/temp-config.d.ts new file mode 100644 index 0000000..799aed6 --- /dev/null +++ b/packages/polymech/temp-config.d.ts @@ -0,0 +1,216 @@ +export interface AppConfig { + site: Site; + footer_left: FooterLeft[]; + footer_right: any[]; + settings: Settings; + params: Params; + navigation: Navigation; + navigation_button: NavigationButton; + ecommerce: Ecommerce; + metadata: Metadata; + shopify: Shopify; + pages: Pages; + core: Core; + dev: Dev; + i18n: I18N; + products: Products; + retail: Retail; + rss: Rss; + osrl: Osrl; + features: { [key: string]: boolean }; + defaults: Defaults; + cad: Cad; + assets: Assets; + optimization: Optimization; +} + +export interface Assets { + cad_url: string; + url: string; + item_url_r: string; + item_url: string; +} + +export interface Cad { + cache: boolean; + export_configurations: boolean; + export_sub_components: boolean; + renderer: string; + renderer_view: string; + renderer_quality: number; + extensions: string[]; + model_ext: string; + default_configuration: string; + main_match: string; + cam_main_match: string; +} + +export interface Core { + logging_namespace: string; + translate_content: boolean; + languages: string[]; + languages_prod: string[]; + rtl_languages: string[]; + osr_root: string; +} + +export interface Defaults { + image_url: string; + license: string; + contact: string; +} + +export interface Dev { + file_server: string; +} + +export interface Ecommerce { + brand: string; + currencySymbol: string; + currencyCode: string; +} + +export interface FooterLeft { + href: string; + text: string; +} + +export interface I18N { + store: string; + cache: boolean; + asset_path: string; +} + +export interface Metadata { + country: string; + city: string; + author: string; + author_bio: string; + author_url: string; + image: string; + description: string; + keywords: string; +} + +export interface Navigation { + top: FooterLeft[]; +} + +export interface NavigationButton { + enable: boolean; + label: string; + link: string; +} + +export interface Optimization { + image_settings: ImageSettings; + presets: Presets; +} + +export interface ImageSettings { + gallery: Gallery; + lightbox: Gallery; +} + +export interface Gallery { + show_title: boolean; + show_description: boolean; + sizes_thumb: string; + sizes_large: string; + sizes_regular: string; +} + +export interface Presets { + slow: Fast; + medium: Fast; + fast: Fast; +} + +export interface Fast { + sizes_medium: string; + sizes_thumbs: string; + sizes_large: string; +} + +export interface Osrl { + env: string; + env_dev: string; + module_name: string; + lang_flavor: string; + product_profile: string; +} + +export interface Pages { + home: Home; +} + +export interface Home { + hero: string; + _blog: Blog; +} + +export interface Blog { + store: string; +} + +export interface Params { + contact_form_action: string; + copyright: string; +} + +export interface Products { + root: string; + howto_migration: string; + glob: string; + enabled: string; +} + +export interface Retail { + library_branch: string; + projects_branch: string; +} + +export interface Rss { + title: string; + description: string; +} + +export interface Settings { + search: boolean; + account: boolean; + sticky_header: boolean; + theme_switcher: boolean; + default_theme: string; +} + +export interface Shopify { + currencySymbol: string; + currencyCode: string; + collections: Collections; +} + +export interface Collections { + hero_slider: string; + featured_products: string; +} + +export interface Site { + title: string; + base_url: string; + description: string; + base_path: string; + trailing_slash: boolean; + favicon: string; + logo: string; + logo_darkmode: string; + logo_width: string; + logo_height: string; + logo_text: string; + image: Image; +} + +export interface Image { + default: string; + error: string; + alt: string; +} diff --git a/packages/polymech/temp-config.schema.ts b/packages/polymech/temp-config.schema.ts new file mode 100644 index 0000000..bc27d76 --- /dev/null +++ b/packages/polymech/temp-config.schema.ts @@ -0,0 +1,221 @@ +// Generated by ts-to-zod +import { z } from "zod"; + +export const footerLeftSchema = z.object({ + href: z.string(), + text: z.string() +}); + +export const settingsSchema = z.object({ + search: z.boolean(), + account: z.boolean(), + sticky_header: z.boolean(), + theme_switcher: z.boolean(), + default_theme: z.string() +}); + +export const paramsSchema = z.object({ + contact_form_action: z.string(), + copyright: z.string() +}); + +export const navigationSchema = z.object({ + top: z.array(footerLeftSchema) +}); + +export const navigationButtonSchema = z.object({ + enable: z.boolean(), + label: z.string(), + link: z.string() +}); + +export const ecommerceSchema = z.object({ + brand: z.string(), + currencySymbol: z.string(), + currencyCode: z.string() +}); + +export const metadataSchema = z.object({ + country: z.string(), + city: z.string(), + author: z.string(), + author_bio: z.string(), + author_url: z.string(), + image: z.string(), + description: z.string(), + keywords: z.string() +}); + +export const coreSchema = z.object({ + logging_namespace: z.string(), + translate_content: z.boolean(), + languages: z.array(z.string()), + languages_prod: z.array(z.string()), + rtl_languages: z.array(z.string()), + osr_root: z.string() +}); + +export const devSchema = z.object({ + file_server: z.string() +}); + +export const i18NSchema = z.object({ + store: z.string(), + cache: z.boolean(), + asset_path: z.string() +}); + +export const productsSchema = z.object({ + root: z.string(), + howto_migration: z.string(), + glob: z.string(), + enabled: z.string() +}); + +export const retailSchema = z.object({ + library_branch: z.string(), + projects_branch: z.string() +}); + +export const rssSchema = z.object({ + title: z.string(), + description: z.string() +}); + +export const osrlSchema = z.object({ + env: z.string(), + env_dev: z.string(), + module_name: z.string(), + lang_flavor: z.string(), + product_profile: z.string() +}); + +export const defaultsSchema = z.object({ + image_url: z.string(), + license: z.string(), + contact: z.string() +}); + +export const cadSchema = z.object({ + cache: z.boolean(), + export_configurations: z.boolean(), + export_sub_components: z.boolean(), + renderer: z.string(), + renderer_view: z.string(), + renderer_quality: z.number(), + extensions: z.array(z.string()), + model_ext: z.string(), + default_configuration: z.string(), + main_match: z.string(), + cam_main_match: z.string() +}); + +export const assetsSchema = z.object({ + cad_url: z.string(), + url: z.string(), + item_url_r: z.string(), + item_url: z.string() +}); + +export const gallerySchema = z.object({ + show_title: z.boolean(), + show_description: z.boolean(), + sizes_thumb: z.string(), + sizes_large: z.string(), + sizes_regular: z.string() +}); + +export const fastSchema = z.object({ + sizes_medium: z.string(), + sizes_thumbs: z.string(), + sizes_large: z.string() +}); + +export const blogSchema = z.object({ + store: z.string() +}); + +export const collectionsSchema = z.object({ + hero_slider: z.string(), + featured_products: z.string() +}); + +export const imageSchema = z.object({ + default: z.string(), + error: z.string(), + alt: z.string() +}); + +export const siteSchema = z.object({ + title: z.string(), + base_url: z.string(), + description: z.string(), + base_path: z.string(), + trailing_slash: z.boolean(), + favicon: z.string(), + logo: z.string(), + logo_darkmode: z.string(), + logo_width: z.string(), + logo_height: z.string(), + logo_text: z.string(), + image: imageSchema +}); + +export const shopifySchema = z.object({ + currencySymbol: z.string(), + currencyCode: z.string(), + collections: collectionsSchema +}); + +export const imageSettingsSchema = z.object({ + gallery: gallerySchema, + lightbox: gallerySchema +}); + +export const presetsSchema = z.object({ + slow: fastSchema, + medium: fastSchema, + fast: fastSchema +}); + +export const homeSchema = z.object({ + hero: z.string(), + _blog: blogSchema +}); + +export const pagesSchema = z.object({ + home: homeSchema +}); + +export const optimizationSchema = z.object({ + image_settings: imageSettingsSchema, + presets: presetsSchema +}); + +export const appConfigSchema = z.object({ + site: siteSchema, + footer_left: z.array(footerLeftSchema), + footer_right: z.array(z.any()), + settings: settingsSchema, + params: paramsSchema, + navigation: navigationSchema, + navigation_button: navigationButtonSchema, + ecommerce: ecommerceSchema, + metadata: metadataSchema, + shopify: shopifySchema, + pages: pagesSchema, + core: coreSchema, + dev: devSchema, + i18n: i18NSchema, + products: productsSchema, + retail: retailSchema, + rss: rssSchema, + osrl: osrlSchema, + features: z.record(z.string(), z.boolean()), + defaults: defaultsSchema, + cad: cadSchema, + assets: assetsSchema, + optimization: optimizationSchema +}); + +export type AppConfig = z.infer; diff --git a/packages/polymech/tsconfig.cli.json b/packages/polymech/tsconfig.cli.json new file mode 100644 index 0000000..c22e32d --- /dev/null +++ b/packages/polymech/tsconfig.cli.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false + }, + "include": [ + "src/bin.ts", + "src/cli.ts", + "src/commands/**/*.ts" + ], + "files": [] +} \ No newline at end of file