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.
- kctl_react/__init__.py +3 -0
- kctl_react/__main__.py +5 -0
- kctl_react/cli.py +201 -0
- kctl_react/commands/__init__.py +0 -0
- kctl_react/commands/a11y.py +78 -0
- kctl_react/commands/affected.py +170 -0
- kctl_react/commands/apps.py +353 -0
- kctl_react/commands/build.py +376 -0
- kctl_react/commands/bundle_cmd.py +217 -0
- kctl_react/commands/cap.py +1465 -0
- kctl_react/commands/clean.py +76 -0
- kctl_react/commands/codegen.py +491 -0
- kctl_react/commands/compliance.py +587 -0
- kctl_react/commands/config_cmd.py +368 -0
- kctl_react/commands/dashboard.py +163 -0
- kctl_react/commands/deploy.py +318 -0
- kctl_react/commands/deps.py +792 -0
- kctl_react/commands/dev.py +96 -0
- kctl_react/commands/docker_cmd.py +73 -0
- kctl_react/commands/doctor.py +170 -0
- kctl_react/commands/e2e.py +343 -0
- kctl_react/commands/env.py +155 -0
- kctl_react/commands/i18n.py +310 -0
- kctl_react/commands/lint.py +306 -0
- kctl_react/commands/maintenance.py +308 -0
- kctl_react/commands/monitor_cmd.py +50 -0
- kctl_react/commands/observe.py +34 -0
- kctl_react/commands/packages.py +129 -0
- kctl_react/commands/perf.py +762 -0
- kctl_react/commands/pipeline.py +289 -0
- kctl_react/commands/pwa.py +193 -0
- kctl_react/commands/scaffold.py +323 -0
- kctl_react/commands/security.py +660 -0
- kctl_react/commands/skill_cmd.py +54 -0
- kctl_react/commands/state.py +254 -0
- kctl_react/commands/test_cmd.py +418 -0
- kctl_react/commands/ui_audit.py +889 -0
- kctl_react/core/__init__.py +0 -0
- kctl_react/core/analyzers.py +200 -0
- kctl_react/core/callbacks.py +70 -0
- kctl_react/core/compliance/__init__.py +3 -0
- kctl_react/core/compliance/api_check/__init__.py +3 -0
- kctl_react/core/compliance/api_check/checks/__init__.py +18 -0
- kctl_react/core/compliance/api_check/checks/endpoints.py +53 -0
- kctl_react/core/compliance/api_check/checks/envelope.py +44 -0
- kctl_react/core/compliance/api_check/checks/naming.py +60 -0
- kctl_react/core/compliance/api_check/checks/params.py +44 -0
- kctl_react/core/compliance/api_check/checks/requests.py +57 -0
- kctl_react/core/compliance/api_check/checks/types.py +55 -0
- kctl_react/core/compliance/api_check/hooks.py +133 -0
- kctl_react/core/compliance/api_check/matcher.py +55 -0
- kctl_react/core/compliance/api_check/schema.py +151 -0
- kctl_react/core/compliance/api_health/__init__.py +35 -0
- kctl_react/core/compliance/api_health/checks/__init__.py +9 -0
- kctl_react/core/compliance/api_health/checks/auth.py +72 -0
- kctl_react/core/compliance/api_health/checks/reachable.py +44 -0
- kctl_react/core/compliance/api_health/checks/response.py +55 -0
- kctl_react/core/compliance/api_health/checks/timing.py +38 -0
- kctl_react/core/compliance/api_health/client.py +99 -0
- kctl_react/core/compliance/api_health/sampler.py +16 -0
- kctl_react/core/compliance/checks/__init__.py +47 -0
- kctl_react/core/compliance/checks/api.py +101 -0
- kctl_react/core/compliance/checks/codegen.py +94 -0
- kctl_react/core/compliance/checks/darkmode.py +57 -0
- kctl_react/core/compliance/checks/errors.py +68 -0
- kctl_react/core/compliance/checks/features.py +66 -0
- kctl_react/core/compliance/checks/i18n_check.py +105 -0
- kctl_react/core/compliance/checks/imports.py +86 -0
- kctl_react/core/compliance/checks/navigation.py +62 -0
- kctl_react/core/compliance/checks/practices.py +122 -0
- kctl_react/core/compliance/checks/providers.py +85 -0
- kctl_react/core/compliance/checks/pwa.py +101 -0
- kctl_react/core/compliance/checks/responsive.py +47 -0
- kctl_react/core/compliance/checks/scripts.py +85 -0
- kctl_react/core/compliance/checks/shadcn.py +51 -0
- kctl_react/core/compliance/checks/structure.py +76 -0
- kctl_react/core/compliance/checks/testing.py +83 -0
- kctl_react/core/compliance/checks/theme.py +92 -0
- kctl_react/core/compliance/checks/ui_standard.py +185 -0
- kctl_react/core/compliance/checks/vite.py +83 -0
- kctl_react/core/compliance/engine.py +87 -0
- kctl_react/core/compliance/exceptions_map.py +15 -0
- kctl_react/core/compliance/fixes/__init__.py +33 -0
- kctl_react/core/compliance/fixes/codegen_fix.py +28 -0
- kctl_react/core/compliance/fixes/i18n_fix.py +61 -0
- kctl_react/core/compliance/fixes/imports_fix.py +36 -0
- kctl_react/core/compliance/fixes/structure_fix.py +20 -0
- kctl_react/core/compliance/fixes/theme_fix.py +29 -0
- kctl_react/core/compliance/models.py +106 -0
- kctl_react/core/config.py +201 -0
- kctl_react/core/discovery.py +185 -0
- kctl_react/core/exceptions.py +17 -0
- kctl_react/core/git.py +146 -0
- kctl_react/core/history.py +121 -0
- kctl_react/core/output.py +5 -0
- kctl_react/core/plugins.py +13 -0
- kctl_react/core/runner.py +34 -0
- kctl_react/py.typed +0 -0
- kctl_react-0.6.2.dist-info/METADATA +17 -0
- kctl_react-0.6.2.dist-info/RECORD +102 -0
- kctl_react-0.6.2.dist-info/WHEEL +4 -0
- 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)}")
|