codz-cli 0.0.0-stage → 0.1.0

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 (117) hide show
  1. package/BETA.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +523 -2
  4. package/assets/gradle-wrapper/README.txt +2 -0
  5. package/assets/gradle-wrapper/gradle-wrapper.jar +0 -0
  6. package/assets/gradle-wrapper/gradlew +251 -0
  7. package/assets/gradle-wrapper/gradlew.bat +94 -0
  8. package/bin/codz.js +33 -0
  9. package/dist/adapters/android.js +321 -0
  10. package/dist/adapters/androidSetup.js +277 -0
  11. package/dist/adapters/common.js +114 -0
  12. package/dist/adapters/dartGen.js +83 -0
  13. package/dist/adapters/flutter.js +291 -0
  14. package/dist/adapters/flutterGen.js +204 -0
  15. package/dist/adapters/flutterScan.js +312 -0
  16. package/dist/adapters/flutterSetup.js +252 -0
  17. package/dist/adapters/flutterStyles.js +130 -0
  18. package/dist/adapters/generic.js +20 -0
  19. package/dist/adapters/getx.js +423 -0
  20. package/dist/adapters/index.js +28 -0
  21. package/dist/adapters/ios.js +440 -0
  22. package/dist/adapters/kotlinGen.js +110 -0
  23. package/dist/adapters/models.js +116 -0
  24. package/dist/adapters/nextjs.js +243 -0
  25. package/dist/adapters/nextjsGen.js +285 -0
  26. package/dist/adapters/nextjsScan.js +364 -0
  27. package/dist/adapters/nextjsSetup.js +119 -0
  28. package/dist/adapters/react.js +89 -0
  29. package/dist/adapters/reactNative.js +109 -0
  30. package/dist/adapters/reactNativeSetup.js +236 -0
  31. package/dist/adapters/reactQuery.js +89 -0
  32. package/dist/adapters/reactSetup.js +150 -0
  33. package/dist/adapters/source.js +165 -0
  34. package/dist/cli.js +1175 -0
  35. package/dist/core/cache.js +76 -0
  36. package/dist/core/changeset.js +404 -0
  37. package/dist/core/commands.js +239 -0
  38. package/dist/core/config.js +45 -0
  39. package/dist/core/diff.js +143 -0
  40. package/dist/core/events.js +41 -0
  41. package/dist/core/fsutil.js +122 -0
  42. package/dist/core/git.js +36 -0
  43. package/dist/core/jsonEdit.js +113 -0
  44. package/dist/core/merge.js +159 -0
  45. package/dist/core/proc.js +23 -0
  46. package/dist/core/safe.js +75 -0
  47. package/dist/core/templates.js +100 -0
  48. package/dist/core/tsAst.js +112 -0
  49. package/dist/core/types.js +12 -0
  50. package/dist/core/validateConfig.js +172 -0
  51. package/dist/core/workflow.js +72 -0
  52. package/dist/core/workspace.js +80 -0
  53. package/dist/core/yaml.js +126 -0
  54. package/dist/core/yamlDoc.js +367 -0
  55. package/dist/detector.js +95 -0
  56. package/dist/input/apiSpec.js +395 -0
  57. package/dist/input/curl.js +259 -0
  58. package/dist/input/endpoint.js +220 -0
  59. package/dist/input/har.js +120 -0
  60. package/dist/input/infer.js +174 -0
  61. package/dist/input/naming.js +145 -0
  62. package/dist/input/paste.js +130 -0
  63. package/dist/skills/androidSkeleton.js +292 -0
  64. package/dist/skills/androidTests.js +221 -0
  65. package/dist/skills/androidUi.js +561 -0
  66. package/dist/skills/androidViews.js +536 -0
  67. package/dist/skills/apiAdd.js +99 -0
  68. package/dist/skills/apiDiff.js +271 -0
  69. package/dist/skills/apiEdit.js +547 -0
  70. package/dist/skills/apiImport.js +127 -0
  71. package/dist/skills/apiList.js +35 -0
  72. package/dist/skills/apiSync.js +221 -0
  73. package/dist/skills/auth.js +1304 -0
  74. package/dist/skills/completion.js +142 -0
  75. package/dist/skills/config.js +93 -0
  76. package/dist/skills/create.js +358 -0
  77. package/dist/skills/doctor.js +304 -0
  78. package/dist/skills/doctorFix.js +214 -0
  79. package/dist/skills/explain.js +61 -0
  80. package/dist/skills/feature.js +418 -0
  81. package/dist/skills/featureEdit.js +225 -0
  82. package/dist/skills/featureLinks.js +179 -0
  83. package/dist/skills/init.js +66 -0
  84. package/dist/skills/iosUi.js +468 -0
  85. package/dist/skills/locale.js +335 -0
  86. package/dist/skills/mcp.js +175 -0
  87. package/dist/skills/mock.js +482 -0
  88. package/dist/skills/module.js +23 -0
  89. package/dist/skills/plans.js +66 -0
  90. package/dist/skills/prefill.js +84 -0
  91. package/dist/skills/reactNativeUi.js +388 -0
  92. package/dist/skills/reactUi.js +352 -0
  93. package/dist/skills/report.js +75 -0
  94. package/dist/skills/route.js +230 -0
  95. package/dist/skills/screen.js +597 -0
  96. package/dist/skills/screenForm.js +410 -0
  97. package/dist/skills/setup.js +28 -0
  98. package/dist/skills/setupDoctor.js +226 -0
  99. package/dist/skills/templatesCmd.js +32 -0
  100. package/dist/skills/testgen.js +331 -0
  101. package/dist/skills/theme.js +388 -0
  102. package/dist/starters/auth.js +144 -0
  103. package/dist/starters/commands.js +283 -0
  104. package/dist/starters/engine.js +315 -0
  105. package/dist/starters/home.js +42 -0
  106. package/dist/starters/http.js +93 -0
  107. package/dist/starters/manifest.js +285 -0
  108. package/dist/starters/paths.js +70 -0
  109. package/dist/starters/publish.js +150 -0
  110. package/dist/starters/registry.js +258 -0
  111. package/dist/starters/sign.js +72 -0
  112. package/dist/starters/source.js +98 -0
  113. package/dist/starters/strictJson.js +148 -0
  114. package/dist/starters/tar.js +180 -0
  115. package/docs/COMMANDS.md +527 -0
  116. package/examples/openapi.yaml +319 -0
  117. package/package.json +57 -4
