codz-cli 0.0.0-stage → 0.1.2

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 (118) hide show
  1. package/BETA.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +525 -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 +638 -0
  77. package/dist/skills/createDemo.js +1026 -0
  78. package/dist/skills/doctor.js +304 -0
  79. package/dist/skills/doctorFix.js +214 -0
  80. package/dist/skills/explain.js +61 -0
  81. package/dist/skills/feature.js +418 -0
  82. package/dist/skills/featureEdit.js +225 -0
  83. package/dist/skills/featureLinks.js +179 -0
  84. package/dist/skills/init.js +66 -0
  85. package/dist/skills/iosUi.js +468 -0
  86. package/dist/skills/locale.js +335 -0
  87. package/dist/skills/mcp.js +175 -0
  88. package/dist/skills/mock.js +482 -0
  89. package/dist/skills/module.js +23 -0
  90. package/dist/skills/plans.js +66 -0
  91. package/dist/skills/prefill.js +84 -0
  92. package/dist/skills/reactNativeUi.js +388 -0
  93. package/dist/skills/reactUi.js +352 -0
  94. package/dist/skills/report.js +75 -0
  95. package/dist/skills/route.js +230 -0
  96. package/dist/skills/screen.js +597 -0
  97. package/dist/skills/screenForm.js +410 -0
  98. package/dist/skills/setup.js +28 -0
  99. package/dist/skills/setupDoctor.js +226 -0
  100. package/dist/skills/templatesCmd.js +32 -0
  101. package/dist/skills/testgen.js +331 -0
  102. package/dist/skills/theme.js +388 -0
  103. package/dist/starters/auth.js +144 -0
  104. package/dist/starters/commands.js +283 -0
  105. package/dist/starters/engine.js +315 -0
  106. package/dist/starters/home.js +42 -0
  107. package/dist/starters/http.js +93 -0
  108. package/dist/starters/manifest.js +285 -0
  109. package/dist/starters/paths.js +70 -0
  110. package/dist/starters/publish.js +150 -0
  111. package/dist/starters/registry.js +258 -0
  112. package/dist/starters/sign.js +72 -0
  113. package/dist/starters/source.js +98 -0
  114. package/dist/starters/strictJson.js +148 -0
  115. package/dist/starters/tar.js +180 -0
  116. package/docs/COMMANDS.md +527 -0
  117. package/examples/openapi.yaml +319 -0
  118. package/package.json +57 -4
