kctl-react 0.6.2__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.
Files changed (102) hide show
  1. kctl_react/__init__.py +3 -0
  2. kctl_react/__main__.py +5 -0
  3. kctl_react/cli.py +201 -0
  4. kctl_react/commands/__init__.py +0 -0
  5. kctl_react/commands/a11y.py +78 -0
  6. kctl_react/commands/affected.py +170 -0
  7. kctl_react/commands/apps.py +353 -0
  8. kctl_react/commands/build.py +376 -0
  9. kctl_react/commands/bundle_cmd.py +217 -0
  10. kctl_react/commands/cap.py +1465 -0
  11. kctl_react/commands/clean.py +76 -0
  12. kctl_react/commands/codegen.py +491 -0
  13. kctl_react/commands/compliance.py +587 -0
  14. kctl_react/commands/config_cmd.py +368 -0
  15. kctl_react/commands/dashboard.py +163 -0
  16. kctl_react/commands/deploy.py +318 -0
  17. kctl_react/commands/deps.py +792 -0
  18. kctl_react/commands/dev.py +96 -0
  19. kctl_react/commands/docker_cmd.py +73 -0
  20. kctl_react/commands/doctor.py +170 -0
  21. kctl_react/commands/e2e.py +343 -0
  22. kctl_react/commands/env.py +155 -0
  23. kctl_react/commands/i18n.py +310 -0
  24. kctl_react/commands/lint.py +306 -0
  25. kctl_react/commands/maintenance.py +308 -0
  26. kctl_react/commands/monitor_cmd.py +50 -0
  27. kctl_react/commands/observe.py +34 -0
  28. kctl_react/commands/packages.py +129 -0
  29. kctl_react/commands/perf.py +762 -0
  30. kctl_react/commands/pipeline.py +289 -0
  31. kctl_react/commands/pwa.py +193 -0
  32. kctl_react/commands/scaffold.py +323 -0
  33. kctl_react/commands/security.py +660 -0
  34. kctl_react/commands/skill_cmd.py +54 -0
  35. kctl_react/commands/state.py +254 -0
  36. kctl_react/commands/test_cmd.py +418 -0
  37. kctl_react/commands/ui_audit.py +889 -0
  38. kctl_react/core/__init__.py +0 -0
  39. kctl_react/core/analyzers.py +200 -0
  40. kctl_react/core/callbacks.py +70 -0
  41. kctl_react/core/compliance/__init__.py +3 -0
  42. kctl_react/core/compliance/api_check/__init__.py +3 -0
  43. kctl_react/core/compliance/api_check/checks/__init__.py +18 -0
  44. kctl_react/core/compliance/api_check/checks/endpoints.py +53 -0
  45. kctl_react/core/compliance/api_check/checks/envelope.py +44 -0
  46. kctl_react/core/compliance/api_check/checks/naming.py +60 -0
  47. kctl_react/core/compliance/api_check/checks/params.py +44 -0
  48. kctl_react/core/compliance/api_check/checks/requests.py +57 -0
  49. kctl_react/core/compliance/api_check/checks/types.py +55 -0
  50. kctl_react/core/compliance/api_check/hooks.py +133 -0
  51. kctl_react/core/compliance/api_check/matcher.py +55 -0
  52. kctl_react/core/compliance/api_check/schema.py +151 -0
  53. kctl_react/core/compliance/api_health/__init__.py +35 -0
  54. kctl_react/core/compliance/api_health/checks/__init__.py +9 -0
  55. kctl_react/core/compliance/api_health/checks/auth.py +72 -0
  56. kctl_react/core/compliance/api_health/checks/reachable.py +44 -0
  57. kctl_react/core/compliance/api_health/checks/response.py +55 -0
  58. kctl_react/core/compliance/api_health/checks/timing.py +38 -0
  59. kctl_react/core/compliance/api_health/client.py +99 -0
  60. kctl_react/core/compliance/api_health/sampler.py +16 -0
  61. kctl_react/core/compliance/checks/__init__.py +47 -0
  62. kctl_react/core/compliance/checks/api.py +101 -0
  63. kctl_react/core/compliance/checks/codegen.py +94 -0
  64. kctl_react/core/compliance/checks/darkmode.py +57 -0
  65. kctl_react/core/compliance/checks/errors.py +68 -0
  66. kctl_react/core/compliance/checks/features.py +66 -0
  67. kctl_react/core/compliance/checks/i18n_check.py +105 -0
  68. kctl_react/core/compliance/checks/imports.py +86 -0
  69. kctl_react/core/compliance/checks/navigation.py +62 -0
  70. kctl_react/core/compliance/checks/practices.py +122 -0
  71. kctl_react/core/compliance/checks/providers.py +85 -0
  72. kctl_react/core/compliance/checks/pwa.py +101 -0
  73. kctl_react/core/compliance/checks/responsive.py +47 -0
  74. kctl_react/core/compliance/checks/scripts.py +85 -0
  75. kctl_react/core/compliance/checks/shadcn.py +51 -0
  76. kctl_react/core/compliance/checks/structure.py +76 -0
  77. kctl_react/core/compliance/checks/testing.py +83 -0
  78. kctl_react/core/compliance/checks/theme.py +92 -0
  79. kctl_react/core/compliance/checks/ui_standard.py +185 -0
  80. kctl_react/core/compliance/checks/vite.py +83 -0
  81. kctl_react/core/compliance/engine.py +87 -0
  82. kctl_react/core/compliance/exceptions_map.py +15 -0
  83. kctl_react/core/compliance/fixes/__init__.py +33 -0
  84. kctl_react/core/compliance/fixes/codegen_fix.py +28 -0
  85. kctl_react/core/compliance/fixes/i18n_fix.py +61 -0
  86. kctl_react/core/compliance/fixes/imports_fix.py +36 -0
  87. kctl_react/core/compliance/fixes/structure_fix.py +20 -0
  88. kctl_react/core/compliance/fixes/theme_fix.py +29 -0
  89. kctl_react/core/compliance/models.py +106 -0
  90. kctl_react/core/config.py +201 -0
  91. kctl_react/core/discovery.py +185 -0
  92. kctl_react/core/exceptions.py +17 -0
  93. kctl_react/core/git.py +146 -0
  94. kctl_react/core/history.py +121 -0
  95. kctl_react/core/output.py +5 -0
  96. kctl_react/core/plugins.py +13 -0
  97. kctl_react/core/runner.py +34 -0
  98. kctl_react/py.typed +0 -0
  99. kctl_react-0.6.2.dist-info/METADATA +17 -0
  100. kctl_react-0.6.2.dist-info/RECORD +102 -0
  101. kctl_react-0.6.2.dist-info/WHEEL +4 -0
  102. kctl_react-0.6.2.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,323 @@
