precis-cli 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app/__init__.py +16 -0
- app/api/__init__.py +32 -0
- app/api/dependencies.py +185 -0
- app/api/main.py +301 -0
- app/api/middleware/__init__.py +34 -0
- app/api/middleware/exception_handler.py +126 -0
- app/api/middleware/request_logging.py +97 -0
- app/api/middleware/token_auth.py +215 -0
- app/api/models/__init__.py +151 -0
- app/api/models/connection_rules.py +98 -0
- app/api/models/files.py +99 -0
- app/api/models/full_validation.py +554 -0
- app/api/models/project.py +117 -0
- app/api/models/projects.py +80 -0
- app/api/models/schema.py +121 -0
- app/api/models/v2_responses.py +46 -0
- app/api/models/validation.py +303 -0
- app/api/models/workspace.py +118 -0
- app/api/routers/__init__.py +56 -0
- app/api/routers/ai/__init__.py +36 -0
- app/api/routers/ai/chat.py +243 -0
- app/api/routers/ai/generate.py +156 -0
- app/api/routers/ai/hardware.py +199 -0
- app/api/routers/ai/jobs.py +544 -0
- app/api/routers/ai/migrate.py +471 -0
- app/api/routers/ai/models.py +352 -0
- app/api/routers/ai/ollama.py +108 -0
- app/api/routers/ai/providers.py +491 -0
- app/api/routers/ai/router.py +44 -0
- app/api/routers/ai/stream.py +281 -0
- app/api/routers/ai/utils.py +182 -0
- app/api/routers/core/__init__.py +34 -0
- app/api/routers/core/connection_rules.py +188 -0
- app/api/routers/core/data_sources.py +374 -0
- app/api/routers/core/regex.py +333 -0
- app/api/routers/core/reporting.py +263 -0
- app/api/routers/files/__init__.py +24 -0
- app/api/routers/files/ops.py +179 -0
- app/api/routers/files/transfer.py +101 -0
- app/api/routers/preview/__init__.py +35 -0
- app/api/routers/preview/content_mode.py +264 -0
- app/api/routers/preview/header_row.py +154 -0
- app/api/routers/preview/models.py +81 -0
- app/api/routers/preview/path_mode.py +440 -0
- app/api/routers/preview/router.py +30 -0
- app/api/routers/project/__init__.py +61 -0
- app/api/routers/project/base.py +111 -0
- app/api/routers/project/constraint.py +345 -0
- app/api/routers/project/full_config.py +411 -0
- app/api/routers/project/full_config_writer.py +327 -0
- app/api/routers/project/helpers.py +167 -0
- app/api/routers/project/inspection_fix.py +330 -0
- app/api/routers/project/manifest.py +605 -0
- app/api/routers/project/models.py +177 -0
- app/api/routers/project/pattern.py +268 -0
- app/api/routers/project/regex.py +358 -0
- app/api/routers/project/scanner.py +157 -0
- app/api/routers/project/schema.py +567 -0
- app/api/routers/project/schema_helpers.py +149 -0
- app/api/routers/project/settings.py +399 -0
- app/api/routers/project/template.py +325 -0
- app/api/routers/project/validation.py +227 -0
- app/api/routers/project/view.py +177 -0
- app/api/routers/project/workspaces.py +167 -0
- app/api/routers/projects/__init__.py +25 -0
- app/api/routers/projects/create.py +121 -0
- app/api/routers/projects/open.py +103 -0
- app/api/routers/projects/scan.py +100 -0
- app/api/routers/validation/__init__.py +39 -0
- app/api/routers/validation/common.py +263 -0
- app/api/routers/validation/content_mode.py +401 -0
- app/api/routers/validation/history.py +235 -0
- app/api/routers/validation/inline_mode.py +196 -0
- app/api/routers/validation/path_mode.py +314 -0
- app/api/routers/validation/router.py +51 -0
- app/api/services/full_validation_response_builder.py +412 -0
- app/api/services/io_error_messages.py +41 -0
- app/api/services/preview_service.py +248 -0
- app/cli/__init__.py +18 -0
- app/cli/__main__.py +30 -0
- app/cli/shared_services/__init__.py +42 -0
- app/cli/shared_services/config_ops.py +581 -0
- app/cli/shared_services/generation_ops.py +160 -0
- app/cli/shared_services/project_ops.py +288 -0
- app/cli/shell/__init__.py +48 -0
- app/cli/shell/commands/__init__.py +63 -0
- app/cli/shell/commands/ai/__init__.py +260 -0
- app/cli/shell/commands/ai/chat.py +331 -0
- app/cli/shell/commands/ai/delete.py +165 -0
- app/cli/shell/commands/ai/diff.py +70 -0
- app/cli/shell/commands/ai/display.py +428 -0
- app/cli/shell/commands/ai/executor.py +225 -0
- app/cli/shell/commands/ai/executor_utils.py +271 -0
- app/cli/shell/commands/ai/generate.py +285 -0
- app/cli/shell/commands/ai/interaction.py +262 -0
- app/cli/shell/commands/ai/migrate.py +281 -0
- app/cli/shell/commands/ai/resolver.py +224 -0
- app/cli/shell/commands/ai/status.py +101 -0
- app/cli/shell/commands/ai/switch.py +158 -0
- app/cli/shell/commands/ai/utils.py +52 -0
- app/cli/shell/commands/base.py +343 -0
- app/cli/shell/commands/config/__init__.py +131 -0
- app/cli/shell/commands/config/base.py +80 -0
- app/cli/shell/commands/config/check.py +246 -0
- app/cli/shell/commands/config/edit.py +148 -0
- app/cli/shell/commands/config/get.py +111 -0
- app/cli/shell/commands/config/init.py +123 -0
- app/cli/shell/commands/config/inspect.py +164 -0
- app/cli/shell/commands/config/list.py +104 -0
- app/cli/shell/commands/config/set.py +119 -0
- app/cli/shell/commands/config/show.py +150 -0
- app/cli/shell/commands/exit.py +67 -0
- app/cli/shell/commands/help.py +104 -0
- app/cli/shell/commands/infer_schema.py +156 -0
- app/cli/shell/commands/open.py +201 -0
- app/cli/shell/commands/project.py +215 -0
- app/cli/shell/commands/provider.py +619 -0
- app/cli/shell/commands/system.py +170 -0
- app/cli/shell/commands/validate.py +455 -0
- app/cli/shell/completer.py +231 -0
- app/cli/shell/config_storage.py +191 -0
- app/cli/shell/exceptions.py +90 -0
- app/cli/shell/formatter.py +371 -0
- app/cli/shell/interactive_menu.py +348 -0
- app/cli/shell/main.py +286 -0
- app/cli/shell/parser.py +266 -0
- app/cli/start.py +180 -0
- app/cli_main.py +52 -0
- app/mcp_server.py +311 -0
- app/shared/core/__init__.py +18 -0
- app/shared/core/app_version.py +59 -0
- app/shared/core/config/__init__.py +119 -0
- app/shared/core/config/server.py +193 -0
- app/shared/core/data_source/__init__.py +106 -0
- app/shared/core/data_source/loader.py +348 -0
- app/shared/core/data_source/loaders/__init__.py +213 -0
- app/shared/core/data_source/loaders/base.py +215 -0
- app/shared/core/data_source/loaders/converter.py +337 -0
- app/shared/core/data_source/loaders/csv_loader.py +252 -0
- app/shared/core/data_source/loaders/excel_loader.py +666 -0
- app/shared/core/data_source/loaders/extractor.py +268 -0
- app/shared/core/data_source/loaders/json_loader.py +354 -0
- app/shared/core/data_source/loaders/registry.py +88 -0
- app/shared/core/data_source/loaders/sql_loader.py +313 -0
- app/shared/core/data_source/loaders/strategies/__init__.py +100 -0
- app/shared/core/data_source/loaders/strategies/array_parser.py +277 -0
- app/shared/core/data_source/loaders/strategies/lines_parser.py +355 -0
- app/shared/core/data_source/loaders/strategies/object_parser.py +278 -0
- app/shared/core/data_source/schema_info.py +50 -0
- app/shared/core/data_source/specs/__init__.py +128 -0
- app/shared/core/data_source/specs/base.py +258 -0
- app/shared/core/data_source/specs/csv_source.py +137 -0
- app/shared/core/data_source/specs/excel_source.py +141 -0
- app/shared/core/data_source/specs/file_base.py +252 -0
- app/shared/core/data_source/specs/json_source.py +226 -0
- app/shared/core/data_source/specs/sql_source.py +161 -0
- app/shared/core/encoding.py +36 -0
- app/shared/core/io/__init__.py +24 -0
- app/shared/core/io/yaml.py +379 -0
- app/shared/core/manifest_schema/__init__.py +52 -0
- app/shared/core/manifest_schema/types.py +99 -0
- app/shared/core/manifest_schema/version.py +175 -0
- app/shared/core/patterns/__init__.py +24 -0
- app/shared/core/patterns/loader.py +134 -0
- app/shared/core/patterns/writer.py +244 -0
- app/shared/core/project/__init__.py +47 -0
- app/shared/core/project/constraint/builders/__init__.py +50 -0
- app/shared/core/project/constraint/builders/base.py +100 -0
- app/shared/core/project/constraint/builders/composite.py +77 -0
- app/shared/core/project/constraint/builders/conditional.py +67 -0
- app/shared/core/project/constraint/builders/foreign_key.py +53 -0
- app/shared/core/project/constraint/builders/registry.py +64 -0
- app/shared/core/project/constraint/builders/scripted.py +51 -0
- app/shared/core/project/constraint/builders/single_column.py +86 -0
- app/shared/core/project/constraint/builders/unique.py +53 -0
- app/shared/core/project/constraint/factory.py +170 -0
- app/shared/core/project/constraint/reader.py +214 -0
- app/shared/core/project/constraint/registry.py +233 -0
- app/shared/core/project/constraint/types/__init__.py +63 -0
- app/shared/core/project/constraint/types/constraint_file.py +261 -0
- app/shared/core/project/constraint/types/refs.py +460 -0
- app/shared/core/project/constraint/types.py +28 -0
- app/shared/core/project/constraint/writer.py +181 -0
- app/shared/core/project/loader/__init__.py +56 -0
- app/shared/core/project/loader/loader.py +30 -0
- app/shared/core/project/loader/loader_parts/config_inspector.py +137 -0
- app/shared/core/project/loader/loader_parts/embedded_constraints.py +224 -0
- app/shared/core/project/loader/loader_parts/file_loaders.py +58 -0
- app/shared/core/project/loader/loader_parts/inspection_ids.py +85 -0
- app/shared/core/project/loader/loader_parts/inspector_helpers.py +312 -0
- app/shared/core/project/loader/loader_parts/inspector_id_checks.py +274 -0
- app/shared/core/project/loader/loader_parts/inspector_reference_checks.py +690 -0
- app/shared/core/project/loader/loader_parts/inspector_uniqueness_checks.py +286 -0
- app/shared/core/project/loader/loader_parts/loading_error_messages.py +298 -0
- app/shared/core/project/loader/loader_parts/main.py +432 -0
- app/shared/core/project/loader/loader_parts/path_validation.py +117 -0
- app/shared/core/project/loader/loader_parts/runtime.py +125 -0
- app/shared/core/project/loader/types.py +246 -0
- app/shared/core/project/manifest/coverage.py +391 -0
- app/shared/core/project/manifest/reader.py +260 -0
- app/shared/core/project/manifest/types.py +91 -0
- app/shared/core/project/manifest/types_parts/__init__.py +60 -0
- app/shared/core/project/manifest/types_parts/constants.py +40 -0
- app/shared/core/project/manifest/types_parts/data_source.py +68 -0
- app/shared/core/project/manifest/types_parts/info.py +59 -0
- app/shared/core/project/manifest/types_parts/manifest.py +294 -0
- app/shared/core/project/manifest/types_parts/refs.py +153 -0
- app/shared/core/project/manifest/types_parts/settings.py +92 -0
- app/shared/core/project/manifest/types_parts/settings_file_processing.py +64 -0
- app/shared/core/project/manifest/types_parts/settings_script_security.py +78 -0
- app/shared/core/project/manifest/types_parts/settings_validation.py +83 -0
- app/shared/core/project/manifest/types_parts/template.py +66 -0
- app/shared/core/project/manifest/writer.py +262 -0
- app/shared/core/project/manual_data/__init__.py +27 -0
- app/shared/core/project/manual_data/types.py +75 -0
- app/shared/core/project/regex/reader.py +197 -0
- app/shared/core/project/regex/types.py +405 -0
- app/shared/core/project/regex/writer.py +123 -0
- app/shared/core/project/schema/reader.py +170 -0
- app/shared/core/project/schema/types.py +47 -0
- app/shared/core/project/schema/types_parts/__init__.py +36 -0
- app/shared/core/project/schema/types_parts/column.py +174 -0
- app/shared/core/project/schema/types_parts/column_utils.py +72 -0
- app/shared/core/project/schema/types_parts/constraint.py +165 -0
- app/shared/core/project/schema/types_parts/schema_id.py +66 -0
- app/shared/core/project/schema/types_parts/source.py +255 -0
- app/shared/core/project/schema/types_parts/source_options.py +347 -0
- app/shared/core/project/schema/types_parts/table.py +230 -0
- app/shared/core/project/schema/writer.py +139 -0
- app/shared/core/project/schema_ref_check.py +95 -0
- app/shared/core/project/template/__init__.py +27 -0
- app/shared/core/project/template/expander.py +263 -0
- app/shared/core/project/template/reader.py +120 -0
- app/shared/core/project/template/types.py +114 -0
- app/shared/core/project/transform/reader.py +76 -0
- app/shared/core/project/transform/types.py +116 -0
- app/shared/core/project/transform/writer.py +84 -0
- app/shared/core/pydantic_messages.py +59 -0
- app/shared/core/reporter/__init__.py +46 -0
- app/shared/core/reporter/reporter.py +220 -0
- app/shared/core/reporter/reporters/__init__.py +65 -0
- app/shared/core/reporter/reporters/base.py +188 -0
- app/shared/core/reporter/reporters/dingtalk_app_reporter.py +274 -0
- app/shared/core/reporter/reporters/email_reporter.py +271 -0
- app/shared/core/reporter/reporters/feishu_app_reporter.py +467 -0
- app/shared/core/reporter/reporters/local_file_reporter.py +208 -0
- app/shared/core/reporter/reporters/wecom_app_reporter.py +268 -0
- app/shared/core/utils/__init__.py +18 -0
- app/shared/core/utils/path_utils.py +60 -0
- app/shared/core/utils/regex_extract.py +113 -0
- app/shared/domain/__init__.py +114 -0
- app/shared/domain/constraints/__init__.py +76 -0
- app/shared/domain/constraints/allowed_values.py +211 -0
- app/shared/domain/constraints/base.py +141 -0
- app/shared/domain/constraints/charset.py +337 -0
- app/shared/domain/constraints/composite.py +174 -0
- app/shared/domain/constraints/condition_registry.py +130 -0
- app/shared/domain/constraints/conditional.py +629 -0
- app/shared/domain/constraints/date_logic.py +731 -0
- app/shared/domain/constraints/foreign_key.py +261 -0
- app/shared/domain/constraints/key_normalization.py +56 -0
- app/shared/domain/constraints/not_null.py +185 -0
- app/shared/domain/constraints/range.py +360 -0
- app/shared/domain/constraints/regex.py +218 -0
- app/shared/domain/constraints/scripted.py +276 -0
- app/shared/domain/constraints/unique.py +233 -0
- app/shared/domain/data_engine.py +384 -0
- app/shared/domain/data_types.py +113 -0
- app/shared/domain/data_types_parts/__init__.py +67 -0
- app/shared/domain/data_types_parts/base.py +204 -0
- app/shared/domain/data_types_parts/composite.py +250 -0
- app/shared/domain/data_types_parts/expression.py +202 -0
- app/shared/domain/data_types_parts/extracted.py +106 -0
- app/shared/domain/data_types_parts/json_types.py +234 -0
- app/shared/domain/data_types_parts/scalars.py +719 -0
- app/shared/domain/data_types_parts/sequence.py +129 -0
- app/shared/domain/dataset_schema.py +48 -0
- app/shared/domain/eval_sandbox.py +63 -0
- app/shared/domain/expression_system.py +366 -0
- app/shared/domain/regex_flags.py +53 -0
- app/shared/domain/schema/builder.py +223 -0
- app/shared/domain/schema/models.py +339 -0
- app/shared/domain/transforms/__init__.py +28 -0
- app/shared/domain/transforms/aggregate.py +141 -0
- app/shared/domain/transforms/base.py +171 -0
- app/shared/domain/transforms/cast_type.py +120 -0
- app/shared/domain/transforms/concat.py +103 -0
- app/shared/domain/transforms/conditional_assign.py +114 -0
- app/shared/domain/transforms/date_format.py +81 -0
- app/shared/domain/transforms/digits.py +77 -0
- app/shared/domain/transforms/drop_duplicates.py +94 -0
- app/shared/domain/transforms/fill_na.py +94 -0
- app/shared/domain/transforms/filter_rows.py +97 -0
- app/shared/domain/transforms/lookup.py +78 -0
- app/shared/domain/transforms/lower_case.py +70 -0
- app/shared/domain/transforms/map_value.py +90 -0
- app/shared/domain/transforms/math_expr.py +112 -0
- app/shared/domain/transforms/modulo.py +82 -0
- app/shared/domain/transforms/regex_extract.py +103 -0
- app/shared/domain/transforms/registry.py +95 -0
- app/shared/domain/transforms/replace.py +88 -0
- app/shared/domain/transforms/sort_rows.py +94 -0
- app/shared/domain/transforms/string_split.py +84 -0
- app/shared/domain/transforms/strip.py +73 -0
- app/shared/domain/transforms/substring.py +92 -0
- app/shared/domain/transforms/upper_case.py +70 -0
- app/shared/domain/transforms/weighted_sum.py +104 -0
- app/shared/domain/validation_constraints.py +69 -0
- app/shared/services/__init__.py +62 -0
- app/shared/services/ai/__init__.py +48 -0
- app/shared/services/ai/agent/__init__.py +40 -0
- app/shared/services/ai/agent/chat_tools/__init__.py +47 -0
- app/shared/services/ai/agent/chat_tools/apply_actions.py +592 -0
- app/shared/services/ai/agent/chat_tools/ask_user.py +237 -0
- app/shared/services/ai/agent/chat_tools/read_canvas.py +140 -0
- app/shared/services/ai/agent/chat_tools/read_project.py +160 -0
- app/shared/services/ai/agent/chat_tools/read_table.py +301 -0
- app/shared/services/ai/agent/chat_tools/schemas.py +105 -0
- app/shared/services/ai/agent/chat_tools/validate_table.py +131 -0
- app/shared/services/ai/agent/executor.py +554 -0
- app/shared/services/ai/agent/memory.py +256 -0
- app/shared/services/ai/agent/planner.py +282 -0
- app/shared/services/ai/agent/tool_registry.py +296 -0
- app/shared/services/ai/agent/tools/__init__.py +32 -0
- app/shared/services/ai/agent/tools/config_generate.py +133 -0
- app/shared/services/ai/agent/tools/config_refine.py +109 -0
- app/shared/services/ai/agent/tools/config_validate.py +403 -0
- app/shared/services/ai/agent/tools/merge_results.py +136 -0
- app/shared/services/ai/agent/tools/plan_chunks.py +79 -0
- app/shared/services/ai/agent/tools/script_parse.py +359 -0
- app/shared/services/ai/agent/types.py +138 -0
- app/shared/services/ai/chat_agent_runner.py +596 -0
- app/shared/services/ai/chat_orchestrator.py +725 -0
- app/shared/services/ai/failure_messages.py +65 -0
- app/shared/services/ai/job_storage.py +274 -0
- app/shared/services/ai/migrate_service.py +550 -0
- app/shared/services/ai/streaming/__init__.py +65 -0
- app/shared/services/ai/streaming/event_journal.py +197 -0
- app/shared/services/ai/streaming/orchestrator.py +262 -0
- app/shared/services/ai/streaming/pending_interaction_store.py +227 -0
- app/shared/services/ai/streaming/sse_response.py +148 -0
- app/shared/services/ai/streaming/types.py +73 -0
- app/shared/services/ai/types.py +213 -0
- app/shared/services/ai/utils.py +322 -0
- app/shared/services/diff/config_diff.py +320 -0
- app/shared/services/hardware.py +237 -0
- app/shared/services/llm/__init__.py +62 -0
- app/shared/services/llm/actions/__init__.py +42 -0
- app/shared/services/llm/actions/_canvas_validator.py +147 -0
- app/shared/services/llm/actions/_constraint_validator.py +401 -0
- app/shared/services/llm/actions/_regex_validator.py +85 -0
- app/shared/services/llm/actions/_schema_validator.py +82 -0
- app/shared/services/llm/actions/_settings_validator.py +76 -0
- app/shared/services/llm/actions/_transform_validator.py +78 -0
- app/shared/services/llm/actions/action_handlers.py +400 -0
- app/shared/services/llm/actions/action_parser.py +63 -0
- app/shared/services/llm/actions/action_processor.py +468 -0
- app/shared/services/llm/actions/action_validator.py +311 -0
- app/shared/services/llm/actions/diff_compute.py +195 -0
- app/shared/services/llm/actions/regex_handlers.py +284 -0
- app/shared/services/llm/actions/registry.py +336 -0
- app/shared/services/llm/actions/schema_handlers.py +335 -0
- app/shared/services/llm/actions/settings_handlers.py +180 -0
- app/shared/services/llm/actions/specs.py +259 -0
- app/shared/services/llm/actions/transform_handlers.py +279 -0
- app/shared/services/llm/actions/validation_types.py +147 -0
- app/shared/services/llm/cache/__init__.py +18 -0
- app/shared/services/llm/cache/response_cache.py +80 -0
- app/shared/services/llm/chat/__init__.py +35 -0
- app/shared/services/llm/chat/chat_system_prompt.py +561 -0
- app/shared/services/llm/chat/response_parser.py +296 -0
- app/shared/services/llm/config/__init__.py +45 -0
- app/shared/services/llm/config/crypto.py +135 -0
- app/shared/services/llm/config/loader.py +258 -0
- app/shared/services/llm/config/models.py +202 -0
- app/shared/services/llm/config/presets.py +128 -0
- app/shared/services/llm/config_generator.py +163 -0
- app/shared/services/llm/constraints/__init__.py +41 -0
- app/shared/services/llm/constraints/constraint_builder.py +177 -0
- app/shared/services/llm/constraints/constraint_deletion.py +84 -0
- app/shared/services/llm/constraints/constraint_id.py +174 -0
- app/shared/services/llm/constraints/frontend_instructions.py +333 -0
- app/shared/services/llm/constraints/inline_batch.py +279 -0
- app/shared/services/llm/discovery/__init__.py +35 -0
- app/shared/services/llm/discovery/scanner.py +181 -0
- app/shared/services/llm/generation/__init__.py +42 -0
- app/shared/services/llm/generation/agent_wiring.py +156 -0
- app/shared/services/llm/generation/config_builder.py +386 -0
- app/shared/services/llm/generation/errors.py +36 -0
- app/shared/services/llm/generation/existing_config.py +99 -0
- app/shared/services/llm/generation/profiler.py +279 -0
- app/shared/services/llm/generation/prompt_builder.py +199 -0
- app/shared/services/llm/generation/response_parser.py +144 -0
- app/shared/services/llm/generation/service.py +622 -0
- app/shared/services/llm/models.py +104 -0
- app/shared/services/llm/providers/__init__.py +48 -0
- app/shared/services/llm/providers/base.py +310 -0
- app/shared/services/llm/providers/cached_provider.py +90 -0
- app/shared/services/llm/providers/ollama.py +498 -0
- app/shared/services/llm/providers/openai.py +334 -0
- app/shared/services/llm/providers/registry.py +108 -0
- app/shared/services/llm/schema_resolver.py +141 -0
- app/shared/services/llm/suggestion_utils.py +219 -0
- app/shared/services/llm/validate_executor.py +163 -0
- app/shared/services/llm/yaml_io.py +406 -0
- app/shared/services/preview/__init__.py +18 -0
- app/shared/services/preview/loader.py +145 -0
- app/shared/services/preview/path_validation.py +138 -0
- app/shared/services/project_loader.py +57 -0
- app/shared/services/schema_inference.py +260 -0
- app/shared/services/schema_runtime_builder.py +120 -0
- app/shared/services/validation/__init__.py +52 -0
- app/shared/services/validation/chunked_loader.py +509 -0
- app/shared/services/validation/dag/__init__.py +35 -0
- app/shared/services/validation/dag/builder.py +141 -0
- app/shared/services/validation/dag/executor.py +225 -0
- app/shared/services/validation/dag/sorter.py +77 -0
- app/shared/services/validation/data_loader.py +231 -0
- app/shared/services/validation/engine.py +446 -0
- app/shared/services/validation/executor.py +1030 -0
- app/shared/services/validation/extractors.py +266 -0
- app/shared/services/validation/history.py +209 -0
- app/shared/services/validation/json_payload.py +136 -0
- app/shared/services/validation/loader.py +152 -0
- app/shared/services/validation/memory_monitor.py +179 -0
- app/shared/services/validation/postprocess.py +208 -0
- app/shared/services/validation/progress.py +64 -0
- app/shared/services/validation/report_export.py +222 -0
- app/shared/services/validation/resolver.py +180 -0
- app/shared/services/validation/service.py +418 -0
- app/shared/services/validation/types.py +238 -0
- app/shared/services/validation/validators/__init__.py +36 -0
- app/shared/services/validation/validators/adapter.py +182 -0
- app/shared/services/validation/validators/base.py +332 -0
- app/shared/services/validation/validators/composite.py +233 -0
- app/shared/services/validation/validators/date_logic.py +276 -0
- app/start_server.py +133 -0
- precis_cli-0.1.3.dist-info/METADATA +180 -0
- precis_cli-0.1.3.dist-info/RECORD +442 -0
- precis_cli-0.1.3.dist-info/WHEEL +5 -0
- precis_cli-0.1.3.dist-info/entry_points.txt +4 -0
- precis_cli-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
"""
|
|
17
|
+
@fileoverview CLI 交互式菜单模块
|
|
18
|
+
|
|
19
|
+
功能概述:
|
|
20
|
+
- 提供支持方向键导航的终端菜单选择功能
|
|
21
|
+
- 支持菜单项高亮、Enter 确认、ESC/0 取消
|
|
22
|
+
- 支持带状态信息的菜单显示
|
|
23
|
+
|
|
24
|
+
架构设计:
|
|
25
|
+
- MenuItem 封装菜单项的键、标签和描述
|
|
26
|
+
- InteractiveMenu 使用 readchar 捕获键盘输入
|
|
27
|
+
- 通过 ANSI 控制码实现菜单的动态渲染与清除
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
import readchar
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MenuItem:
|
|
36
|
+
"""菜单项"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, key: str, label: str, description: str = ""):
|
|
39
|
+
self.key = key # 用于返回的标识
|
|
40
|
+
self.label = label # 显示的文本
|
|
41
|
+
self.description = description # 描述(可选)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InteractiveMenu:
|
|
45
|
+
"""交互式菜单
|
|
46
|
+
|
|
47
|
+
支持方向键导航、Enter 确认、ESC/0 取消。
|
|
48
|
+
|
|
49
|
+
使用示例:
|
|
50
|
+
menu = InteractiveMenu("请选择操作:")
|
|
51
|
+
menu.add_item("chat", "💬 chat", "进入交互式对话模式")
|
|
52
|
+
menu.add_item("status", "📊 status", "查看 AI 配置状态")
|
|
53
|
+
menu.add_item("switch", "🔄 switch", "切换 AI Provider")
|
|
54
|
+
menu.add_item("help", "❓ help", "显示帮助信息")
|
|
55
|
+
|
|
56
|
+
result = menu.show()
|
|
57
|
+
if result:
|
|
58
|
+
print(f"选择了: {result}")
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# ANSI 控制码
|
|
62
|
+
CLEAR_LINE = "\r\033[K" # 清除当前行(回到行首并清除到行尾)
|
|
63
|
+
MOVE_UP = "\033[{n}A" # 向上移动 n 行
|
|
64
|
+
HIDE_CURSOR = "\033[?25l" # 隐藏光标
|
|
65
|
+
SHOW_CURSOR = "\033[?25h" # 显示光标
|
|
66
|
+
|
|
67
|
+
# 颜色代码
|
|
68
|
+
COLOR_RESET = "\033[0m"
|
|
69
|
+
COLOR_HIGHLIGHT = "\033[7m" # 反色(高亮)
|
|
70
|
+
COLOR_GREEN = "\033[32m"
|
|
71
|
+
COLOR_YELLOW = "\033[33m"
|
|
72
|
+
COLOR_GRAY = "\033[90m"
|
|
73
|
+
|
|
74
|
+
def __init__(self, title: str = "", show_cancel: bool = True):
|
|
75
|
+
self.title = title
|
|
76
|
+
self.items: list[MenuItem] = []
|
|
77
|
+
self.show_cancel = show_cancel
|
|
78
|
+
self.selected_index = 0
|
|
79
|
+
|
|
80
|
+
def add_item(self, key: str, label: str, description: str = "") -> "InteractiveMenu":
|
|
81
|
+
"""添加菜单项"""
|
|
82
|
+
self.items.append(MenuItem(key, label, description))
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def _get_menu_line_count(self) -> int:
|
|
86
|
+
"""计算菜单总行数(用于清除)"""
|
|
87
|
+
count = 0
|
|
88
|
+
if self.title:
|
|
89
|
+
count += 1
|
|
90
|
+
count += len(self.items)
|
|
91
|
+
if self.show_cancel:
|
|
92
|
+
count += 1
|
|
93
|
+
count += 1 # 提示行(虽然是 end="" 但清除时需要处理)
|
|
94
|
+
return count
|
|
95
|
+
|
|
96
|
+
def _clear_menu(self) -> None:
|
|
97
|
+
"""清除已渲染的菜单"""
|
|
98
|
+
line_count = self._get_menu_line_count()
|
|
99
|
+
sys.stdout.write(self.MOVE_UP.format(n=line_count - 1))
|
|
100
|
+
for i in range(line_count):
|
|
101
|
+
sys.stdout.write(self.CLEAR_LINE)
|
|
102
|
+
if i < line_count - 1:
|
|
103
|
+
sys.stdout.write("\n")
|
|
104
|
+
# 清除后将光标移回第一行起始位置,防止后续渲染时产生空白行堆积
|
|
105
|
+
sys.stdout.write(self.MOVE_UP.format(n=line_count - 1))
|
|
106
|
+
sys.stdout.flush()
|
|
107
|
+
|
|
108
|
+
def _render_menu(self) -> None:
|
|
109
|
+
"""渲染菜单"""
|
|
110
|
+
# 标题
|
|
111
|
+
if self.title:
|
|
112
|
+
print(f"{self.COLOR_GREEN}{self.title}{self.COLOR_RESET}")
|
|
113
|
+
|
|
114
|
+
# 菜单项
|
|
115
|
+
for i, item in enumerate(self.items):
|
|
116
|
+
num = i + 1
|
|
117
|
+
if i == self.selected_index:
|
|
118
|
+
# 高亮当前选中项(反色 + 数字)
|
|
119
|
+
prefix = f"{self.COLOR_HIGHLIGHT} {num}."
|
|
120
|
+
suffix = f"{self.COLOR_RESET}"
|
|
121
|
+
else:
|
|
122
|
+
prefix = f" {num}."
|
|
123
|
+
suffix = ""
|
|
124
|
+
|
|
125
|
+
desc = f" {self.COLOR_GRAY}{item.description}{self.COLOR_RESET}" if item.description else ""
|
|
126
|
+
print(f"{prefix} {item.label}{suffix}{desc}")
|
|
127
|
+
|
|
128
|
+
# 取消选项
|
|
129
|
+
if self.show_cancel:
|
|
130
|
+
cancel_idx = len(self.items) + 1
|
|
131
|
+
if self.selected_index == len(self.items):
|
|
132
|
+
prefix = f"{self.COLOR_HIGHLIGHT} {cancel_idx}."
|
|
133
|
+
suffix = f"{self.COLOR_RESET}"
|
|
134
|
+
else:
|
|
135
|
+
prefix = f" {cancel_idx}."
|
|
136
|
+
suffix = ""
|
|
137
|
+
print(f"{prefix} cancel - 返回{suffix}")
|
|
138
|
+
|
|
139
|
+
# 提示(不换行,保持在同一行)
|
|
140
|
+
print(f"{self.COLOR_GRAY}↑/↓ 选择, Enter 确认, ESC/0 取消{self.COLOR_RESET}", end="", flush=True)
|
|
141
|
+
|
|
142
|
+
def show(self) -> str | None:
|
|
143
|
+
"""
|
|
144
|
+
显示菜单并等待用户选择
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
选中项的 key,如果取消则返回 None
|
|
148
|
+
"""
|
|
149
|
+
if not self.items:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
# 隐藏光标
|
|
153
|
+
sys.stdout.write(self.HIDE_CURSOR)
|
|
154
|
+
sys.stdout.flush()
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# 初始渲染
|
|
158
|
+
self._render_menu()
|
|
159
|
+
|
|
160
|
+
while True:
|
|
161
|
+
# 读取单个字符
|
|
162
|
+
key = readchar.readkey()
|
|
163
|
+
|
|
164
|
+
# ESC 或 0 取消
|
|
165
|
+
if key in (readchar.key.ESC, "0"):
|
|
166
|
+
# 清除菜单
|
|
167
|
+
self._clear_menu()
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
# Enter 确认
|
|
171
|
+
if key in (readchar.key.ENTER, readchar.key.CR):
|
|
172
|
+
self._clear_menu()
|
|
173
|
+
if self.selected_index < len(self.items):
|
|
174
|
+
return self.items[self.selected_index].key
|
|
175
|
+
else:
|
|
176
|
+
return None # 选择了 cancel
|
|
177
|
+
|
|
178
|
+
# 上箭头
|
|
179
|
+
if key == readchar.key.UP:
|
|
180
|
+
self.selected_index = (self.selected_index - 1) % (len(self.items) + (1 if self.show_cancel else 0))
|
|
181
|
+
self._clear_menu()
|
|
182
|
+
self._render_menu()
|
|
183
|
+
|
|
184
|
+
# 下箭头
|
|
185
|
+
elif key == readchar.key.DOWN:
|
|
186
|
+
self.selected_index = (self.selected_index + 1) % (len(self.items) + (1 if self.show_cancel else 0))
|
|
187
|
+
self._clear_menu()
|
|
188
|
+
self._render_menu()
|
|
189
|
+
|
|
190
|
+
# 数字键直接选择
|
|
191
|
+
elif key.isdigit():
|
|
192
|
+
num = int(key)
|
|
193
|
+
if num == 0:
|
|
194
|
+
self._clear_menu()
|
|
195
|
+
return None
|
|
196
|
+
elif 1 <= num <= len(self.items):
|
|
197
|
+
self._clear_menu()
|
|
198
|
+
return self.items[num - 1].key
|
|
199
|
+
|
|
200
|
+
finally:
|
|
201
|
+
# 确保恢复光标
|
|
202
|
+
sys.stdout.write(self.SHOW_CURSOR)
|
|
203
|
+
sys.stdout.flush()
|
|
204
|
+
|
|
205
|
+
def show_with_status(self, status_lines: list[str]) -> str | None:
|
|
206
|
+
"""
|
|
207
|
+
显示带状态信息的菜单
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
status_lines: 状态信息行列表(显示在标题下方)
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
选中项的 key,如果取消则返回 None
|
|
214
|
+
"""
|
|
215
|
+
if not self.items:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
# 隐藏光标
|
|
219
|
+
sys.stdout.write(self.HIDE_CURSOR)
|
|
220
|
+
sys.stdout.flush()
|
|
221
|
+
|
|
222
|
+
# 保存状态行数(用于清除)
|
|
223
|
+
self._status_line_count = len(status_lines)
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
# 初始渲染
|
|
227
|
+
self._render_menu_with_status(status_lines)
|
|
228
|
+
|
|
229
|
+
while True:
|
|
230
|
+
key = readchar.readkey()
|
|
231
|
+
|
|
232
|
+
if key in (readchar.key.ESC, "0"):
|
|
233
|
+
self._clear_menu_with_status(status_lines)
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
if key in (readchar.key.ENTER, readchar.key.CR):
|
|
237
|
+
self._clear_menu_with_status(status_lines)
|
|
238
|
+
if self.selected_index < len(self.items):
|
|
239
|
+
return self.items[self.selected_index].key
|
|
240
|
+
else:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
if key == readchar.key.UP:
|
|
244
|
+
self.selected_index = (self.selected_index - 1) % (len(self.items) + (1 if self.show_cancel else 0))
|
|
245
|
+
self._clear_menu_with_status(status_lines)
|
|
246
|
+
self._render_menu_with_status(status_lines)
|
|
247
|
+
|
|
248
|
+
elif key == readchar.key.DOWN:
|
|
249
|
+
self.selected_index = (self.selected_index + 1) % (len(self.items) + (1 if self.show_cancel else 0))
|
|
250
|
+
self._clear_menu_with_status(status_lines)
|
|
251
|
+
self._render_menu_with_status(status_lines)
|
|
252
|
+
|
|
253
|
+
elif key.isdigit():
|
|
254
|
+
num = int(key)
|
|
255
|
+
if num == 0:
|
|
256
|
+
self._clear_menu_with_status(status_lines)
|
|
257
|
+
return None
|
|
258
|
+
elif 1 <= num <= len(self.items):
|
|
259
|
+
self._clear_menu_with_status(status_lines)
|
|
260
|
+
return self.items[num - 1].key
|
|
261
|
+
|
|
262
|
+
finally:
|
|
263
|
+
sys.stdout.write(self.SHOW_CURSOR)
|
|
264
|
+
sys.stdout.flush()
|
|
265
|
+
|
|
266
|
+
def _get_menu_with_status_line_count(self, status_lines: list[str]) -> int:
|
|
267
|
+
"""计算带状态信息的菜单总行数(用于清除)"""
|
|
268
|
+
count = 0
|
|
269
|
+
if self.title:
|
|
270
|
+
count += 1
|
|
271
|
+
count += len(status_lines)
|
|
272
|
+
count += len(self.items)
|
|
273
|
+
if self.show_cancel:
|
|
274
|
+
count += 1
|
|
275
|
+
count += 1
|
|
276
|
+
return count
|
|
277
|
+
|
|
278
|
+
def _render_menu_with_status(self, status_lines: list[str]) -> None:
|
|
279
|
+
"""渲染带状态信息的菜单"""
|
|
280
|
+
# 标题
|
|
281
|
+
if self.title:
|
|
282
|
+
print(f"{self.COLOR_GREEN}{self.title}{self.COLOR_RESET}")
|
|
283
|
+
|
|
284
|
+
# 状态信息
|
|
285
|
+
for line in status_lines:
|
|
286
|
+
print(line)
|
|
287
|
+
|
|
288
|
+
# 菜单项
|
|
289
|
+
for i, item in enumerate(self.items):
|
|
290
|
+
num = i + 1
|
|
291
|
+
if i == self.selected_index:
|
|
292
|
+
prefix = f"{self.COLOR_HIGHLIGHT} {num}."
|
|
293
|
+
suffix = f"{self.COLOR_RESET}"
|
|
294
|
+
else:
|
|
295
|
+
prefix = f" {num}."
|
|
296
|
+
suffix = ""
|
|
297
|
+
|
|
298
|
+
desc = f" {self.COLOR_GRAY}{item.description}{self.COLOR_RESET}" if item.description else ""
|
|
299
|
+
print(f"{prefix} {item.label}{suffix}{desc}")
|
|
300
|
+
|
|
301
|
+
# 取消选项
|
|
302
|
+
if self.show_cancel:
|
|
303
|
+
cancel_idx = len(self.items) + 1
|
|
304
|
+
if self.selected_index == len(self.items):
|
|
305
|
+
prefix = f"{self.COLOR_HIGHLIGHT} {cancel_idx}."
|
|
306
|
+
suffix = f"{self.COLOR_RESET}"
|
|
307
|
+
else:
|
|
308
|
+
prefix = f" {cancel_idx}."
|
|
309
|
+
suffix = ""
|
|
310
|
+
print(f"{prefix} cancel - 返回{suffix}")
|
|
311
|
+
|
|
312
|
+
# 提示(不换行)
|
|
313
|
+
print(f"{self.COLOR_GRAY}↑/↓ 选择, Enter 确认, ESC/0 取消{self.COLOR_RESET}", end="", flush=True)
|
|
314
|
+
|
|
315
|
+
def _clear_menu_with_status(self, status_lines: list[str]) -> None:
|
|
316
|
+
"""清除带状态信息的菜单"""
|
|
317
|
+
line_count = self._get_menu_with_status_line_count(status_lines)
|
|
318
|
+
sys.stdout.write(self.MOVE_UP.format(n=line_count - 1))
|
|
319
|
+
for i in range(line_count):
|
|
320
|
+
sys.stdout.write(self.CLEAR_LINE)
|
|
321
|
+
if i < line_count - 1:
|
|
322
|
+
sys.stdout.write("\n")
|
|
323
|
+
# 清除后将光标移回第一行起始位置,防止后续渲染时产生空白行堆积
|
|
324
|
+
sys.stdout.write(self.MOVE_UP.format(n=line_count - 1))
|
|
325
|
+
sys.stdout.flush()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def show_simple_menu(title: str, options: list[tuple[str, str, str]]) -> str | None:
|
|
329
|
+
"""
|
|
330
|
+
快速显示简单菜单
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
title: 菜单标题
|
|
334
|
+
options: [(key, label, description), ...]
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
选中项的 key,如果取消则返回 None
|
|
338
|
+
|
|
339
|
+
示例:
|
|
340
|
+
result = show_simple_menu("请选择:", [
|
|
341
|
+
("a", "选项 A", "这是选项 A"),
|
|
342
|
+
("b", "选项 B", "这是选项 B"),
|
|
343
|
+
])
|
|
344
|
+
"""
|
|
345
|
+
menu = InteractiveMenu(title)
|
|
346
|
+
for key, label, desc in options:
|
|
347
|
+
menu.add_item(key, label, desc)
|
|
348
|
+
return menu.show()
|
app/cli/shell/main.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 Precis Team
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
# backend/app/cli/shell/main.py
|
|
17
|
+
"""
|
|
18
|
+
@fileoverview CLI Shell 主循环模块
|
|
19
|
+
|
|
20
|
+
功能概述:
|
|
21
|
+
- 提供交互式命令行 Shell 主循环
|
|
22
|
+
- 管理命令注册、解析和执行
|
|
23
|
+
- 支持初始参数直接执行与持续交互模式
|
|
24
|
+
|
|
25
|
+
架构设计:
|
|
26
|
+
- CLIShell 类管理命令注册表和项目上下文
|
|
27
|
+
- CommandParser 解析输入,CommandExecutor 执行命令
|
|
28
|
+
- 支持全局快捷退出命令(exit!, quit!, qq)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import re
|
|
33
|
+
import sys
|
|
34
|
+
|
|
35
|
+
from app.cli.shell.commands import (
|
|
36
|
+
AICommand,
|
|
37
|
+
ConfigCommand,
|
|
38
|
+
ExitCommand,
|
|
39
|
+
HelpCommand,
|
|
40
|
+
InferSchemaCommand,
|
|
41
|
+
LsCommand,
|
|
42
|
+
OpenCommand,
|
|
43
|
+
ProjectCommand,
|
|
44
|
+
ProviderCommand,
|
|
45
|
+
PwdCommand,
|
|
46
|
+
ValidateCommand,
|
|
47
|
+
)
|
|
48
|
+
from app.cli.shell.commands.base import ProjectContext
|
|
49
|
+
from app.cli.shell.exceptions import CLIError
|
|
50
|
+
from app.cli.shell.formatter import Colors, Formatter
|
|
51
|
+
from app.cli.shell.parser import CommandExecutor, CommandParser, CommandRegistry
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _setup_logging() -> None:
|
|
55
|
+
"""配置日志级别,CLI 模式下只显示警告及以上级别。"""
|
|
56
|
+
import os
|
|
57
|
+
|
|
58
|
+
debug_mode = os.environ.get("PRECIS_DEBUG", "").lower() in ("1", "true", "yes")
|
|
59
|
+
level = logging.DEBUG if debug_mode else logging.WARNING
|
|
60
|
+
logging.basicConfig(level=level, format="%(levelname)s: %(message)s", handlers=[logging.StreamHandler(sys.stderr)])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _setup_encoding() -> None:
|
|
64
|
+
"""确保 stdout/stderr 使用 UTF-8 编码(委托 shared.core.encoding 公共助手)。"""
|
|
65
|
+
from app.shared.core.encoding import setup_utf8_console
|
|
66
|
+
|
|
67
|
+
setup_utf8_console()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# 匹配 ANSI 颜色/样式转义序列(如 \x1b[36m、\x1b[0m)
|
|
71
|
+
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_version() -> str:
|
|
75
|
+
"""获取 CLI 版本号(委托 shared.core.app_version,供 --version 旗标)。"""
|
|
76
|
+
from app.shared.core.app_version import get_app_version
|
|
77
|
+
|
|
78
|
+
return get_app_version()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _wrap_ansi_for_readline(prompt: str) -> str:
|
|
82
|
+
"""将 ANSI 转义序列用 readline 非打印分隔符 \\x01/\\x02 包裹。
|
|
83
|
+
|
|
84
|
+
readline/pyreadline3 计算 prompt 可见宽度时会把 ANSI 颜色码误算为可见字符,
|
|
85
|
+
导致输入行光标错位。用 \\x01...\\x02 包裹后 readline 会跳过这些序列的宽度计算。
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
prompt: 含 ANSI 转义的原始 prompt 字符串
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
包裹后的 prompt,颜色显示不变但 readline 宽度计算正确
|
|
92
|
+
"""
|
|
93
|
+
return _ANSI_ESCAPE_RE.sub(lambda m: "\x01" + m.group(0) + "\x02", prompt)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CLIShell:
|
|
97
|
+
"""CLI Shell 主类。
|
|
98
|
+
|
|
99
|
+
管理命令注册和交互式主循环。
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self) -> None:
|
|
103
|
+
self.registry = CommandRegistry()
|
|
104
|
+
self.context = ProjectContext()
|
|
105
|
+
self._setup_commands()
|
|
106
|
+
# Tab 补全是否已激活(交互模式下由 _setup_readline 设置)
|
|
107
|
+
self._readline_active: bool = False
|
|
108
|
+
|
|
109
|
+
def _setup_commands(self) -> None:
|
|
110
|
+
"""注册所有内置命令。"""
|
|
111
|
+
self.registry.register(HelpCommand(self.registry))
|
|
112
|
+
self.registry.register(OpenCommand())
|
|
113
|
+
self.registry.register(ProjectCommand())
|
|
114
|
+
self.registry.register(ValidateCommand())
|
|
115
|
+
self.registry.register(InferSchemaCommand())
|
|
116
|
+
self.registry.register(ConfigCommand())
|
|
117
|
+
self.registry.register(ProviderCommand())
|
|
118
|
+
self.registry.register(AICommand())
|
|
119
|
+
self.registry.register(PwdCommand())
|
|
120
|
+
self.registry.register(LsCommand())
|
|
121
|
+
self.registry.register(ExitCommand())
|
|
122
|
+
|
|
123
|
+
def run(self, initial_args: list | None = None) -> int:
|
|
124
|
+
"""运行 CLI Shell。
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
initial_args: 初始参数,如果提供则执行后退出
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
退出码
|
|
131
|
+
"""
|
|
132
|
+
parser = CommandParser(self.registry)
|
|
133
|
+
executor = CommandExecutor(parser, self.context)
|
|
134
|
+
|
|
135
|
+
if initial_args:
|
|
136
|
+
input_line = " ".join(initial_args)
|
|
137
|
+
# 全局快捷退出命令
|
|
138
|
+
if input_line.lower() in ("exit!", "quit!", "qq"):
|
|
139
|
+
print(Formatter.success("再见!"))
|
|
140
|
+
sys.exit(0)
|
|
141
|
+
result = executor.execute_with_args(initial_args)
|
|
142
|
+
if result.message:
|
|
143
|
+
if result.success:
|
|
144
|
+
print(result.message)
|
|
145
|
+
else:
|
|
146
|
+
Formatter.print_error(result.message)
|
|
147
|
+
# 单发模式退出码契约:0 = 校验通过;1 = 校验完成但发现数据违规;
|
|
148
|
+
# 2 = 工具自身错误(参数错误、文件不存在、异常崩溃)。
|
|
149
|
+
# 命令显式携带 exit_code 时优先(如 validate 的清单不存在传 2),
|
|
150
|
+
# 其余按 success 推导(成功 0 / 失败 1)。这对 CI/CD 与 agent
|
|
151
|
+
# 自动化至关重要:仅凭退出码即可区分红绿与工具故障。
|
|
152
|
+
if result.exit_code is not None:
|
|
153
|
+
return result.exit_code
|
|
154
|
+
return 0 if result.success else 1
|
|
155
|
+
|
|
156
|
+
Formatter.print_welcome()
|
|
157
|
+
|
|
158
|
+
# 交互模式:尝试安装 Tab 补全(无 readline 后端时静默降级)
|
|
159
|
+
self._readline_active = self._setup_readline()
|
|
160
|
+
|
|
161
|
+
while True:
|
|
162
|
+
try:
|
|
163
|
+
prompt = self._get_prompt()
|
|
164
|
+
# 主循环使用普通 input 保持颜色,get_input_cached 只用于 AI chat
|
|
165
|
+
input_line = input(prompt).strip()
|
|
166
|
+
|
|
167
|
+
if not input_line:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# 全局快捷退出命令
|
|
171
|
+
if input_line.lower() in ("exit!", "quit!", "qq"):
|
|
172
|
+
print(Formatter.success("再见!"))
|
|
173
|
+
sys.exit(0)
|
|
174
|
+
|
|
175
|
+
exit_code = self._execute_line(executor, input_line)
|
|
176
|
+
|
|
177
|
+
if exit_code != 0:
|
|
178
|
+
# _execute_line 的非 0 仅是 should_exit 的循环哨兵(用户主动 exit),
|
|
179
|
+
# 属正常退出——进程退出码必须为 0,与 qq/EOF 路径一致。
|
|
180
|
+
# 原实现把哨兵透传为进程退出码,脚本化 `echo exit | precis` 会被误判为失败。
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
except KeyboardInterrupt:
|
|
184
|
+
print()
|
|
185
|
+
print(Formatter.info("使用 'exit' 或 'quit' 退出,'qq' 直接退出程序"))
|
|
186
|
+
except EOFError:
|
|
187
|
+
print()
|
|
188
|
+
print(Formatter.success("再见!"))
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
def _execute_line(self, executor: CommandExecutor, input_line: str) -> int:
|
|
192
|
+
"""执行一行输入。
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
executor: 命令执行器
|
|
196
|
+
input_line: 输入行
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
退出码,0 表示继续, 非0 表示退出
|
|
200
|
+
"""
|
|
201
|
+
result = executor.execute(input_line)
|
|
202
|
+
|
|
203
|
+
if result.message:
|
|
204
|
+
if result.success:
|
|
205
|
+
print(result.message)
|
|
206
|
+
else:
|
|
207
|
+
Formatter.print_error(result.message)
|
|
208
|
+
|
|
209
|
+
if result.should_exit:
|
|
210
|
+
return 1
|
|
211
|
+
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
def _get_prompt(self) -> str:
|
|
215
|
+
"""获取命令提示符。
|
|
216
|
+
|
|
217
|
+
当 Tab 补全激活时(readline 后端接管 input),将 ANSI 转义序列用
|
|
218
|
+
\\x01/\\x02 非打印分隔符包裹,避免 readline/pyreadline3 计算 prompt
|
|
219
|
+
宽度时把颜色码计入可见字符导致光标错位。
|
|
220
|
+
"""
|
|
221
|
+
if self.context.is_project_open:
|
|
222
|
+
# project_config 在 open 命令后可能尚未加载(OpenCommand 只设置 project_path),
|
|
223
|
+
# 此处做空值守卫避免 AttributeError 导致 Shell 崩溃退出。
|
|
224
|
+
config = self.context.project_config or {}
|
|
225
|
+
project_name = config.get("project", {}).get("name", "project")
|
|
226
|
+
prompt = Formatter.colorize(f"precis:{project_name}> ", Colors.CYAN)
|
|
227
|
+
else:
|
|
228
|
+
prompt = Formatter.colorize("precis> ", Colors.CYAN)
|
|
229
|
+
|
|
230
|
+
if self._readline_active:
|
|
231
|
+
return _wrap_ansi_for_readline(prompt)
|
|
232
|
+
return prompt
|
|
233
|
+
|
|
234
|
+
def _setup_readline(self) -> bool:
|
|
235
|
+
"""安装 Tab 补全到 readline 后端。
|
|
236
|
+
|
|
237
|
+
仅在交互模式下调用。Windows 用 pyreadline3,Unix 用 stdlib readline,
|
|
238
|
+
两者均不可用时返回 False(REPL 照常运行,仅无补全)。
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True 表示补全已激活;False 表示降级为无补全模式
|
|
242
|
+
"""
|
|
243
|
+
# 延迟导入:避免在单次执行模式(非交互)下加载补全模块的依赖
|
|
244
|
+
from app.cli.shell.completer import install_readline_completer
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
return install_readline_completer(self.registry)
|
|
248
|
+
except Exception:
|
|
249
|
+
# 任何意外异常都不应阻断 REPL 启动
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def main(args: list | None = None) -> int:
|
|
254
|
+
"""CLI 主入口函数。
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
args: 命令行参数列表,默认为 sys.argv[1:]
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
退出码
|
|
261
|
+
"""
|
|
262
|
+
_setup_encoding()
|
|
263
|
+
_setup_logging()
|
|
264
|
+
|
|
265
|
+
if args is None:
|
|
266
|
+
args = sys.argv[1:]
|
|
267
|
+
|
|
268
|
+
# 全局版本旗标:独立于命令体系,供外部集成(如 Kimi Code 插件)探测 CLI 可用性
|
|
269
|
+
if args and args[0] in ("--version", "-v"):
|
|
270
|
+
print(f"precis {_get_version()}")
|
|
271
|
+
return 0
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
shell = CLIShell()
|
|
275
|
+
return shell.run(args)
|
|
276
|
+
except CLIError as e:
|
|
277
|
+
Formatter.print_error(e.message)
|
|
278
|
+
return e.exit_code
|
|
279
|
+
except Exception as e:
|
|
280
|
+
# 未预期崩溃属工具自身错误,退出码 2(区别于"校验发现数据违规"的 1)
|
|
281
|
+
Formatter.print_error(f"未预期的错误: {e}")
|
|
282
|
+
return 2
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
sys.exit(main())
|