@@ -0,0 +1,179 @@
1
+ // Connects the screens feature add made: tapping a row of the list opens the
2
+ // detail screen, and a "New" button opens the create form. Each function edits
3
+ // the list screen screen add wrote (a known shape) and returns null when the
4
+ // shape is not there, so a project with its own templates is left alone.
5
+ import { posix } from 'node:path';
6
+ const goPath = (route, param, expr) => route.replace(`:${param}`, expr);
7
+ /** Next.js server page: rows become links, a "New" link under the heading. */
8
+ export function linkNextList(text, t) {
9
+ let out = text;
10
+ let changed = false;
11
+ if (t.detail && t.idField) {
12
+ const re = /<li key=\{([^}]+)\}>(\{[^<]+\})<\/li>/;
13
+ if (re.test(out)) {
14
+ out = out.replace(re, (_m, key, body) => `<li key={${key}}><Link href={\`${goPath(t.detail.route, t.detail.param, `\${item.${t.idField}}`)}\`}>${body}</Link></li>`);
15
+ changed = true;
16
+ }
17
+ }
18
+ if (t.create) {
19
+ const re = /(\n(\s*)<h1>[^<]*<\/h1>)/;
20
+ if (re.test(out)) {
21
+ out = out.replace(re, (_m, h, indent) => `${h}\n${indent}<p>\n${indent} <Link href="${t.create.route}">New ${t.noun}</Link>\n${indent}</p>`);
22
+ changed = true;
23
+ }
24
+ }
25
+ if (!changed)
26
+ return null;
27
+ if (!/from ['"]next\/link['"]/.test(out))
28
+ out = `import Link from 'next/link';\n${out}`;
29
+ return out;
30
+ }
31
+ /** React web (React Router): rows become links, a "New" link under the heading. */
32
+ export function linkReactList(text, t) {
33
+ const out = linkNextList(text, t);
34
+ if (!out)
35
+ return null;
36
+ return out.replace(/import Link from 'next\/link';\n/, '').replace(/^/, (s) => (/from 'react-router-dom'/.test(out) ? s : `import { Link } from 'react-router-dom';\n`)).replace(/<Link href=/g, '<Link to=');
37
+ }
38
+ /** React Native (React Navigation or Expo Router): rows open the detail screen, a header button opens the form. */
39
+ export function linkRnList(text, t, c, screenFile) {
40
+ const expoRouter = c.router === 'expo-router';
41
+ let out = text;
42
+ let changed = false;
43
+ const go = (route, screen, params) => (expoRouter ? `router.push(${params ? `\`${route}\`` : `'${route}'`})` : `navigation.navigate('${screen}'${params ? `, ${params}` : ''})`);
44
+ if (t.detail && t.idField) {
45
+ const re = /renderItem=\{\(\{ item \}\) => <Text style=\{styles\.row\}>(\{[^<]+\})<\/Text>\}/;
46
+ if (re.test(out)) {
47
+ const call = expoRouter ? go(goPath(t.detail.route, t.detail.param, `\${item.${t.idField}}`), t.detail.screen, 'x') : go('', t.detail.screen, `{ ${t.detail.param}: String(item.${t.idField}) }`);
48
+ out = out.replace(re, (_m, body) => `renderItem={({ item }) => (\n <Pressable accessibilityRole="button" onPress={() => ${call}}>\n <Text style={styles.row}>${body}</Text>\n </Pressable>\n )}`);
49
+ changed = true;
50
+ }
51
+ }
52
+ if (t.create) {
53
+ const re = /(\n(\s*)ListEmptyComponent=)/;
54
+ if (re.test(out)) {
55
+ out = out.replace(re, (_m, all, indent) => `\n${indent}ListHeaderComponent={<Button title="New ${t.noun}" onPress={() => ${go(t.create.route, t.create.screen)}} />}${all}`);
56
+ changed = true;
57
+ }
58
+ }
59
+ if (!changed)
60
+ return null;
61
+ // Imports and the navigation hook.
62
+ out = out.replace(/import \{ ([^}]*) \} from 'react-native';/, (_m, names) => {
63
+ const set = new Set(names.split(',').map((s) => s.trim()).filter(Boolean));
64
+ set.add('Pressable');
65
+ set.add('Button');
66
+ return `import { ${[...set].sort().join(', ')} } from 'react-native';`;
67
+ });
68
+ if (expoRouter) {
69
+ if (!/from 'expo-router'/.test(out))
70
+ out = out.replace(/(import [^\n]+ from 'react-native';\n)/, `$1import { router } from 'expo-router';\n`);
71
+ }
72
+ else {
73
+ const nav = c.navigatorFile ? posix.relative(posix.dirname(screenFile), c.navigatorFile.replace(/\.(tsx?|jsx?)$/, '')) : '../navigation/RootNavigator';
74
+ const navImport = nav.startsWith('.') ? nav : `./${nav}`;
75
+ out = out.replace(/(import [^\n]+ from 'react-native';\n)/, `$1import { useNavigation } from '@react-navigation/native';\nimport type { NativeStackNavigationProp } from '@react-navigation/native-stack';\nimport type { RootStackParamList } from '${navImport}';\n`);
76
+ out = out.replace(/(export default function \w+\(\) \{\n)/, `$1 const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();\n`);
77
+ }
78
+ return out;
79
+ }
80
+ /** Flutter (go_router, GetX routes or Navigator): rows open the detail page, a floating button opens the form; the list reloads after the form. */
81
+ export function linkFlutterList(text, t, c) {
82
+ const router = c.router;
83
+ // A GetX app navigates by GetX routes even when the router convention still says navigator.
84
+ if (router === 'getx' || c.di === 'getx' || c.stateManagement === 'getx')
85
+ return linkGetxList(text, t);
86
+ if (router !== 'go_router' && router !== 'navigator')
87
+ return null;
88
+ // Navigator named routes cannot carry a path parameter, so the detail page is pushed with its constructor.
89
+ if (router === 'navigator' && t.detail && !(t.detail.widget && t.detail.dartImport))
90
+ t = { ...t, detail: undefined };
91
+ const push = (path) => (router === 'go_router' ? `context.push(${path})` : `Navigator.pushNamed(context, ${path})`);
92
+ const open = (d, id) => (router === 'navigator' ? `Navigator.push(context, MaterialPageRoute(builder: (_) => ${d.widget}(${d.param}: '\${${id}}')))` : push(`'${goPath(d.route, d.param, `\${${id}}`)}'`));
93
+ let out = text;
94
+ let changed = false;
95
+ const reload = /void _reload\(\)/.test(out) ? '.then((_) => _reload())' : '';
96
+ if (t.detail && t.idField) {
97
+ const re = /ListTile\(title: (Text\([^\n]*?\))\)(?=,?\n)/;
98
+ if (re.test(out)) {
99
+ const call = open(t.detail, `items[index].${t.idField}`);
100
+ out = out.replace(re, (_m, title) => `ListTile(\n title: ${title},\n onTap: () => ${call}${reload},\n )`);
101
+ changed = true;
102
+ }
103
+ }
104
+ if (t.create) {
105
+ const re = /(\n(\s*)appBar: AppBar\([^\n]*\),)/;
106
+ if (re.test(out)) {
107
+ out = out.replace(re, (_m, all, indent) => `${all}\n${indent}floatingActionButton: FloatingActionButton(\n${indent} tooltip: 'New ${t.noun}',\n${indent} onPressed: () => ${push(`'${t.create.route}'`)}${reload},\n${indent} child: const Icon(Icons.add),\n${indent}),`);
108
+ changed = true;
109
+ }
110
+ }
111
+ if (!changed)
112
+ return null;
113
+ if (router === 'go_router' && !/package:go_router\/go_router\.dart/.test(out))
114
+ out = out.replace(/(import 'package:flutter\/material\.dart';\n)/, `$1import 'package:go_router/go_router.dart';\n`);
115
+ if (router === 'navigator' && t.detail?.dartImport && !out.includes(`'${t.detail.dartImport}'`))
116
+ out = out.replace(/(import 'package:flutter\/material\.dart';\n)/, `$1import '${t.detail.dartImport}';\n`);
117
+ return out;
118
+ }
119
+ /**
120
+ * GetX module view: rows open the detail module with its id in Get.parameters, a floating button opens
121
+ * the create module; the list reloads when they come back. Modules keep their own route names
122
+ * (Routes.PRODUCT_DETAIL = /product-detail), so navigation goes by the route constant.
123
+ */
124
+ function linkGetxList(text, t) {
125
+ let out = text;
126
+ let changed = false;
127
+ const loader = /onRefresh: controller\.(\w+),/.exec(out)?.[1];
128
+ // Get.toNamed returns a nullable Future.
129
+ const reload = loader ? `?.then((_) => controller.${loader}())` : '';
130
+ if (t.detail?.routeConst && t.idField) {
131
+ const re = /return ListTile\(title: (Text\([^\n]*?\))\);/;
132
+ if (re.test(out)) {
133
+ const call = `Get.toNamed(Routes.${t.detail.routeConst}, parameters: {'${t.detail.param}': '\${item.${t.idField}}'})`;
134
+ out = out.replace(re, (_m, title) => `return ListTile(\n title: ${title},\n onTap: () => ${call}${reload},\n );`);
135
+ changed = true;
136
+ }
137
+ }
138
+ if (t.create?.routeConst) {
139
+ const re = /(\n(\s*)appBar: AppBar\([^\n]*\),)/;
140
+ if (re.test(out)) {
141
+ out = out.replace(re, (_m, all, indent) => `${all}\n${indent}floatingActionButton: FloatingActionButton(\n${indent} tooltip: 'New ${t.noun}',\n${indent} onPressed: () => Get.toNamed(Routes.${t.create.routeConst})${reload},\n${indent} child: const Icon(Icons.add),\n${indent}),`);
142
+ changed = true;
143
+ }
144
+ }
145
+ if (!changed)
146
+ return null;
147
+ if (!/package:get\/get\.dart/.test(out))
148
+ out = out.replace(/(import 'package:flutter\/material\.dart';\n)/, `$1import 'package:get/get.dart';\n`);
149
+ if (t.routesImport && !out.includes(t.routesImport))
150
+ out = out.replace(/(import 'package:get\/get\.dart';\n)/, `$1\nimport '${t.routesImport}';\n`);
151
+ return out;
152
+ }
153
+ /** Android Compose: the list screen takes onOpen/onNew callbacks; the NavHost passes navigation into them. */
154
+ export function linkAndroidList(screen, navHost, listName, t) {
155
+ let s = screen;
156
+ const sig = new RegExp(`fun ${listName}\\(viewModel: `);
157
+ if (!sig.test(s))
158
+ return null;
159
+ const params = [t.detail && t.idField ? 'onOpen: (String) -> Unit = {}' : '', t.create ? 'onNew: () -> Unit = {}' : ''].filter(Boolean);
160
+ if (!params.length)
161
+ return null;
162
+ s = s.replace(sig, `fun ${listName}(${params.join(', ')}, viewModel: `);
163
+ if (t.detail && t.idField) {
164
+ s = s.replace(/Text\(item\.(\w+)\.toString\(\), Modifier\.fillMaxWidth\(\)\.padding\(16\.dp\)\)/, `Text(item.$1.toString(), Modifier.fillMaxWidth().clickable { onOpen(item.${t.idField}.toString()) }.padding(16.dp))`);
165
+ if (!/import androidx\.compose\.foundation\.clickable/.test(s))
166
+ s = s.replace(/(import androidx\.compose\.foundation\.layout\.Box\n)/, `import androidx.compose.foundation.clickable\n$1`);
167
+ }
168
+ if (t.create) {
169
+ s = s.replace(/Scaffold \{ padding ->/, `Scaffold(\n floatingActionButton = { FloatingActionButton(onClick = onNew) { Text("+") } },\n ) { padding ->`);
170
+ if (!/import androidx\.compose\.material3\.FloatingActionButton/.test(s))
171
+ s = s.replace(/(import androidx\.compose\.material3\.HorizontalDivider\n)/, `import androidx.compose.material3.FloatingActionButton\n$1`);
172
+ }
173
+ const call = new RegExp(`(\\n\\s*)${listName}\\(\\)`);
174
+ if (!call.test(navHost))
175
+ return null;
176
+ const args = [t.detail && t.idField ? `onOpen = { id -> navController.navigate("${t.detail.route.replace(/^\//, '').replace(`:${t.detail.param}`, '$id')}") }` : '', t.create ? `onNew = { navController.navigate("${t.create.route.replace(/^\//, '')}") }` : ''].filter(Boolean);
177
+ const nav = navHost.replace(call, `$1${listName}(${args.join(', ')})`);
178
+ return { screen: s, navHost: nav };
179
+ }
@@ -0,0 +1,66 @@
1
+ // init: detect the framework and scan. The CLI then either writes
2
+ // codz.config.yaml directly, or, for a project with no API layer yet,
3
+ // offers `setup` so the config and the new files land in one change.
4
+ import { RN_CONVENTION_DOCS } from "../adapters/reactNative.js";
5
+ import { REACT_CONVENTION_DOCS } from "../adapters/react.js";
6
+ import { IOS_CONVENTION_DOCS } from "../adapters/ios.js";
7
+ import { ANDROID_CONVENTION_DOCS } from "../adapters/android.js";
8
+ import { join, resolve } from 'node:path';
9
+ import { ADAPTERS } from "../adapters/index.js";
10
+ import { FLUTTER_CONVENTION_DOCS } from "../adapters/flutter.js";
11
+ import { NEXT_CONVENTION_DOCS } from "../adapters/nextjs.js";
12
+ import { loadMap } from "../core/cache.js";
13
+ import { CONFIG_FILE, loadConfig, renderConfig } from "../core/config.js";
14
+ import { exists, readText } from "../core/fsutil.js";
15
+ import { isGitRepo } from "../core/git.js";
16
+ import { ToolError } from "../core/types.js";
17
+ import { detect } from "../detector.js";
18
+ import { writeFileSync } from 'node:fs';
19
+ export async function detectProject(start, opts) {
20
+ let root = resolve(start, opts.root ?? '.');
21
+ const found = detect(root);
22
+ const supported = found.filter((d) => d.supported);
23
+ if (!opts.root && supported.length > 1) {
24
+ throw new ToolError('many_projects', `Found ${supported.length} projects here:\n${supported.map((d) => ` ${d.root} (${d.label})`).join('\n')}`, 'Run `codz init --root <folder>` for the one you want; each project gets its own codz.config.yaml.');
25
+ }
26
+ const pick = supported[0] ?? found[0];
27
+ let adapter = ADAPTERS.generic;
28
+ if (pick) {
29
+ root = resolve(root, pick.root);
30
+ adapter = ADAPTERS[pick.framework];
31
+ }
32
+ const cfg = loadConfig(root);
33
+ if (cfg.exists && cfg.framework && !opts.force)
34
+ adapter = ADAPTERS[cfg.framework] ?? adapter;
35
+ const conventions = cfg.exists && !opts.force ? cfg.conventions : {};
36
+ const { map } = await loadMap(root, adapter, conventions, { force: true });
37
+ return { root, adapter, label: pick?.label ?? 'unknown', map, configExists: cfg.exists };
38
+ }
39
+ export function conventionDocs(adapter) {
40
+ return adapter.id === 'nextjs' ? NEXT_CONVENTION_DOCS : adapter.id === 'flutter' ? FLUTTER_CONVENTION_DOCS : adapter.id === 'react-native' ? RN_CONVENTION_DOCS : adapter.id === 'react' ? REACT_CONVENTION_DOCS : adapter.id === 'ios' ? IOS_CONVENTION_DOCS : adapter.id === 'android' ? ANDROID_CONVENTION_DOCS : {};
41
+ }
42
+ /** The config file and .gitignore change that init makes, as file contents. */
43
+ export function initFiles(root, adapter, conventions) {
44
+ const files = new Map();
45
+ files.set(CONFIG_FILE, renderConfig(adapter.id, conventions, conventionDocs(adapter)));
46
+ const gi = join(root, '.gitignore');
47
+ if (isGitRepo(root) || exists(gi)) {
48
+ const text = readText(gi) ?? '';
49
+ if (!/^\/?\.codz\/?$/m.test(text))
50
+ files.set('.gitignore', `${text}${text && !text.endsWith('\n') ? '\n' : ''}# codz cache, backups and history\n.codz/\n`);
51
+ }
52
+ return files;
53
+ }
54
+ /** Writes init's files directly (no API layer to create, or the user skipped setup). */
55
+ export function writeInitFiles(root, files, keepConfig) {
56
+ let configWritten = false;
57
+ for (const [path, content] of files) {
58
+ if (path === CONFIG_FILE && keepConfig)
59
+ continue;
60
+ writeFileSync(join(root, path), content);
61
+ if (path === CONFIG_FILE)
62
+ configWritten = true;
63
+ }
64
+ return { configWritten, gitignoreUpdated: files.has('.gitignore') };
65
+ }
66
+ export { CONFIG_FILE };