@@ -0,0 +1,114 @@
1
+ // Scanning helpers shared by adapters.
2
+ import { extname, join } from 'node:path';
3
+ import { readJson, readText, walk } from "../core/fsutil.js";
4
+ import { MAP_SCHEMA_VERSION } from "../core/types.js";
5
+ export function emptyMap(framework, adapter) {
6
+ return {
7
+ schema: MAP_SCHEMA_VERSION,
8
+ framework,
9
+ adapter,
10
+ conventions: {},
11
+ routes: [],
12
+ endpoints: [],
13
+ models: [],
14
+ locales: { languages: [], files: [], keyCount: 0 },
15
+ baseUrls: [],
16
+ stats: { files: 0, byExtension: {} },
17
+ notes: [],
18
+ };
19
+ }
20
+ export function fileStats(files) {
21
+ const byExtension = {};
22
+ for (const f of files) {
23
+ const ext = extname(f) || '(none)';
24
+ byExtension[ext] = (byExtension[ext] ?? 0) + 1;
25
+ }
26
+ const sorted = Object.fromEntries(Object.entries(byExtension).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])));
27
+ return { files: files.length, byExtension: sorted };
28
+ }
29
+ const LANG = /^[a-z]{2,3}(?:[-_][A-Za-z]{2,4})?$/;
30
+ function countKeys(value) {
31
+ if (!value || typeof value !== 'object' || Array.isArray(value))
32
+ return 0;
33
+ let n = 0;
34
+ for (const [k, v] of Object.entries(value)) {
35
+ if (k.startsWith('@'))
36
+ continue; // ARB metadata
37
+ if (v && typeof v === 'object' && !Array.isArray(v))
38
+ n += countKeys(v);
39
+ else
40
+ n++;
41
+ }
42
+ return n;
43
+ }
44
+ /** Finds locale files: <dir>/<lang>.json in common folders, and Flutter ARB files. */
45
+ export function scanLocales(root, files) {
46
+ const localeFiles = [];
47
+ for (const f of files) {
48
+ const parts = f.split('/');
49
+ const base = parts[parts.length - 1];
50
+ const dir = parts.slice(0, -1).join('/');
51
+ const json = base.match(/^([a-z]{2,3}(?:[-_][A-Za-z]{2,4})?)\.json$/);
52
+ if (json && /(^|\/)(messages|locales|i18n|lang|translations)(\/|$)/.test(dir) && LANG.test(json[1])) {
53
+ localeFiles.push({ file: f, lang: json[1] });
54
+ continue;
55
+ }
56
+ const arb = base.match(/^(?:[a-z0-9_]+_)?([a-z]{2,3}(?:_[A-Z]{2})?)\.arb$/);
57
+ if (arb)
58
+ localeFiles.push({ file: f, lang: arb[1] });
59
+ }
60
+ if (!localeFiles.length)
61
+ return { languages: [], files: [], keyCount: 0 };
62
+ // Keep the folder with the most locale files.
63
+ const byDir = new Map();
64
+ for (const l of localeFiles) {
65
+ const d = l.file.split('/').slice(0, -1).join('/');
66
+ byDir.set(d, [...(byDir.get(d) ?? []), l]);
67
+ }
68
+ const [, chosen] = [...byDir.entries()].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]))[0];
69
+ const languages = chosen.map((l) => l.lang).sort();
70
+ let keyCount = 0;
71
+ for (const l of chosen)
72
+ keyCount = Math.max(keyCount, countKeys(readJson(join(root, l.file))));
73
+ const def = languages.includes('en') ? 'en' : languages[0];
74
+ return { default: def, languages, files: chosen.map((l) => l.file).sort(), keyCount };
75
+ }
76
+ /** Reads URL-valued variables from .env files (names only for anything that is not a URL). */
77
+ export function scanEnvBaseUrls(root, files) {
78
+ const out = [];
79
+ const envFiles = files.filter((f) => /(^|\/)\.env(\.[\w.-]+)?$/.test(f) && !f.includes('/'));
80
+ for (const f of envFiles) {
81
+ const text = readText(join(root, f)) ?? '';
82
+ for (const line of text.split(/\r?\n/)) {
83
+ const m = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*['"]?(https?:\/\/[^'"\s#]+)['"]?/);
84
+ if (m)
85
+ out.push({ name: m[1], value: m[2], file: f });
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ /** walk() skips dot-files' folders but not dot files; env files need an explicit listing. */
91
+ export function listRootEnvFiles(root) {
92
+ return walk(root, '.', { exts: ['.env', '.local', '.example', '.development', '.production', '.staging', '.test'] }).filter((f) => !f.includes('/') && f.startsWith('.env'));
93
+ }
94
+ /**
95
+ * The path to call through an HTTP client that already has a base URL (Dio, Retrofit, a fetch
96
+ * helper, an axios instance), for an endpoint that came from an API spec. The spec's host is a
97
+ * stand-in for the app's own base URL (often another environment: test, staging), so it is never
98
+ * written into the call. The spec's path prefix (/v1) stays unless the client's base URL already
99
+ * ends with it. Returns null for other inputs (cURL, HAR, Postman), which keep their host rules.
100
+ */
101
+ export function specRelativePath(spec, clientBaseUrl) {
102
+ if (spec.serverPrefix === undefined)
103
+ return null;
104
+ let clientPrefix = '';
105
+ try {
106
+ if (clientBaseUrl)
107
+ clientPrefix = new URL(clientBaseUrl).pathname.replace(/\/$/, '');
108
+ }
109
+ catch {
110
+ // Not a literal URL: nothing to strip.
111
+ }
112
+ const p = spec.path;
113
+ return clientPrefix && (p === clientPrefix || p.startsWith(`${clientPrefix}/`)) ? p.slice(clientPrefix.length) || '/' : p;
114
+ }
@@ -0,0 +1,83 @@
1
+ // Shared Dart code generation: types, JSON reading and writing expressions,
2
+ // string literals. Used by the Flutter adapter and its style variants
3
+ // (freezed models, Retrofit clients, Cubits).
4
+ import { DART_RESERVED, identifier } from "../input/naming.js";
5
+ export function dartType(t, g) {
6
+ switch (t.kind) {
7
+ case 'string':
8
+ return 'String';
9
+ case 'date':
10
+ return 'DateTime';
11
+ case 'int':
12
+ return 'int';
13
+ case 'double':
14
+ return 'double';
15
+ case 'bool':
16
+ return 'bool';
17
+ case 'unknown':
18
+ return 'dynamic';
19
+ case 'object':
20
+ return g.cls(t.model);
21
+ case 'array':
22
+ return `List<${dartType(t.item, g)}>`;
23
+ }
24
+ }
25
+ /** Expression that reads a JSON value of type t from `v` (a dynamic). */
26
+ export function fromJsonExpr(t, v, nullable, g) {
27
+ const nn = nullable ? '?' : '';
28
+ switch (t.kind) {
29
+ case 'string':
30
+ case 'int':
31
+ case 'bool':
32
+ return `${v} as ${dartType(t, g)}${nn}`;
33
+ case 'double':
34
+ return nullable ? `(${v} as num?)?.toDouble()` : `(${v} as num).toDouble()`;
35
+ case 'date':
36
+ return nullable ? `${v} == null ? null : DateTime.parse(${v} as String)` : `DateTime.parse(${v} as String)`;
37
+ case 'unknown':
38
+ return v;
39
+ case 'object':
40
+ return nullable ? `${v} == null ? null : ${g.cls(t.model)}.fromJson(${v} as Map<String, dynamic>)` : `${g.cls(t.model)}.fromJson(${v} as Map<String, dynamic>)`;
41
+ case 'array': {
42
+ const item = fromJsonExpr(t.item, 'e', false, g);
43
+ if (t.item.kind === 'unknown')
44
+ return `${v} as List<dynamic>${nn}`;
45
+ return nullable ? `(${v} as List<dynamic>?)?.map((e) => ${item}).toList()` : `(${v} as List<dynamic>).map((e) => ${item}).toList()`;
46
+ }
47
+ }
48
+ }
49
+ export function toJsonExpr(t, v, nullable) {
50
+ const q = nullable ? '?' : '';
51
+ switch (t.kind) {
52
+ case 'date':
53
+ return `${v}${q}.toIso8601String()`;
54
+ case 'object':
55
+ return `${v}${q}.toJson()`;
56
+ case 'array': {
57
+ if (t.item.kind === 'object' || t.item.kind === 'date' || t.item.kind === 'array')
58
+ return `${v}${q}.map((e) => ${toJsonExpr(t.item, 'e', false)}).toList()`;
59
+ return v;
60
+ }
61
+ default:
62
+ return v;
63
+ }
64
+ }
65
+ export function dq(s) {
66
+ return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\$/g, '\\$').replace(/\r/g, '\\r').replace(/\n/g, '\\n')}'`;
67
+ }
68
+ export function fieldName(f) {
69
+ return identifier(f.jsonName, DART_RESERVED);
70
+ }
71
+ export function dartPathLiteral(path) {
72
+ // {id} → $id, or ${id} when followed by an identifier character.
73
+ // Literal text is escaped (\\, ', $); only {param} placeholders become Dart interpolation.
74
+ const esc = (t) => t.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\$/g, '\\$').replace(/[\r\n]/g, '');
75
+ const parts = path.split(/(\{\w+\})/);
76
+ const body = parts.map((part, i) => {
77
+ const m = /^\{(\w+)\}$/.exec(part);
78
+ if (!m)
79
+ return esc(part);
80
+ return /^[A-Za-z0-9_]/.test(parts[i + 1] ?? '') ? `\${${m[1]}}` : `$${m[1]}`;
81
+ }).join('');
82
+ return `'${body}'`;
83
+ }
@@ -0,0 +1,291 @@
1
+ // Flutter adapter. Supported setup: Dio, called from repository/service
2
+ // classes or through Retrofit clients; models with hand-written fromJson,
3
+ // json_serializable or freezed; GetX, Riverpod providers or get_it for
4
+ // registration; go_router or auto_route for routes; ARB locales.
5
+ // Not yet: the http package, Chopper.
6
+ //
7
+ // Dart code is edited with brace-aware text edits at well-defined insertion
8
+ // points (end of class, import block, registration list). `dart format` and
9
+ // `dart analyze` run afterwards when the Dart SDK is installed, and a new
10
+ // analyzer error reverts the change.
11
+ import { join } from 'node:path';
12
+ import { detectEol, exists, readText, walk, withEol } from "../core/fsutil.js";
13
+ import { ToolError } from "../core/types.js";
14
+ import { camel, singular, snake, typeName } from "../input/naming.js";
15
+ import { specRelativePath } from "./common.js";
16
+ import { referencedModels, resolveModels } from "./models.js";
17
+ import { flutterArchitectures, planFlutterSetup } from "./flutterSetup.js";
18
+ import { planGetxModule, planGetxWire } from "./getx.js";
19
+ import { matchBrace } from "./source.js";
20
+ import { dartType } from "./dartGen.js";
21
+ import { applyTemplate } from "../core/templates.js";
22
+ import { newRetrofitClient, renderRetrofitMethod } from "./flutterStyles.js";
23
+ import { scan, base, pubspecDeps } from "./flutterScan.js";
24
+ import { addDartImport, addToClass, importUri, indentStyled, insertGetxRegistration, r0, renderMethod, renderModel, resourceOf } from "./flutterGen.js";
25
+ import { runTool } from "../core/proc.js";
26
+ // Other modules import these from here.
27
+ export { DIO_FIELD, pubspecDeps } from "./flutterScan.js";
28
+ export { addDartImport, addToClass, importUri, insertGetxRegistration } from "./flutterGen.js";
29
+ export const VERSION = '0.1.0';
30
+ export const FLUTTER_CONVENTION_DOCS = {
31
+ templatesFolder: 'folder with custom templates (codz templates list)',
32
+ packageName: 'package name from pubspec.yaml',
33
+ http: 'HTTP client: dio or http',
34
+ serialization: 'model serialization: manual (hand-written fromJson), json_serializable or freezed',
35
+ freezedClass: 'freezed class declaration: abstract (freezed 3) or plain (freezed 2)',
36
+ apiStyle: 'how API calls are written: repository (classes calling Dio) or retrofit (@RestApi clients)',
37
+ jsonAnnotation: 'annotation used on json_serializable models',
38
+ modelsFolder: 'where model classes live',
39
+ modelFileSuffix: 'suffix in model file names, for example _model, or empty',
40
+ modelClassSuffix: 'suffix in model class names, for example Model, or empty',
41
+ repoFolder: 'where repository or service classes live',
42
+ repoKind: 'class kind for API calls: repository, service or api',
43
+ dioField: 'name of the Dio field inside repository classes',
44
+ di: 'how repositories are registered: getx, riverpod, get_it or none',
45
+ dioProvider: 'Riverpod provider that supplies Dio (riverpod only)',
46
+ dioProviderFile: 'file that defines the Dio provider (riverpod only)',
47
+ diFile: 'file where new classes are registered: the GetX binding or the get_it setup',
48
+ diInstance: 'name of the get_it instance, for example getIt (get_it only)',
49
+ baseUrl: 'Dio base URL found in the code, if it is a literal',
50
+ importStyle: 'package or relative imports',
51
+ stateManagement: 'state management library',
52
+ router: 'router library',
53
+ indent: 'indentation used in Dart files',
54
+ networkFile: 'file that creates the Dio instance (mocks are registered there)',
55
+ modulesFolder: 'where feature modules live (GetX CLI layout: <name>/bindings, controllers, views)',
56
+ routesFile: 'GetX app_pages.dart with the GetPage list (app_routes.dart sits next to it)',
57
+ };
58
+ async function planEndpoint(ctx, input, overlay, opts = {}) {
59
+ const { root, map } = ctx;
60
+ const c = map.conventions;
61
+ if (c.http !== 'dio')
62
+ throw new ToolError('unsupported_setup', `This project uses the ${c.http} package; the Flutter adapter supports Dio only so far.`);
63
+ const missingDio = !pubspecDeps(readText(join(root, 'pubspec.yaml')) ?? '').has('dio');
64
+ const retrofit = c.apiStyle === 'retrofit';
65
+ if (retrofit && input.unwrapData)
66
+ throw new ToolError('unsupported_setup', 'This response wraps its data in {"data": ...}, and a Retrofit method returns the whole body.', 'Run again with --no-unwrap to generate a model for the whole body.');
67
+ const suffix = c.modelClassSuffix ?? '';
68
+ const g = { c, indent: c.indent === 'tab' ? '\t' : ' '.repeat(Number(c.indent) || 2), cls: (n) => (suffix && !n.endsWith(suffix) ? `${n}${suffix}` : n) };
69
+ const files = new Map(overlay ?? []);
70
+ const read = (p) => files.get(p) ?? readText(join(root, p));
71
+ const summary = [];
72
+ const warnings = [...input.warnings];
73
+ const { spec, reused, replaced, notes } = resolveModels(input, map.models, g.cls, { replace: opts.replaceModels });
74
+ warnings.push(...notes);
75
+ if (missingDio)
76
+ warnings.push('dio is not in pubspec.yaml yet; the generated code needs it. Run `flutter pub add dio` (or `codz setup` for the whole networking layer).');
77
+ // Base URL: keep only the path when the host matches Dio's baseUrl. An endpoint from an API spec is
78
+ // always relative to Dio's baseUrl: the spec's server is a stand-in for it (another environment).
79
+ let path = spec.path;
80
+ const fromSpec = specRelativePath(spec, c.baseUrl);
81
+ if (fromSpec !== null && (c.baseUrl || map.clientBaseUrl))
82
+ path = fromSpec;
83
+ else if (c.baseUrl) {
84
+ const b = new URL(c.baseUrl);
85
+ const prefix = b.pathname.replace(/\/$/, '');
86
+ if ((!spec.origin || spec.origin === b.origin) && path.startsWith(prefix))
87
+ path = path.slice(prefix.length) || '/';
88
+ else if ((!spec.origin || spec.origin === b.origin) && prefix && !opts.targetFile) {
89
+ warnings.push(`Dio's baseUrl ends in ${prefix}, so this call goes to ${prefix}${path}. If the API path is really ${path}, remove ${prefix} from the baseUrl.`);
90
+ }
91
+ else if (spec.origin && spec.origin !== b.origin) {
92
+ path = `${spec.origin}${spec.path}`;
93
+ warnings.push(`The URL host ${spec.origin} differs from Dio's baseUrl (${c.baseUrl}), so the full URL is used for this call.`);
94
+ }
95
+ }
96
+ // Already there?
97
+ const existingEp = map.endpoints.find((e) => e.name === spec.name);
98
+ if (existingEp) {
99
+ if (existingEp.method === spec.method && existingEp.path.replace(/\{\w+\}/g, '{}') === path.replace(/\{\w+\}/g, '{}')) {
100
+ return { files, summary: [`${spec.name} already exists in ${existingEp.file}; nothing to do.`], warnings: [], generated: [] };
101
+ }
102
+ throw new ToolError('name_conflict', `A method called ${spec.name} already exists in ${existingEp.file} for ${existingEp.method} ${existingEp.path}.`, 'Pass --name <methodName> to use another name.');
103
+ }
104
+ // Models.
105
+ const modelFile = new Map();
106
+ for (const [name, info] of reused)
107
+ modelFile.set(name, info.file);
108
+ for (const m of spec.models)
109
+ modelFile.set(m.name, replaced.get(m.name)?.file ?? `${c.modelsFolder}/${snake(g.cls(m.name).replace(new RegExp(`${suffix}$`), ''))}${c.modelFileSuffix}.dart`);
110
+ for (const m of spec.models) {
111
+ const file = modelFile.get(m.name);
112
+ const existingText = read(file);
113
+ let content;
114
+ if (replaced.has(m.name) && existingText !== null) {
115
+ // Rewrite just this class; keep the rest of the file (other classes, part directives).
116
+ const rendered = applyTemplate(root, 'model.dart', indentStyled(renderModel(m, g, base(file)), g), { name: g.cls(m.name) }, warnings);
117
+ const classOnly = rendered.slice(rendered.search(/^(@JsonSerializable|@freezed|@Freezed|(?:abstract |sealed )?class )/m));
118
+ const cm = new RegExp(`((?:^|\\n)(?:[ \\t]*(?:///[^\\n]*|@\\w+(?:\\([^)]*\\))?)\\n)*)[ \\t]*(?:abstract\\s+|sealed\\s+)?class\\s+${g.cls(m.name)}\\b[^{]*\\{`).exec(existingText);
119
+ if (!cm)
120
+ throw new ToolError('class_not_found', `Could not find class ${g.cls(m.name)} in ${file}.`);
121
+ const start = cm.index + (cm[1].startsWith('\n') ? 1 : 0);
122
+ const close = matchBrace(existingText, cm.index + cm[0].length - 1, 'dart');
123
+ content = existingText.slice(0, start) + classOnly + existingText.slice(close + 1);
124
+ if (c.serialization === 'json_serializable')
125
+ content = addDartImport(content, 'package:json_annotation/json_annotation.dart');
126
+ if (c.serialization === 'freezed')
127
+ content = addDartImport(content, 'package:freezed_annotation/freezed_annotation.dart');
128
+ }
129
+ else {
130
+ if (existingText !== null)
131
+ throw new ToolError('file_exists', `${file} already exists but does not define ${g.cls(m.name)}.`, 'Pass --model-name <Name> to use another name.');
132
+ content = applyTemplate(root, 'model.dart', indentStyled(renderModel(m, g, base(file)), g), { name: g.cls(m.name) }, warnings) + '\n';
133
+ }
134
+ const deps = new Set();
135
+ for (const f of m.fields)
136
+ referencedModels(f.type, deps);
137
+ for (const d of [...deps].sort()) {
138
+ const df = modelFile.get(d);
139
+ if (df && df !== file)
140
+ content = addDartImport(content, importUri(file, df, c));
141
+ }
142
+ files.set(file, content);
143
+ summary.push(`${replaced.has(m.name) ? 'Update' : 'Create'} ${file}: class ${g.cls(m.name)}${replaced.has(m.name) ? ' (rewritten)' : ''}`);
144
+ }
145
+ if ((c.serialization === 'json_serializable' || c.serialization === 'freezed') && spec.models.length)
146
+ warnings.push(`Run \`dart run build_runner build\` to generate the ${c.serialization === 'freezed' ? '.freezed.dart and .g.dart' : '.g.dart'} files for the new models.`);
147
+ // Repository.
148
+ const resource = resourceOf(spec.path);
149
+ const kind = retrofit ? 'api' : c.repoKind || 'repository';
150
+ const clsWanted = typeName(`${singular(resource)} ${kind}`, 'Api');
151
+ const fromEndpoints = map.endpoints.find((e) => resourceOf(e.path) === resource)?.file;
152
+ const repoFiles = [...new Set([...walk(root, 'lib', { exts: ['.dart'] }), ...[...files.keys()].filter((f) => f.startsWith('lib/') && f.endsWith('.dart'))])].sort();
153
+ const byClass = repoFiles.find((f) => new RegExp(`\\bclass\\s+${clsWanted}\\b`).test(read(f) ?? ''));
154
+ let repoFile = opts.targetFile ?? fromEndpoints ?? byClass;
155
+ let repoClass;
156
+ let repoText;
157
+ const field = c.dioField || '_dio';
158
+ let created = false;
159
+ if (repoFile) {
160
+ repoText = read(repoFile);
161
+ const classes = [...repoText.matchAll(/\bclass\s+(\w+)[^{]*\{/g)].map((m) => m[1]);
162
+ repoClass = retrofit
163
+ ? (/@RestApi\s*\([^)]*\)\s*abstract\s+class\s+(\w+)/.exec(repoText)?.[1] ?? classes[0])
164
+ : (classes.find((n) => new RegExp(`class\\s+${n}\\b[^{]*\\{[\\s\\S]*\\bDio\\??\\s+\\w+\\s*[=;]`).test(repoText)) ?? classes[0]);
165
+ if (new RegExp(`\\b${spec.name}\\s*\\(`).test(repoText))
166
+ throw new ToolError('name_conflict', `${repoFile} already has a member called ${spec.name}.`, 'Pass --name <methodName> to use another name.');
167
+ }
168
+ else {
169
+ created = true;
170
+ repoClass = clsWanted;
171
+ repoFile = `${c.repoFolder}/${snake(clsWanted)}.dart`;
172
+ const L = [];
173
+ if (c.di === 'riverpod') {
174
+ L.push(`final ${camel(repoClass)}Provider = Provider<${repoClass}>((ref) {`, ` return ${repoClass}(ref.watch(${c.dioProvider}));`, '});', '');
175
+ }
176
+ if (retrofit) {
177
+ repoText = newRetrofitClient(repoClass, base(repoFile));
178
+ if (L.length)
179
+ repoText = repoText.replace(/\n@RestApi/, `\n${L.join('\n')}\n@RestApi`);
180
+ }
181
+ else {
182
+ L.push(`class ${repoClass} {`, ` ${repoClass}(this.${field});`, '', ` final Dio ${field};`, '}');
183
+ repoText = L.join('\n') + '\n';
184
+ }
185
+ repoText = addDartImport(repoText, 'package:dio/dio.dart');
186
+ if (c.di === 'riverpod') {
187
+ repoText = addDartImport(repoText, 'package:flutter_riverpod/flutter_riverpod.dart');
188
+ repoText = addDartImport(repoText, importUri(repoFile, c.dioProviderFile, c));
189
+ }
190
+ }
191
+ const eol = detectEol(repoText);
192
+ const methodText = indentStyled(retrofit ? renderRetrofitMethod(spec, g, path) : renderMethod(spec, g, path, field), g);
193
+ let newRepo = addToClass(repoText.replace(/\r\n/g, '\n'), repoClass, applyTemplate(root, 'api-method.dart', methodText, { name: spec.name, method: spec.method, path, returnType: r0(spec, g) }, warnings));
194
+ if (retrofit)
195
+ warnings.push(`Run \`dart run build_runner build\` to regenerate ${base(repoFile)}.g.dart after this change.`);
196
+ const typeRefs = new Set();
197
+ referencedModels(spec.response, typeRefs);
198
+ referencedModels(spec.request, typeRefs);
199
+ for (const t of [...typeRefs].sort())
200
+ newRepo = addDartImport(newRepo, importUri(repoFile, modelFile.get(t), c));
201
+ files.set(repoFile, withEol(newRepo, eol));
202
+ summary.push(`${created ? 'Create' : 'Update'} ${repoFile}: ${repoClass}.${spec.name}() for ${spec.method} ${spec.path}`);
203
+ // Registration for a new repository.
204
+ if (created) {
205
+ if (c.di === 'riverpod') {
206
+ summary.push(`Register ${repoClass} with a Riverpod provider (${camel(repoClass)}Provider) in ${repoFile}`);
207
+ }
208
+ else if (c.di === 'get_it' && c.diFile) {
209
+ const di = read(c.diFile);
210
+ const inst = c.diInstance ?? 'getIt';
211
+ const instRe = inst.replace(/\./g, '\\.');
212
+ // After the last one-line registration (lazy singletons first, then any kind).
213
+ const regs = [...di.matchAll(new RegExp(`^([ \\t]*)${instRe}\\.registerLazySingleton<.*\\);[ \\t]*$`, 'gm'))];
214
+ if (!regs.length)
215
+ regs.push(...di.matchAll(new RegExp(`^([ \\t]*)${instRe}\\.register\\w*<.*\\);[ \\t]*$`, 'gm')));
216
+ const line = `${inst}.registerLazySingleton<${repoClass}>(() => ${repoClass}(${inst}()));`;
217
+ if (regs.length) {
218
+ const last = regs[regs.length - 1];
219
+ const at = last.index + last[0].length;
220
+ const updated = addDartImport(`${di.slice(0, at)}\n${last[1]}${line}${di.slice(at)}`.replace(/\r\n/g, '\n'), importUri(c.diFile, repoFile, c));
221
+ files.set(c.diFile, withEol(updated, detectEol(di)));
222
+ summary.push(`Update ${c.diFile}: register ${repoClass} with get_it`);
223
+ if (!new RegExp(`register\\w*<Dio>|register\\w*\\(\\s*(?:\\(\\)\\s*=>\\s*)?Dio\\(`).test(di)) {
224
+ warnings.push(`No Dio is registered in ${c.diFile}, and ${repoClass} gets its Dio from get_it. Register one, for example ${inst}.registerLazySingleton<Dio>(() => Dio(BaseOptions(baseUrl: ...))).`);
225
+ }
226
+ }
227
+ else {
228
+ warnings.push(`Could not find where ${c.diFile} registers services with get_it; register ${repoClass} there by hand: ${line}`);
229
+ }
230
+ }
231
+ else if (c.di === 'getx' && c.diFile) {
232
+ const di = read(c.diFile);
233
+ if (di === null) {
234
+ warnings.push(`The GetX binding ${c.diFile} was not found; register ${repoClass} there by hand.`);
235
+ }
236
+ else {
237
+ const line = `Get.lazyPut<${repoClass}>(() => ${repoClass}(Get.find<Dio>()), fenix: true);`;
238
+ let updated = insertGetxRegistration(di.replace(/\r\n/g, '\n'), line);
239
+ if (updated === null) {
240
+ warnings.push(`Could not find where ${c.diFile} registers dependencies; register ${repoClass} there by hand.`);
241
+ }
242
+ else {
243
+ updated = addDartImport(updated, 'package:dio/dio.dart');
244
+ updated = addDartImport(updated, importUri(c.diFile, repoFile, c));
245
+ files.set(c.diFile, withEol(updated, detectEol(di)));
246
+ summary.push(`Update ${c.diFile}: register ${repoClass} with Get.lazyPut`);
247
+ }
248
+ }
249
+ }
250
+ else {
251
+ warnings.push(`No dependency injection setup was detected, so ${repoClass} is not registered anywhere. Run \`codz setup\` to add one.`);
252
+ }
253
+ }
254
+ return {
255
+ files,
256
+ summary,
257
+ warnings,
258
+ generated: [{ skill: 'api add', file: repoFile, symbol: spec.name, inputHash: '' }],
259
+ endpoints: [{ name: spec.name, method: spec.method, path, response: r0(spec, g), file: repoFile, calledFrom: [] }],
260
+ models: spec.models.map((m) => ({ name: g.cls(m.name), file: modelFile.get(m.name), fields: m.fields.map((f) => ({ name: f.jsonName, type: dartType(f.type, g), nullable: f.nullable })) })),
261
+ };
262
+ }
263
+ export const flutterAdapter = {
264
+ id: 'flutter',
265
+ version: VERSION,
266
+ capabilities: { explain: true, apiAdd: true },
267
+ supportedSetup: 'Flutter with Dio (repository classes or Retrofit clients); manual fromJson, json_serializable or freezed models; GetX, Riverpod or get_it. Not yet: http package, Chopper.',
268
+ scan,
269
+ planEndpoint,
270
+ planModule: planGetxModule,
271
+ planWire: planGetxWire,
272
+ architectures: flutterArchitectures,
273
+ planSetup: planFlutterSetup,
274
+ afterSetup() {
275
+ const probe = runTool('flutter', ['--version']);
276
+ if (probe.error || probe.status !== 0)
277
+ return null;
278
+ return { cmd: 'flutter', args: ['pub', 'get'], label: 'flutter pub get' };
279
+ },
280
+ formatCommand(_root, files) {
281
+ const dart = files.filter((f) => f.endsWith('.dart'));
282
+ return dart.length ? { cmd: 'dart', args: ['format', ...dart] } : null;
283
+ },
284
+ verifyCommand(root, files) {
285
+ const dart = files.filter((f) => f.endsWith('.dart'));
286
+ // Analysis is only meaningful after `flutter pub get`.
287
+ if (!dart.length || !exists(join(root, '.dart_tool', 'package_config.json')))
288
+ return null;
289
+ return { cmd: 'dart', args: ['analyze', '--no-fatal-warnings', ...dart] };
290
+ },
291
+ };