config | pm-astro cli

This commit is contained in:
babayaga 2025-12-27 19:36:05 +01:00
parent 5c81694024
commit 940be340fa
14 changed files with 1452 additions and 168 deletions

View File

@ -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"
}
}
}

View File

@ -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()

View File

@ -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)

View File

@ -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<typeof E_RouterTypeSchema>
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<typeof E_AppendMode>
// Define the new enum for wrap modes
export const E_WrapMode = z.enum(['meta', 'none'])
export type E_WrapModeType = z.infer<typeof E_WrapMode>
// 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<typeof E_GlobExtension>
export type OptionsSchemaMeta = Record<string, unknown>
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<OptionsSchemaMeta>()
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())
}

View File

@ -0,0 +1,3 @@
export * from './config.js'
export * from './config-loader.js'

View File

@ -0,0 +1,6 @@
#!/usr/bin/env node
import { cli } from './cli.js';
import './commands/build-config.js';
import './commands/build.js';
cli.parse();

View File

@ -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 <cmd> [args]')
.demandCommand(1, 'You need at least one command before moving on')
.help();

View File

@ -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);

View File

@ -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);

View File

@ -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'

View File

@ -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<string>()
allProducts.forEach(product => {
const segments = product.id.split('/').filter(segment => segment !== '')
const folders = new Set<string>();
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 (
<BaseLayout>
<Wrapper variant="standard" class="py-4">
<section class="mb-12">
<h1 class="mb-6 text-2xl font-semibold">
<Translate>{categoryTitle}</Translate>
</h1>
<p class="text-lg text-gray-600 mb-8">
<Translate>{categoryDescription}</Translate>
</p>
{Object.keys(groupedProducts).length > 0 ? (
Object.keys(groupedProducts).map((categoryKey) => (
<section class="mb-12">
<div class="grid sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2 gap-6">
{groupedProducts[categoryKey].map((product: any) => (
<StoreEntries
key={product.id}
url={store + product.id}
title={product.data.title}
type={product.data.type}
alt={product.data.title}
model={product.data}
/>
))}
</div>
</section>
))
) : (
<div class="text-center py-12">
<div class="text-gray-400 mb-4">
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">
<Translate>No products in this category</Translate>
</h3>
<p class="text-gray-600">
<Translate>This category appears to be empty.</Translate>
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 (
<BaseLayout>
<Wrapper variant="standard" class="py-4">
<section class="mb-12">
<h1 class="mb-6 text-2xl font-semibold">
<Translate>{categoryTitle}</Translate>
</h1>
<p class="text-lg text-gray-600 mb-8">
<Translate>{categoryDescription}</Translate>
</p>
</div>
)}
<div class="mt-12 text-center">
<a
href={`/${locale}/store/`}
class="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
<Translate>Back to Store</Translate>
</a>
</div>
</section>
</Wrapper>
</BaseLayout>
)
})()
) : (
(() => {
const { data } = page
return <StoreLayout frontmatter={data} {...rest}/>
})()
)}
{Object.keys(groupedProducts).length > 0 ? (
Object.keys(groupedProducts).map(
(categoryKey) => (
<section class="mb-12">
<div class="grid sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2 gap-6">
{groupedProducts[
categoryKey
].map((product: any) => (
<StoreEntries
key={product.id}
url={
store +
product.id
}
title={
product.data
.title
}
type={
product.data
.type
}
alt={
product.data
.title
}
model={product.data}
/>
))}
</div>
</section>
),
)
) : (
<div class="text-center py-12">
<div class="text-gray-400 mb-4">
<svg
class="w-16 h-16 mx-auto"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">
<Translate>
No products in this category
</Translate>
</h3>
<p class="text-gray-600">
<Translate>
This category appears to be
empty.
</Translate>
</p>
</div>
)}
<div class="mt-12 text-center">
<a
href={`/${locale}/store/`}
class="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
>
<svg
class="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
<Translate>Back to Store</Translate>
</a>
</div>
</section>
</Wrapper>
</BaseLayout>
);
})()
: (() => {
const { data } = page;
return <StoreLayout frontmatter={data} {...rest} />;
})()
}

216
packages/polymech/temp-config.d.ts vendored Normal file
View File

@ -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;
}

View File

@ -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<typeof appConfigSchema>;

View File

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false
},
"include": [
"src/bin.ts",
"src/cli.ts",
"src/commands/**/*.ts"
],
"files": []
}