quickscale-cli 0.72.0__tar.gz → 0.73.0__tar.gz
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.
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/PKG-INFO +9 -9
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/pyproject.toml +7 -7
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/_version.py +1 -1
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/apply_command.py +4 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/deployment_commands.py +3 -5
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/module_commands.py +2 -2
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/module_config.py +291 -8
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/plan_command.py +6 -7
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/remove_command.py +9 -7
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/schema/config_schema.py +1 -1
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/utils/railway_utils.py +4 -21
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/README.md +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/__init__.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/__init__.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/development_commands.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/status_command.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/main.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/schema/__init__.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/schema/delta.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/schema/state_schema.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/utils/__init__.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/utils/dependency_utils.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/utils/docker_utils.py +0 -0
- {quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/utils/project_manager.py +0 -0
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: quickscale-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.73.0
|
|
4
4
|
Summary: Command-line interface for QuickScale Django project generator
|
|
5
|
-
Home-page: https://github.com/Experto-AI/quickscale
|
|
6
5
|
License: Apache-2.0
|
|
7
6
|
Keywords: django,cli,project-generator,saas
|
|
8
7
|
Author: Experto AI
|
|
9
8
|
Author-email: victor@experto.ai
|
|
10
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.12,<4.0
|
|
11
10
|
Classifier: Development Status :: 3 - Alpha
|
|
12
11
|
Classifier: Framework :: Django
|
|
13
|
-
Classifier: Framework :: Django ::
|
|
12
|
+
Classifier: Framework :: Django :: 6.0
|
|
14
13
|
Classifier: Intended Audience :: Developers
|
|
15
14
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Dist: click (>=8.3.1,<9.0.0)
|
|
20
|
+
Requires-Dist: quickscale-core (>=0.73.0,<0.74.0)
|
|
22
21
|
Project-URL: Documentation, https://github.com/Experto-AI/quickscale/tree/main/docs
|
|
22
|
+
Project-URL: Homepage, https://github.com/Experto-AI/quickscale
|
|
23
23
|
Project-URL: Repository, https://github.com/Experto-AI/quickscale
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "quickscale-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.73.0"
|
|
4
4
|
description = "Command-line interface for QuickScale Django project generator"
|
|
5
5
|
authors = ["Experto AI <victor@experto.ai>"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -14,18 +14,18 @@ classifiers = [
|
|
|
14
14
|
"Intended Audience :: Developers",
|
|
15
15
|
"License :: OSI Approved :: Apache Software License",
|
|
16
16
|
"Programming Language :: Python :: 3",
|
|
17
|
-
"Programming Language :: Python :: 3.10",
|
|
18
|
-
"Programming Language :: Python :: 3.11",
|
|
19
17
|
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
20
|
"Framework :: Django",
|
|
21
|
-
"Framework :: Django ::
|
|
21
|
+
"Framework :: Django :: 6.0",
|
|
22
22
|
]
|
|
23
23
|
packages = [{include = "quickscale_cli", from = "src"}]
|
|
24
24
|
|
|
25
25
|
[tool.poetry.dependencies]
|
|
26
|
-
python = "^3.
|
|
27
|
-
click = "^8.3.
|
|
28
|
-
quickscale-core =
|
|
26
|
+
python = "^3.12"
|
|
27
|
+
click = "^8.3.1"
|
|
28
|
+
quickscale-core = "^0.73.0"
|
|
29
29
|
|
|
30
30
|
[tool.poetry.group.dev.dependencies]
|
|
31
31
|
# Dev dependencies centralized in root pyproject.toml
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Auto-generated by scripts/version_tool.sh
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.73.0"
|
{quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/apply_command.py
RENAMED
|
@@ -23,6 +23,7 @@ from quickscale_cli.schema.state_schema import (
|
|
|
23
23
|
QuickScaleState,
|
|
24
24
|
StateManager,
|
|
25
25
|
)
|
|
26
|
+
from quickscale_core.utils.git_utils import is_working_directory_clean
|
|
26
27
|
from quickscale_core.generator import ProjectGenerator
|
|
27
28
|
from quickscale_core.manifest import ModuleManifest
|
|
28
29
|
from quickscale_core.manifest.loader import get_manifest_for_module
|
|
@@ -516,6 +517,9 @@ def _embed_modules_step(
|
|
|
516
517
|
f"⚠️ Module embedding failed for {module_name}, continuing...",
|
|
517
518
|
fg="yellow",
|
|
518
519
|
)
|
|
520
|
+
# Commit any partial changes to allow subsequent modules to be embedded
|
|
521
|
+
if not is_working_directory_clean(output_path):
|
|
522
|
+
_git_commit(output_path, f"Partial module: {module_name} (incomplete)")
|
|
519
523
|
else:
|
|
520
524
|
embedded_modules.append(module_name)
|
|
521
525
|
_git_commit(output_path, f"Add module: {module_name}")
|
{quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/deployment_commands.py
RENAMED
|
@@ -95,21 +95,19 @@ def _check_poetry_lock_step() -> None:
|
|
|
95
95
|
click.echo(
|
|
96
96
|
"💡 The lock file needs to be regenerated to match pyproject.toml"
|
|
97
97
|
)
|
|
98
|
-
if click.confirm("Run 'poetry lock
|
|
98
|
+
if click.confirm("Run 'poetry lock' to fix?", default=True):
|
|
99
99
|
click.echo("🔄 Updating poetry.lock...")
|
|
100
100
|
success, fix_message = fix_poetry_lock()
|
|
101
101
|
if success:
|
|
102
102
|
click.secho(f"✅ {fix_message}", fg="green")
|
|
103
103
|
else:
|
|
104
104
|
click.secho(f"❌ Error: {fix_message}", fg="red", err=True)
|
|
105
|
-
click.echo(
|
|
106
|
-
"💡 Try running manually: poetry lock --no-update", err=True
|
|
107
|
-
)
|
|
105
|
+
click.echo("💡 Try running manually: poetry lock", err=True)
|
|
108
106
|
if not click.confirm("Continue anyway?", default=False):
|
|
109
107
|
click.echo("Deployment cancelled")
|
|
110
108
|
sys.exit(0)
|
|
111
109
|
else:
|
|
112
|
-
click.echo("💡 Run 'poetry lock
|
|
110
|
+
click.echo("💡 Run 'poetry lock' before deploying")
|
|
113
111
|
if not click.confirm("Continue without fixing?", default=False):
|
|
114
112
|
click.echo("Deployment cancelled")
|
|
115
113
|
sys.exit(0)
|
{quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/module_commands.py
RENAMED
|
@@ -23,7 +23,7 @@ from .module_config import (
|
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
# Available modules
|
|
26
|
-
AVAILABLE_MODULES = ["auth", "billing", "teams", "blog", "listings"]
|
|
26
|
+
AVAILABLE_MODULES = ["auth", "billing", "teams", "blog", "listings", "crm"]
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _validate_git_environment() -> bool:
|
|
@@ -171,7 +171,7 @@ def _perform_module_embed(
|
|
|
171
171
|
applier(project_path, config)
|
|
172
172
|
|
|
173
173
|
# Install dependencies for modules that need it
|
|
174
|
-
if module in ["auth", "blog", "listings"]:
|
|
174
|
+
if module in ["auth", "blog", "listings", "crm"]:
|
|
175
175
|
if not _install_module_dependencies(project_path, module):
|
|
176
176
|
return False
|
|
177
177
|
|
{quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/module_config.py
RENAMED
|
@@ -12,6 +12,35 @@ from typing import Any
|
|
|
12
12
|
import click
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def _is_app_in_installed_apps(settings_content: str, app_name: str) -> bool:
|
|
16
|
+
"""Check if an app is already in INSTALLED_APPS.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
settings_content: The content of settings.py
|
|
20
|
+
app_name: The app name to check for (e.g., 'django_filters')
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
True if the app is already in INSTALLED_APPS, False otherwise
|
|
24
|
+
"""
|
|
25
|
+
# Check for app in INSTALLED_APPS list or INSTALLED_APPS +=
|
|
26
|
+
# Match patterns like: "app_name", 'app_name' in lists
|
|
27
|
+
pattern = rf'["\']({re.escape(app_name)})["\']'
|
|
28
|
+
return bool(re.search(pattern, settings_content))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _filter_new_apps(settings_content: str, apps: list[str]) -> list[str]:
|
|
32
|
+
"""Filter out apps that are already in INSTALLED_APPS.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
settings_content: The content of settings.py
|
|
36
|
+
apps: List of app names to filter
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of apps that are NOT already in settings.py
|
|
40
|
+
"""
|
|
41
|
+
return [app for app in apps if not _is_app_in_installed_apps(settings_content, app)]
|
|
42
|
+
|
|
43
|
+
|
|
15
44
|
def has_migrations_been_run() -> bool:
|
|
16
45
|
"""Check if Django migrations have been run in the current project"""
|
|
17
46
|
# Check for SQLite database file
|
|
@@ -560,22 +589,33 @@ def apply_listings_configuration(project_path: Path, config: dict[str, Any]) ->
|
|
|
560
589
|
if pyproject_path.exists():
|
|
561
590
|
_add_django_filter_dependency(project_path, pyproject_path)
|
|
562
591
|
|
|
563
|
-
#
|
|
564
|
-
|
|
592
|
+
# Determine which apps need to be added (avoid duplicates)
|
|
593
|
+
required_apps = ["django_filters", "quickscale_modules_listings"]
|
|
594
|
+
new_apps = _filter_new_apps(settings_content, required_apps)
|
|
595
|
+
|
|
596
|
+
if not new_apps:
|
|
597
|
+
click.echo("ℹ️ All required apps already in INSTALLED_APPS")
|
|
598
|
+
else:
|
|
599
|
+
# Build the INSTALLED_APPS addition with only new apps
|
|
600
|
+
apps_list = ",\n ".join([f'"{app}"' for app in new_apps])
|
|
601
|
+
installed_apps_addition = f"""
|
|
565
602
|
# QuickScale Listings Module - Added by quickscale embed
|
|
566
603
|
INSTALLED_APPS += [
|
|
567
|
-
|
|
568
|
-
"quickscale_modules_listings", # Listings module
|
|
604
|
+
{apps_list},
|
|
569
605
|
]
|
|
606
|
+
"""
|
|
607
|
+
# Append to settings.py
|
|
608
|
+
with open(settings_path, "a") as f:
|
|
609
|
+
f.write("\n" + installed_apps_addition)
|
|
570
610
|
|
|
611
|
+
# Add settings (always add these)
|
|
612
|
+
settings_addition = f"""
|
|
571
613
|
# Listings Module Settings
|
|
614
|
+
LISTINGS_PER_PAGE = {config["listings_per_page"]}
|
|
572
615
|
"""
|
|
573
616
|
|
|
574
|
-
installed_apps_addition += f"LISTINGS_PER_PAGE = {config['listings_per_page']}\n"
|
|
575
|
-
|
|
576
|
-
# Append to settings.py
|
|
577
617
|
with open(settings_path, "a") as f:
|
|
578
|
-
f.write(
|
|
618
|
+
f.write(settings_addition)
|
|
579
619
|
|
|
580
620
|
click.secho(" ✅ Updated settings.py with listings configuration", fg="green")
|
|
581
621
|
|
|
@@ -602,6 +642,248 @@ INSTALLED_APPS += [
|
|
|
602
642
|
click.echo(f" • Listings per page: {config['listings_per_page']}")
|
|
603
643
|
|
|
604
644
|
|
|
645
|
+
# ============================================================================
|
|
646
|
+
# CRM MODULE CONFIGURATION
|
|
647
|
+
# ============================================================================
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def get_default_crm_config() -> dict[str, Any]:
|
|
651
|
+
"""Get default configuration for CRM module (non-interactive mode)"""
|
|
652
|
+
return {
|
|
653
|
+
"enable_api": True,
|
|
654
|
+
"deals_per_page": 25,
|
|
655
|
+
"contacts_per_page": 50,
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def configure_crm_module(non_interactive: bool = False) -> dict[str, Any]:
|
|
660
|
+
"""Interactive configuration for CRM module"""
|
|
661
|
+
if non_interactive:
|
|
662
|
+
click.echo("\n⚙️ Using default CRM module configuration...")
|
|
663
|
+
config = get_default_crm_config()
|
|
664
|
+
click.echo(" • API: Enabled")
|
|
665
|
+
click.echo(f" • Deals per page: {config['deals_per_page']}")
|
|
666
|
+
click.echo(f" • Contacts per page: {config['contacts_per_page']}")
|
|
667
|
+
return config
|
|
668
|
+
|
|
669
|
+
click.echo("\n⚙️ Configuring CRM module...")
|
|
670
|
+
click.echo(
|
|
671
|
+
"The CRM module provides contact management, companies, and deal pipeline.\n"
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
config = {
|
|
675
|
+
"enable_api": click.confirm("Enable REST API endpoints?", default=True),
|
|
676
|
+
"deals_per_page": click.prompt(
|
|
677
|
+
"Deals per page",
|
|
678
|
+
type=int,
|
|
679
|
+
default=25,
|
|
680
|
+
),
|
|
681
|
+
"contacts_per_page": click.prompt(
|
|
682
|
+
"Contacts per page",
|
|
683
|
+
type=int,
|
|
684
|
+
default=50,
|
|
685
|
+
),
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return config
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _add_drf_and_filter_dependencies(project_path: Path, pyproject_path: Path) -> None:
|
|
692
|
+
"""Add djangorestframework and django-filter dependencies to project's pyproject.toml."""
|
|
693
|
+
with open(pyproject_path) as f:
|
|
694
|
+
pyproject_content = f.read()
|
|
695
|
+
|
|
696
|
+
# Read versions from the embedded CRM module
|
|
697
|
+
crm_pyproject_path = project_path / "modules" / "crm" / "pyproject.toml"
|
|
698
|
+
|
|
699
|
+
if not crm_pyproject_path.exists():
|
|
700
|
+
click.secho(
|
|
701
|
+
"❌ Error: CRM module pyproject.toml not found. "
|
|
702
|
+
"Cannot determine dependency version requirements.",
|
|
703
|
+
fg="red",
|
|
704
|
+
err=True,
|
|
705
|
+
)
|
|
706
|
+
click.echo(f"Expected file: {crm_pyproject_path}", err=True)
|
|
707
|
+
click.echo(
|
|
708
|
+
"This indicates the CRM module was not embedded correctly.",
|
|
709
|
+
err=True,
|
|
710
|
+
)
|
|
711
|
+
raise click.Abort()
|
|
712
|
+
|
|
713
|
+
try:
|
|
714
|
+
with open(crm_pyproject_path) as f:
|
|
715
|
+
crm_pyproject_content = f.read()
|
|
716
|
+
|
|
717
|
+
# Extract djangorestframework version
|
|
718
|
+
drf_version = None
|
|
719
|
+
if "djangorestframework" not in pyproject_content:
|
|
720
|
+
drf_match = re.search(
|
|
721
|
+
r'djangorestframework\s*=\s*["\']([^"\']+)["\']',
|
|
722
|
+
crm_pyproject_content,
|
|
723
|
+
)
|
|
724
|
+
if drf_match:
|
|
725
|
+
drf_version = drf_match.group(1)
|
|
726
|
+
|
|
727
|
+
# Extract django-filter version
|
|
728
|
+
filter_version = None
|
|
729
|
+
if "django-filter" not in pyproject_content:
|
|
730
|
+
filter_match = re.search(
|
|
731
|
+
r'django-filter\s*=\s*["\']([^"\']+)["\']',
|
|
732
|
+
crm_pyproject_content,
|
|
733
|
+
)
|
|
734
|
+
if filter_match:
|
|
735
|
+
filter_version = filter_match.group(1)
|
|
736
|
+
|
|
737
|
+
# Add dependencies if needed
|
|
738
|
+
if drf_version or filter_version:
|
|
739
|
+
dependencies_pattern = r"(\[tool\.poetry\.dependencies\][^\[]*)"
|
|
740
|
+
match = re.search(dependencies_pattern, pyproject_content, re.DOTALL)
|
|
741
|
+
if match:
|
|
742
|
+
dependencies_section = match.group(1)
|
|
743
|
+
additions = ""
|
|
744
|
+
if drf_version:
|
|
745
|
+
additions += f'\ndjangorestframework = "{drf_version}"'
|
|
746
|
+
if filter_version:
|
|
747
|
+
additions += f'\ndjango-filter = "{filter_version}"'
|
|
748
|
+
|
|
749
|
+
updated_dependencies = re.sub(
|
|
750
|
+
r'(python = "[^"]*")',
|
|
751
|
+
rf"\1{additions}",
|
|
752
|
+
dependencies_section,
|
|
753
|
+
)
|
|
754
|
+
pyproject_content = pyproject_content.replace(
|
|
755
|
+
dependencies_section, updated_dependencies
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
with open(pyproject_path, "w") as f:
|
|
759
|
+
f.write(pyproject_content)
|
|
760
|
+
|
|
761
|
+
if drf_version:
|
|
762
|
+
click.secho(
|
|
763
|
+
" ✅ Added djangorestframework to pyproject.toml", fg="green"
|
|
764
|
+
)
|
|
765
|
+
if filter_version:
|
|
766
|
+
click.secho(
|
|
767
|
+
" ✅ Added django-filter to pyproject.toml", fg="green"
|
|
768
|
+
)
|
|
769
|
+
except (FileNotFoundError, AttributeError) as e:
|
|
770
|
+
click.secho(
|
|
771
|
+
f"❌ Error: Failed to parse dependencies from CRM module: {e}",
|
|
772
|
+
fg="red",
|
|
773
|
+
err=True,
|
|
774
|
+
)
|
|
775
|
+
click.echo(f"File: {crm_pyproject_path}", err=True)
|
|
776
|
+
raise click.Abort()
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def apply_crm_configuration(project_path: Path, config: dict[str, Any]) -> None:
|
|
780
|
+
"""Apply CRM module configuration to project settings"""
|
|
781
|
+
# QuickScale uses settings/base.py and project_name/urls.py structure
|
|
782
|
+
settings_path = project_path / f"{project_path.name}" / "settings" / "base.py"
|
|
783
|
+
urls_path = project_path / f"{project_path.name}" / "urls.py"
|
|
784
|
+
pyproject_path = project_path / "pyproject.toml"
|
|
785
|
+
|
|
786
|
+
if not settings_path.exists():
|
|
787
|
+
click.secho(
|
|
788
|
+
"⚠️ Warning: settings.py not found, skipping auto-configuration",
|
|
789
|
+
fg="yellow",
|
|
790
|
+
)
|
|
791
|
+
return
|
|
792
|
+
|
|
793
|
+
# Read settings.py
|
|
794
|
+
with open(settings_path) as f:
|
|
795
|
+
settings_content = f.read()
|
|
796
|
+
|
|
797
|
+
# Check if already configured
|
|
798
|
+
if "quickscale_modules_crm" in settings_content:
|
|
799
|
+
click.echo("ℹ️ CRM module already configured in settings.py")
|
|
800
|
+
return
|
|
801
|
+
|
|
802
|
+
# Add DRF and django-filter dependencies to pyproject.toml
|
|
803
|
+
if pyproject_path.exists():
|
|
804
|
+
_add_drf_and_filter_dependencies(project_path, pyproject_path)
|
|
805
|
+
|
|
806
|
+
# Determine which apps need to be added (avoid duplicates)
|
|
807
|
+
required_apps = ["rest_framework", "django_filters", "quickscale_modules_crm"]
|
|
808
|
+
new_apps = _filter_new_apps(settings_content, required_apps)
|
|
809
|
+
|
|
810
|
+
if not new_apps:
|
|
811
|
+
click.echo("ℹ️ All required apps already in INSTALLED_APPS")
|
|
812
|
+
else:
|
|
813
|
+
# Build the INSTALLED_APPS addition with only new apps
|
|
814
|
+
apps_list = ",\n ".join([f'"{app}"' for app in new_apps])
|
|
815
|
+
installed_apps_addition = f"""
|
|
816
|
+
# QuickScale CRM Module - Added by quickscale embed
|
|
817
|
+
INSTALLED_APPS += [
|
|
818
|
+
{apps_list},
|
|
819
|
+
]
|
|
820
|
+
"""
|
|
821
|
+
# Append INSTALLED_APPS to settings.py
|
|
822
|
+
with open(settings_path, "a") as f:
|
|
823
|
+
f.write("\n" + installed_apps_addition)
|
|
824
|
+
|
|
825
|
+
# Add CRM settings (always add these)
|
|
826
|
+
settings_addition = f"""
|
|
827
|
+
# CRM Module Settings
|
|
828
|
+
CRM_DEALS_PER_PAGE = {config["deals_per_page"]}
|
|
829
|
+
CRM_CONTACTS_PER_PAGE = {config["contacts_per_page"]}
|
|
830
|
+
CRM_ENABLE_API = {config["enable_api"]}
|
|
831
|
+
"""
|
|
832
|
+
|
|
833
|
+
# Add REST Framework settings if enabling API
|
|
834
|
+
if config["enable_api"]:
|
|
835
|
+
settings_addition += """
|
|
836
|
+
# REST Framework Settings
|
|
837
|
+
REST_FRAMEWORK = {
|
|
838
|
+
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
839
|
+
"rest_framework.authentication.SessionAuthentication",
|
|
840
|
+
],
|
|
841
|
+
"DEFAULT_PERMISSION_CLASSES": [
|
|
842
|
+
"rest_framework.permissions.IsAuthenticated",
|
|
843
|
+
],
|
|
844
|
+
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
|
845
|
+
"PAGE_SIZE": 25,
|
|
846
|
+
"DEFAULT_FILTER_BACKENDS": [
|
|
847
|
+
"django_filters.rest_framework.DjangoFilterBackend",
|
|
848
|
+
"rest_framework.filters.SearchFilter",
|
|
849
|
+
"rest_framework.filters.OrderingFilter",
|
|
850
|
+
],
|
|
851
|
+
}
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
# Append settings to settings.py
|
|
855
|
+
with open(settings_path, "a") as f:
|
|
856
|
+
f.write(settings_addition)
|
|
857
|
+
|
|
858
|
+
click.secho(" ✅ Updated settings.py with CRM configuration", fg="green")
|
|
859
|
+
|
|
860
|
+
# Update urls.py
|
|
861
|
+
if urls_path.exists():
|
|
862
|
+
with open(urls_path) as f:
|
|
863
|
+
urls_content = f.read()
|
|
864
|
+
|
|
865
|
+
if "quickscale_modules_crm" not in urls_content:
|
|
866
|
+
# Find urlpatterns and add CRM URLs
|
|
867
|
+
if "urlpatterns = [" in urls_content:
|
|
868
|
+
urls_addition = (
|
|
869
|
+
' path("crm/", include("quickscale_modules_crm.urls")),\n'
|
|
870
|
+
)
|
|
871
|
+
urls_content = urls_content.replace(
|
|
872
|
+
"urlpatterns = [", "urlpatterns = [\n" + urls_addition
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
with open(urls_path, "w") as f:
|
|
876
|
+
f.write(urls_content)
|
|
877
|
+
|
|
878
|
+
click.secho(" ✅ Updated urls.py with CRM URLs", fg="green")
|
|
879
|
+
|
|
880
|
+
# Show configuration summary
|
|
881
|
+
click.echo("\n📋 Configuration applied:")
|
|
882
|
+
click.echo(f" • API: {'Enabled' if config['enable_api'] else 'Disabled'}")
|
|
883
|
+
click.echo(f" • Deals per page: {config['deals_per_page']}")
|
|
884
|
+
click.echo(f" • Contacts per page: {config['contacts_per_page']}")
|
|
885
|
+
|
|
886
|
+
|
|
605
887
|
# ============================================================================
|
|
606
888
|
# MODULE CONFIGURATORS REGISTRY
|
|
607
889
|
# ============================================================================
|
|
@@ -610,4 +892,5 @@ MODULE_CONFIGURATORS = {
|
|
|
610
892
|
"auth": (configure_auth_module, apply_auth_configuration),
|
|
611
893
|
"blog": (configure_blog_module, apply_blog_configuration),
|
|
612
894
|
"listings": (configure_listings_module, apply_listings_configuration),
|
|
895
|
+
"crm": (configure_crm_module, apply_crm_configuration),
|
|
613
896
|
}
|
|
@@ -30,6 +30,7 @@ AVAILABLE_MODULES = [
|
|
|
30
30
|
("auth", "Authentication with django-allauth"),
|
|
31
31
|
("blog", "Markdown-powered blog with categories and RSS"),
|
|
32
32
|
("listings", "Generic listings for marketplace verticals"),
|
|
33
|
+
("crm", "Customer Relationship Management (contacts, deals, pipeline)"),
|
|
33
34
|
("billing", "Stripe integration (placeholder)"),
|
|
34
35
|
("teams", "Multi-tenancy and team management (placeholder)"),
|
|
35
36
|
]
|
|
@@ -66,9 +67,9 @@ def _confirm_unimplemented_theme(theme_id: str) -> bool:
|
|
|
66
67
|
def _select_theme() -> str:
|
|
67
68
|
"""Interactive theme selection"""
|
|
68
69
|
click.echo("\n🎨 Select a theme for your project:")
|
|
69
|
-
for i, (
|
|
70
|
-
status = " ⚠️ not yet implemented" if
|
|
71
|
-
click.echo(f" {i}. {
|
|
70
|
+
for i, (tid, description) in enumerate(AVAILABLE_THEMES, start=1):
|
|
71
|
+
status = " ⚠️ not yet implemented" if tid != "showcase_html" else ""
|
|
72
|
+
click.echo(f" {i}. {tid} - {description}{status}")
|
|
72
73
|
|
|
73
74
|
while True:
|
|
74
75
|
choice = click.prompt(
|
|
@@ -78,13 +79,13 @@ def _select_theme() -> str:
|
|
|
78
79
|
)
|
|
79
80
|
|
|
80
81
|
# Try numeric choice first
|
|
81
|
-
theme_id
|
|
82
|
+
theme_id: str | None
|
|
82
83
|
if choice.isdigit():
|
|
83
84
|
theme_id = _get_theme_by_index(int(choice) - 1)
|
|
84
85
|
else:
|
|
85
86
|
theme_id = _get_theme_by_name(choice)
|
|
86
87
|
|
|
87
|
-
if theme_id and _confirm_unimplemented_theme(theme_id):
|
|
88
|
+
if theme_id is not None and _confirm_unimplemented_theme(theme_id):
|
|
88
89
|
return theme_id
|
|
89
90
|
|
|
90
91
|
if theme_id is None:
|
|
@@ -298,8 +299,6 @@ def _parse_add_module_selection(
|
|
|
298
299
|
|
|
299
300
|
return selected
|
|
300
301
|
|
|
301
|
-
return selected
|
|
302
|
-
|
|
303
302
|
|
|
304
303
|
def _select_modules_to_add(existing_modules: list[str]) -> list[str]:
|
|
305
304
|
"""Interactive module selection for adding to existing project
|
{quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/remove_command.py
RENAMED
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
import click
|
|
10
10
|
import yaml
|
|
11
11
|
|
|
12
|
-
from quickscale_cli.schema.state_schema import StateManager
|
|
12
|
+
from quickscale_cli.schema.state_schema import QuickScaleState, StateManager
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _update_quickscale_yml(project_path: Path, module_name: str) -> bool:
|
|
@@ -120,7 +120,7 @@ def _update_urls_py(project_path: Path, module_name: str) -> tuple[bool, str]:
|
|
|
120
120
|
|
|
121
121
|
def _check_module_exists(
|
|
122
122
|
project_path: Path, module_name: str, state_manager: StateManager
|
|
123
|
-
) -> tuple[bool, bool,
|
|
123
|
+
) -> tuple[bool, bool, QuickScaleState | None]:
|
|
124
124
|
"""Check if module exists in state or filesystem
|
|
125
125
|
|
|
126
126
|
Returns:
|
|
@@ -135,7 +135,9 @@ def _check_module_exists(
|
|
|
135
135
|
return module_in_state, module_in_filesystem, state
|
|
136
136
|
|
|
137
137
|
|
|
138
|
-
def _show_module_not_found_error(
|
|
138
|
+
def _show_module_not_found_error(
|
|
139
|
+
module_name: str, state: QuickScaleState | None
|
|
140
|
+
) -> None:
|
|
139
141
|
"""Display error when module is not found"""
|
|
140
142
|
click.secho(
|
|
141
143
|
f"❌ Module '{module_name}' is not installed in this project",
|
|
@@ -143,7 +145,7 @@ def _show_module_not_found_error(module_name: str, state: object) -> None:
|
|
|
143
145
|
err=True,
|
|
144
146
|
)
|
|
145
147
|
click.echo("\n💡 Installed modules:", err=True)
|
|
146
|
-
if state and
|
|
148
|
+
if state and state.modules:
|
|
147
149
|
for name in state.modules:
|
|
148
150
|
click.echo(f" - {name}", err=True)
|
|
149
151
|
else:
|
|
@@ -192,10 +194,10 @@ def _log_step_result(success: bool, message: str, is_error: bool = False) -> Non
|
|
|
192
194
|
|
|
193
195
|
|
|
194
196
|
def _update_state_for_removal(
|
|
195
|
-
state:
|
|
197
|
+
state: QuickScaleState | None, module_name: str, state_manager: StateManager
|
|
196
198
|
) -> None:
|
|
197
199
|
"""Remove module from state and save"""
|
|
198
|
-
if not (state and
|
|
200
|
+
if not (state and module_name in state.modules):
|
|
199
201
|
return
|
|
200
202
|
|
|
201
203
|
del state.modules[module_name]
|
|
@@ -209,7 +211,7 @@ def _update_state_for_removal(
|
|
|
209
211
|
def _perform_removal_steps(
|
|
210
212
|
project_path: Path,
|
|
211
213
|
module_name: str,
|
|
212
|
-
state:
|
|
214
|
+
state: QuickScaleState | None,
|
|
213
215
|
state_manager: StateManager,
|
|
214
216
|
) -> None:
|
|
215
217
|
"""Execute all removal steps"""
|
|
@@ -72,7 +72,7 @@ VALID_TOP_LEVEL_KEYS = {"version", "project", "modules", "docker"}
|
|
|
72
72
|
VALID_PROJECT_KEYS = {"name", "theme"}
|
|
73
73
|
VALID_DOCKER_KEYS = {"start", "build"}
|
|
74
74
|
VALID_THEMES = {"showcase_html", "showcase_htmx", "showcase_react"}
|
|
75
|
-
AVAILABLE_MODULES = {"auth", "billing", "teams", "blog", "listings"}
|
|
75
|
+
AVAILABLE_MODULES = {"auth", "billing", "teams", "blog", "listings", "crm"}
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
def _find_line_number(yaml_content: str, key: str) -> int | None:
|
|
@@ -588,7 +588,7 @@ def check_poetry_lock_consistency() -> tuple[bool, str]:
|
|
|
588
588
|
|
|
589
589
|
def fix_poetry_lock() -> tuple[bool, str]:
|
|
590
590
|
"""
|
|
591
|
-
Fix poetry.lock by running poetry lock
|
|
591
|
+
Fix poetry.lock by running poetry lock.
|
|
592
592
|
|
|
593
593
|
Returns
|
|
594
594
|
-------
|
|
@@ -598,34 +598,17 @@ def fix_poetry_lock() -> tuple[bool, str]:
|
|
|
598
598
|
|
|
599
599
|
"""
|
|
600
600
|
try:
|
|
601
|
-
# Prefer `--no-update` when available to avoid changing resolved versions.
|
|
602
601
|
result = subprocess.run(
|
|
603
|
-
["poetry", "lock"
|
|
602
|
+
["poetry", "lock"],
|
|
604
603
|
capture_output=True,
|
|
605
604
|
text=True,
|
|
606
|
-
timeout=
|
|
605
|
+
timeout=120,
|
|
607
606
|
)
|
|
608
607
|
|
|
609
608
|
if result.returncode == 0:
|
|
610
609
|
return True, "poetry.lock updated successfully"
|
|
611
610
|
|
|
612
|
-
|
|
613
|
-
# fall back to a plain `poetry lock` which is more widely supported.
|
|
614
|
-
# Detect common failure patterns but always attempt fallback once.
|
|
615
|
-
fallback = subprocess.run(
|
|
616
|
-
["poetry", "lock"],
|
|
617
|
-
capture_output=True,
|
|
618
|
-
text=True,
|
|
619
|
-
timeout=120,
|
|
620
|
-
)
|
|
621
|
-
|
|
622
|
-
if fallback.returncode == 0:
|
|
623
|
-
return True, "poetry.lock updated successfully (fallback)"
|
|
624
|
-
else:
|
|
625
|
-
return False, (
|
|
626
|
-
f"Failed to update poetry.lock (--no-update stderr: {result.stderr}; "
|
|
627
|
-
f"fallback stderr: {fallback.stderr})"
|
|
628
|
-
)
|
|
611
|
+
return False, f"Failed to update poetry.lock: {result.stderr}"
|
|
629
612
|
except subprocess.TimeoutExpired:
|
|
630
613
|
return False, "poetry lock timed out"
|
|
631
614
|
except (subprocess.SubprocessError, FileNotFoundError) as e:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/development_commands.py
RENAMED
|
File without changes
|
{quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/commands/status_command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quickscale_cli-0.72.0 → quickscale_cli-0.73.0}/src/quickscale_cli/utils/dependency_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|