1
+ """Scaffolding commands for generating code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from kctl_react.core.callbacks import AppContext
10
+ from kctl_react.core.discovery import get_app_dir
11
+
12
+ app = typer.Typer(help="Scaffold new apps, pages, hooks, and components.")
13
+
14
+
15
+ @app.command()
16
+ def page(
17
+ ctx: typer.Context,
18
+ app_name: Annotated[str, typer.Argument(help="App name (e.g. sfa, wms)")],
19
+ name: Annotated[str, typer.Argument(help="Page name in PascalCase (e.g. CustomerDetail)")],
20
+ ) -> None:
21
+ """Scaffold a new page component.
22
+
23
+ Creates: src/pages/{Name}.tsx with standard imports, i18n, and layout.
24
+ """
25
+ actx: AppContext = ctx.obj
26
+ out = actx.output
27
+ root = actx.project_root
28
+
29
+ actx.validate_app(app_name)
30
+ app_dir = get_app_dir(root, app_name)
31
+
32
+ if actx.is_nextjs(app_name):
33
+ # Next.js App Router: app/{slug}/page.tsx
34
+ slug = name.lower().replace(" ", "-")
35
+ page_file = app_dir / "app" / slug / "page.tsx"
36
+ if page_file.exists():
37
+ out.error(f"Page already exists: {page_file}")
38
+ raise typer.Exit(1)
39
+ page_file.parent.mkdir(parents=True, exist_ok=True)
40
+
41
+ content = f'''import type {{ Metadata }} from "next";
42
+
43
+ export const metadata: Metadata = {{
44
+ title: "{name}",
45
+ }};
46
+
47
+ export default function {name}Page() {{
48
+ return (
49
+ <main className="p-4">
50
+ <h1 className="text-lg font-semibold">{name}</h1>
51
+ </main>
52
+ );
53
+ }}
54
+ '''
55
+ page_file.write_text(content)
56
+ out.success(f"Created {page_file.relative_to(root)}")
57
+ out.info(f"Add navigation link for /{slug} in your layout or nav component")
58
+ else:
59
+ # Vite React: src/pages/{Name}.tsx
60
+ page_file = app_dir / "src" / "pages" / f"{name}.tsx"
61
+ if page_file.exists():
62
+ out.error(f"Page already exists: {page_file}")
63
+ raise typer.Exit(1)
64
+ page_file.parent.mkdir(parents=True, exist_ok=True)
65
+
66
+ content = f'''import {{ useTranslation }} from "react-i18next";
67
+ import {{ usePageTitle }} from "@kodemeio/core/hooks";
68
+ import {{ MobileLayout }} from "@kodemeio/ui";
69
+
70
+ export function {name}() {{
71
+ const {{ t }} = useTranslation();
72
+ usePageTitle(t("{name.lower()}.title"));
73
+
74
+ return (
75
+ <MobileLayout title={{t("{name.lower()}.title")}}>
76
+ <div className="p-4">
77
+ <h1 className="text-lg font-semibold">{{t("{name.lower()}.title")}}</h1>
78
+ </div>
79
+ </MobileLayout>
80
+ );
81
+ }}
82
+ '''
83
+ page_file.write_text(content)
84
+ out.success(f"Created {page_file.relative_to(root)}")
85
+ out.info(f"Add route in {app_name}/src/App.tsx and i18n keys in en.json + id.json")
86
+
87
+
88
+ @app.command()
89
+ def hook(
90
+ ctx: typer.Context,
91
+ app_name: Annotated[str, typer.Argument(help="App name")],
92
+ resource: Annotated[str, typer.Argument(help="Resource name (e.g. customers, orders)")],
93
+ ) -> None:
94
+ """Scaffold a TanStack Query API hook.
95
+
96
+ Creates: src/hooks/use{Resource}.ts with list + detail queries and mutation.
97
+ """
98
+ actx: AppContext = ctx.obj
99
+ out = actx.output
100
+ root = actx.project_root
101
+
102
+ actx.validate_app(app_name)
103
+ app_dir = get_app_dir(root, app_name)
104
+
105
+ # Convert resource to hook name
106
+ singular = resource[:-1] if resource.endswith("s") and not resource.endswith("ss") else resource
107
+ pascal = singular.capitalize()
108
+ plural = resource
109
+
110
+ hook_file = app_dir / "src" / "hooks" / f"use{pascal}.ts"
111
+ if hook_file.exists():
112
+ out.error(f"Hook already exists: {hook_file}")
113
+ raise typer.Exit(1)
114
+
115
+ hook_file.parent.mkdir(parents=True, exist_ok=True)
116
+
117
+ content = f'''import {{ useQuery, useMutation, useQueryClient }} from "@tanstack/react-query";
118
+ import {{ toast }} from "sonner";
119
+ import {{ useTranslation }} from "react-i18next";
120
+ import {{ client }} from "@/lib/client";
121
+ import {{ getErrorMessage }} from "@kodemeio/core/errors";
122
+ import type {{ {pascal}ListResponse, {pascal}DetailResponse, {pascal}CreateData }} from "@/types/api";
123
+
124
+ export function use{pascal}List(params?: {pascal}ListResponse["params"]) {{
125
+ return useQuery({{
126
+ queryKey: ["{plural}", params],
127
+ queryFn: () => client.get<{pascal}ListResponse>("/{plural}/", {{ params }}),
128
+ }});
129
+ }}
130
+
131
+ export function use{pascal}Detail(id: number | undefined) {{
132
+ return useQuery({{
133
+ queryKey: ["{plural}", id],
134
+ queryFn: () => client.get<{pascal}DetailResponse>(`/{plural}/${{id}}/`),
135
+ enabled: !!id,
136
+ }});
137
+ }}
138
+
139
+ export function useCreate{pascal}() {{
140
+ const queryClient = useQueryClient();
141
+ const {{ t }} = useTranslation();
142
+
143
+ return useMutation({{
144
+ mutationFn: (data: {pascal}CreateData) => client.post<{pascal}DetailResponse>("/{plural}/", data),
145
+ onSuccess: () => {{
146
+ queryClient.invalidateQueries({{ queryKey: ["{plural}"] }});
147
+ toast.success(t("{singular}.created"));
148
+ }},
149
+ onError: (err) => toast.error(getErrorMessage(err)),
150
+ }});
151
+ }}
152
+ '''
153
+ hook_file.write_text(content)
154
+ out.success(f"Created {hook_file.relative_to(root)}")
155
+ out.info("Import types from @/types/api and add i18n keys")
156
+
157
+
158
+ @app.command()
159
+ def form(
160
+ ctx: typer.Context,
161
+ app_name: Annotated[str, typer.Argument(help="App name")],
162
+ name: Annotated[str, typer.Argument(help="Form component name in PascalCase (e.g. CustomerForm)")],
163
+ ) -> None:
164
+ """Scaffold a react-hook-form + zod form component.
165
+
166
+ Creates: src/components/{Name}.tsx with useForm, zodResolver, and shadcn/ui inputs.
167
+ """
168
+ actx: AppContext = ctx.obj
169
+ out = actx.output
170
+ root = actx.project_root
171
+
172
+ actx.validate_app(app_name)
173
+ app_dir = get_app_dir(root, app_name)
174
+
175
+ form_file = app_dir / "src" / "components" / f"{name}.tsx"
176
+ if form_file.exists():
177
+ out.error(f"Form component already exists: {form_file}")
178
+ raise typer.Exit(1)
179
+
180
+ form_file.parent.mkdir(parents=True, exist_ok=True)
181
+
182
+ content = f"""import {{ useForm }} from "react-hook-form";
183
+ import {{ zodResolver }} from "@hookform/resolvers/zod";
184
+ import {{ z }} from "zod";
185
+ import {{ Button }} from "@kodemeio/ui";
186
+ import {{ Input }} from "@kodemeio/ui";
187
+ import {{ Label }} from "@kodemeio/ui";
188
+
189
+ const schema = z.object({{
190
+ name: z.string().min(1, "Name is required"),
191
+ }});
192
+
193
+ type FormValues = z.infer<typeof schema>;
194
+
195
+ interface {name}Props {{
196
+ onSubmit: (data: FormValues) => void;
197
+ defaultValues?: Partial<FormValues>;
198
+ }}
199
+
200
+ export function {name}({{ onSubmit, defaultValues }}: {name}Props) {{
201
+ const {{
202
+ register,
203
+ handleSubmit,
204
+ formState: {{ errors }},
205
+ }} = useForm<FormValues>({{
206
+ resolver: zodResolver(schema),
207
+ defaultValues,
208
+ }});
209
+
210
+ return (
211
+ <form onSubmit={{handleSubmit(onSubmit)}} className="space-y-4">
212
+ <div className="space-y-1">
213
+ <Label htmlFor="name">Name</Label>
214
+ <Input id="name" {{...register("name")}} />
215
+ {{errors.name && <p className="text-sm text-destructive">{{errors.name.message}}</p>}}
216
+ </div>
217
+ <Button type="submit">Submit</Button>
218
+ </form>
219
+ );
220
+ }}
221
+ """
222
+ form_file.write_text(content)
223
+ out.success(f"Created {form_file.relative_to(root)}")
224
+ out.info("Extend the zod schema with fields matching your API types from @/types/api")
225
+
226
+
227
+ @app.command()
228
+ def test(
229
+ ctx: typer.Context,
230
+ app_name: Annotated[str, typer.Argument(help="App name")],
231
+ name: Annotated[str, typer.Argument(help="Component or page name in PascalCase (e.g. App, CustomerList)")],
232
+ ) -> None:
233
+ """Scaffold a vitest test file.
234
+
235
+ Creates: src/pages/{Name}.test.tsx (or src/components/ based on where source exists).
236
+ """
237
+ actx: AppContext = ctx.obj
238
+ out = actx.output
239
+ root = actx.project_root
240
+
241
+ actx.validate_app(app_name)
242
+ app_dir = get_app_dir(root, app_name)
243
+ src_dir = app_dir / "src"
244
+
245
+ # Determine directory: follow the source file if it exists
246
+ source_dirs = ["pages", "components", "hooks"]
247
+ target_dir = src_dir / "pages" # default
248
+ for candidate in source_dirs:
249
+ source_file = src_dir / candidate / f"{name}.tsx"
250
+ if source_file.exists():
251
+ target_dir = src_dir / candidate
252
+ break
253
+
254
+ test_file = target_dir / f"{name}.test.tsx"
255
+ if test_file.exists():
256
+ out.error(f"Test file already exists: {test_file}")
257
+ raise typer.Exit(1)
258
+
259
+ test_file.parent.mkdir(parents=True, exist_ok=True)
260
+
261
+ content = f'''import {{ render, screen }} from "@testing-library/react";
262
+ import {{ describe, it, expect, vi }} from "vitest";
263
+ import {{ createWrapper }} from "@kodemeio/testing";
264
+ import {{ {name} }} from "@/{target_dir.relative_to(src_dir)}/{name}";
265
+
266
+ vi.mock("@/lib/client");
267
+ vi.mock("@kodemeio/offline");
268
+ vi.mock("sonner");
269
+
270
+ describe("{name}", () => {{
271
+ it("renders without crashing", () => {{
272
+ render(<{name} />, {{ wrapper: createWrapper() }});
273
+ expect(screen.getByRole("main")).toBeDefined();
274
+ }});
275
+
276
+ it("matches snapshot", () => {{
277
+ const {{ container }} = render(<{name} />, {{ wrapper: createWrapper() }});
278
+ expect(container.firstChild).toMatchSnapshot();
279
+ }});
280
+ }});
281
+ '''
282
+ test_file.write_text(content)
283
+ out.success(f"Created {test_file.relative_to(root)}")
284
+ out.info("Update the import path and add specific assertions for your component")
285
+
286
+
287
+ @app.command()
288
+ def component(
289
+ ctx: typer.Context,
290
+ app_name: Annotated[str, typer.Argument(help="App name")],
291
+ name: Annotated[str, typer.Argument(help="Component name in PascalCase")],
292
+ ) -> None:
293
+ """Scaffold a new component."""
294
+ actx: AppContext = ctx.obj
295
+ out = actx.output
296
+ root = actx.project_root
297
+
298
+ actx.validate_app(app_name)
299
+ app_dir = get_app_dir(root, app_name)
300
+
301
+ # Next.js: components/ at root, Vite: src/components/
302
+ comp_dir = app_dir / "components" if actx.is_nextjs(app_name) else app_dir / "src" / "components"
303
+ comp_file = comp_dir / f"{name}.tsx"
304
+ if comp_file.exists():
305
+ out.error(f"Component already exists: {comp_file}")
306
+ raise typer.Exit(1)
307
+
308
+ comp_file.parent.mkdir(parents=True, exist_ok=True)
309
+
310
+ content = f"""interface {name}Props {{
311
+ // TODO: define props
312
+ }}
313
+
314
+ export function {name}({{ }}: {name}Props) {{
315
+ return (
316
+ <div>
317
+ {name}
318
+ </div>
319
+ );
320
+ }}
321
+ """
322
+ comp_file.write_text(content)
323
+ out.success(f"Created {comp_file.relative_to(root)}")