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,335 @@
1
+ // locale add / locale key: keep every language file in sync.
2
+ //
3
+ // Formats: JSON message files (next-intl, i18next; nested keys with dots),
4
+ // Flutter ARB files (gen-l10n), and GetX Translations classes. A GetX project
5
+ // without translations gets lib/app/translations/app_translations.dart, wired
6
+ // into GetMaterialApp.
7
+ import { join, posix } from 'node:path';
8
+ import { addDartImport, importUri } from "../adapters/flutter.js";
9
+ import { matchBrace } from "../adapters/source.js";
10
+ import { loadMap } from "../core/cache.js";
11
+ import { buildChangeSet } from "../core/changeset.js";
12
+ import { loadConfig } from "../core/config.js";
13
+ import { jsonSet } from "../core/jsonEdit.js";
14
+ import { detectEol, readText, walk, withEol } from "../core/fsutil.js";
15
+ import { ToolError } from "../core/types.js";
16
+ import { camel } from "../input/naming.js";
17
+ const COUNTRY = { en: 'US', hi: 'IN', es: 'ES', fr: 'FR', de: 'DE', it: 'IT', pt: 'BR', ar: 'SA', zh: 'CN', ja: 'JP', ko: 'KR', ru: 'RU', bn: 'IN', ta: 'IN', te: 'IN', mr: 'IN', gu: 'IN', kn: 'IN', ml: 'IN', pa: 'IN', ur: 'PK', tr: 'TR', nl: 'NL', id: 'ID', th: 'TH', vi: 'VN' };
18
+ /** GetX keys are language_COUNTRY (en_US); codz talks in language codes (en). */
19
+ const getxKey = (lang) => (/[_-]/.test(lang) ? lang.replace('-', '_') : COUNTRY[lang] ? `${lang}_${COUNTRY[lang]}` : lang);
20
+ const fromGetxKey = (k) => k.split(/[_-]/)[0];
21
+ function flatten(obj, prefix = '', out = new Map()) {
22
+ if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
23
+ for (const [k, v] of Object.entries(obj)) {
24
+ if (k.startsWith('@'))
25
+ continue;
26
+ const key = prefix ? `${prefix}.${k}` : k;
27
+ if (v && typeof v === 'object' && !Array.isArray(v))
28
+ flatten(v, key, out);
29
+ else
30
+ out.set(key, String(v));
31
+ }
32
+ }
33
+ return out;
34
+ }
35
+ function jsonIndent(text) {
36
+ const m = /\n([ \t]+)"/.exec(text);
37
+ if (!m)
38
+ return 2;
39
+ return m[1][0] === '\t' ? '\t' : m[1].length;
40
+ }
41
+ /** Finds GetX `'en_US': {...}` blocks, following `'en_US': enUS` to `const enUS = {...}` in other files. */
42
+ export function getxBlocks(root, file) {
43
+ const t = readText(join(root, file)) ?? '';
44
+ const out = [];
45
+ const keysGetter = /get\s+keys\s*=>\s*(?:<[^>]*>\s*)?\{/.exec(t) ?? /get\s+keys\s*\{\s*return\s*(?:<[^>]*>\s*)?\{/.exec(t);
46
+ if (!keysGetter)
47
+ return out;
48
+ const mapOpen = keysGetter.index + keysGetter[0].length - 1;
49
+ const mapClose = matchBrace(t, mapOpen, 'dart');
50
+ const body = t.slice(mapOpen, mapClose);
51
+ for (const m of body.matchAll(/['"]([A-Za-z]{2,3}(?:[_-][A-Za-z]{2,4})?)['"]\s*:\s*(\{|[A-Za-z_]\w*)/g)) {
52
+ if (m[2] === '{') {
53
+ const open = mapOpen + m.index + m[0].length - 1;
54
+ out.push({ lang: fromGetxKey(m[1]), code: m[1], file, open, close: matchBrace(t, open, 'dart'), text: t });
55
+ }
56
+ else {
57
+ // A map defined elsewhere: const Map<String, String> enUS = {...};
58
+ for (const f of walk(root, 'lib', { exts: ['.dart'] })) {
59
+ const ft = readText(join(root, f)) ?? '';
60
+ const d = new RegExp(`\\b(?:const|final)\\s+(?:Map<String,\\s*String>\\s+)?${m[2]}\\s*=\\s*(?:<[^>]*>\\s*)?\\{`).exec(ft);
61
+ if (d) {
62
+ const open = d.index + d[0].length - 1;
63
+ out.push({ lang: fromGetxKey(m[1]), code: m[1], file: f, open, close: matchBrace(ft, open, 'dart'), text: ft });
64
+ break;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ function getxEntries(block) {
72
+ const out = new Map();
73
+ for (const m of block.matchAll(/(['"])((?:\\.|(?!\1).)*)\1\s*:\s*(['"])((?:\\.|(?!\3).)*)\3/g))
74
+ out.set(m[2], m[4].replace(/\\(['"$\\])/g, '$1'));
75
+ return out;
76
+ }
77
+ export function readLocales(root, map) {
78
+ // GetX translations first: they live in Dart, not in locale files.
79
+ if (map.framework === 'flutter') {
80
+ const file = walk(root, 'lib', { exts: ['.dart'] }).find((f) => /\bextends\s+Translations\b/.test(readText(join(root, f)) ?? ''));
81
+ if (file) {
82
+ const blocks = getxBlocks(root, file);
83
+ const files = new Map();
84
+ const keys = new Map();
85
+ for (const b of blocks) {
86
+ files.set(b.lang, b.file);
87
+ keys.set(b.lang, getxEntries(b.text.slice(b.open, b.close + 1)));
88
+ }
89
+ const langs = [...files.keys()];
90
+ return { kind: 'getx', defaultLang: langs.includes('en') ? 'en' : langs[0] ?? 'en', files, keys, translationsFile: file };
91
+ }
92
+ }
93
+ if (!map.locales.files.length)
94
+ return null;
95
+ const kind = map.locales.files[0].endsWith('.arb') ? 'arb' : 'json';
96
+ const files = new Map();
97
+ const keys = new Map();
98
+ for (const f of map.locales.files) {
99
+ const base = f.slice(f.lastIndexOf('/') + 1);
100
+ const lang = kind === 'arb' ? base.replace(/\.arb$/, '').replace(/^(?:[a-z0-9_]+_)?([a-z]{2,3}(?:_[A-Z]{2})?)$/, '$1') : base.replace(/\.json$/, '');
101
+ files.set(lang, f);
102
+ let parsed = {};
103
+ try {
104
+ parsed = JSON.parse(readText(join(root, f)) ?? '{}');
105
+ }
106
+ catch {
107
+ throw new ToolError('bad_locale', `${f} is not valid JSON.`);
108
+ }
109
+ keys.set(lang, flatten(parsed));
110
+ }
111
+ return { kind, defaultLang: map.locales.default ?? [...files.keys()][0], files, keys };
112
+ }
113
+ export function dartString(s) {
114
+ return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\$/g, '\\$')}'`;
115
+ }
116
+ // ---------------- GetX: create translations when there are none ----------------
117
+ function createGetxTranslations(root, map, langs, files, summary, warnings) {
118
+ const c = map.conventions;
119
+ const file = `${(c.modulesFolder || 'lib/app/modules').replace(/\/modules$/, '')}/translations/app_translations.dart`;
120
+ const blocks = langs.map((l) => [` '${getxKey(l)}': {`, ' },'].join('\n')).join('\n');
121
+ let t = [
122
+ "/// App texts per language. Use them as 'key'.tr in widgets.",
123
+ 'class AppTranslations extends Translations {',
124
+ ' @override',
125
+ ' Map<String, Map<String, String>> get keys => {',
126
+ blocks,
127
+ ' };',
128
+ '}',
129
+ ].join('\n') + '\n';
130
+ t = addDartImport(t, 'package:get/get.dart');
131
+ files.set(file, t);
132
+ summary.push(`Create ${file}: AppTranslations with ${langs.map(getxKey).join(', ')}`);
133
+ const main = files.get('lib/main.dart') ?? readText(join(root, 'lib/main.dart'));
134
+ if (main !== null && !/\btranslations\s*:/.test(main)) {
135
+ const m = /\bGetMaterialApp(?:\.router)?\s*\(/.exec(main);
136
+ if (m) {
137
+ const lineStart = main.lastIndexOf('\n', m.index) + 1;
138
+ const indent = main.slice(lineStart).match(/^[ \t]*/)[0];
139
+ const [lang, country] = getxKey(langs[0]).split('_');
140
+ const loc = country ? `const Locale('${lang}', '${country}')` : `const Locale('${lang}')`;
141
+ const insert = `\n${indent} translations: AppTranslations(),\n${indent} locale: ${loc},\n${indent} fallbackLocale: ${loc},`;
142
+ let nm = main.slice(0, m.index + m[0].length) + insert + main.slice(m.index + m[0].length);
143
+ nm = addDartImport(nm.replace(/\r\n/g, '\n'), importUri('lib/main.dart', file, c));
144
+ files.set('lib/main.dart', withEol(nm, detectEol(main)));
145
+ summary.push('Update lib/main.dart: GetMaterialApp uses AppTranslations');
146
+ }
147
+ else
148
+ warnings.push(`lib/main.dart has no GetMaterialApp; pass translations: AppTranslations() to it yourself.`);
149
+ }
150
+ return file;
151
+ }
152
+ // ---------------- locale add ----------------
153
+ export async function planLocaleAdd(root, adapter, lang, opts = {}) {
154
+ if (!/^[a-z]{2,3}([_-][A-Za-z]{2,4})?$/.test(lang))
155
+ throw new ToolError('bad_lang', `"${lang}" is not a language code.`, 'Use codes like hi, en, fr, pt_BR.');
156
+ const { map } = await loadMap(root, adapter, loadConfig(root).conventions);
157
+ const files = new Map();
158
+ const summary = [];
159
+ const warnings = [];
160
+ let set = readLocales(root, map);
161
+ if (!set) {
162
+ if (adapter.id === 'flutter' && (map.conventions.stateManagement === 'getx' || map.conventions.di === 'getx')) {
163
+ const def = opts.from ?? 'en';
164
+ createGetxTranslations(root, map, lang === def ? [def] : [def, lang], files, summary, warnings);
165
+ return buildChangeSet(root, 'locale add', { files, summary, warnings, generated: [] });
166
+ }
167
+ if (adapter.id === 'nextjs') {
168
+ const def = opts.from ?? 'en';
169
+ for (const l of new Set([def, lang])) {
170
+ files.set(`messages/${l}.json`, '{}\n');
171
+ summary.push(`Create messages/${l}.json`);
172
+ }
173
+ warnings.push('Load these files with your i18n library (for example next-intl); codz keeps them in sync from now on.');
174
+ return buildChangeSet(root, 'locale add', { files, summary, warnings, generated: [] });
175
+ }
176
+ throw new ToolError('no_locales', 'No locale files were found.', 'For Flutter, set up flutter_localizations with ARB files (lib/l10n/app_en.arb) first; codz then keeps them in sync.');
177
+ }
178
+ if (set.files.has(lang))
179
+ throw new ToolError('locale_exists', `The language ${lang} already exists (${set.files.get(lang)}).`);
180
+ const from = opts.from ?? set.defaultLang;
181
+ if (!set.files.has(from))
182
+ throw new ToolError('bad_lang', `There is no ${from} language to copy from.`, `Languages: ${[...set.files.keys()].join(', ')}`);
183
+ const source = set.files.get(from);
184
+ const count = set.keys.get(from).size;
185
+ if (set.kind === 'json' || set.kind === 'arb') {
186
+ const text = readText(join(root, source));
187
+ const data = JSON.parse(text);
188
+ let target;
189
+ if (set.kind === 'arb') {
190
+ for (const k of Object.keys(data))
191
+ if (k.startsWith('@'))
192
+ delete data[k];
193
+ target = posix.join(posix.dirname(source), source.slice(source.lastIndexOf('/') + 1).replace(new RegExp(`${from}\\.arb$`), `${lang}.arb`));
194
+ const out = { '@@locale': lang, ...data };
195
+ files.set(target, JSON.stringify(out, null, jsonIndent(text)) + '\n');
196
+ }
197
+ else {
198
+ target = posix.join(posix.dirname(source), `${lang}.json`);
199
+ files.set(target, text);
200
+ }
201
+ summary.push(`Create ${target}: ${count} keys copied from ${from}`);
202
+ }
203
+ else {
204
+ const blocks = getxBlocks(root, set.translationsFile);
205
+ const src = blocks.find((b) => b.lang === from);
206
+ const t = (files.get(set.translationsFile) ?? readText(join(root, set.translationsFile))).replace(/\r\n/g, '\n');
207
+ const fresh = getxBlocks(root, set.translationsFile).find((b) => b.file === set.translationsFile);
208
+ const mapClose = (() => {
209
+ const g = /get\s+keys\s*=>\s*(?:<[^>]*>\s*)?\{/.exec(t) ?? /get\s+keys\s*\{\s*return\s*(?:<[^>]*>\s*)?\{/.exec(t);
210
+ return matchBrace(t, g.index + g[0].length - 1, 'dart');
211
+ })();
212
+ const lineStart = t.lastIndexOf('\n', fresh ? fresh.open : mapClose) + 1;
213
+ const indent = t.slice(lineStart).match(/^[ \t]*/)[0];
214
+ const entries = [...set.keys.get(from).entries()].map(([k, v]) => `${indent} ${dartString(k)}: ${dartString(v)},`).join('\n');
215
+ const block = `${indent}'${getxKey(lang)}': {\n${entries}${entries ? '\n' : ''}${indent}},`;
216
+ const before = t.slice(0, mapClose).replace(/\s*$/, '');
217
+ const next = `${before}${before.endsWith(',') || before.endsWith('{') ? '' : ','}\n${block}\n${t.slice(t.lastIndexOf('\n', mapClose) + 1)}`;
218
+ files.set(set.translationsFile, withEol(next, detectEol(readText(join(root, set.translationsFile)))));
219
+ summary.push(`Update ${set.translationsFile}: add '${getxKey(lang)}' with ${count} keys copied from ${from}`);
220
+ void src;
221
+ }
222
+ if (count)
223
+ warnings.push(`The ${count} ${lang} texts are copies of ${from}; translate them (codz doctor lists the untranslated ones).`);
224
+ return buildChangeSet(root, 'locale add', { files, summary, warnings, generated: [] });
225
+ }
226
+ // ---------------- locale key ----------------
227
+ export async function planLocaleKey(root, adapter, key, text, opts = {}) {
228
+ const { map } = await loadMap(root, adapter, loadConfig(root).conventions);
229
+ const files = new Map();
230
+ const summary = [];
231
+ const warnings = [];
232
+ let set = readLocales(root, map);
233
+ if (!set) {
234
+ if (adapter.id === 'flutter' && (map.conventions.stateManagement === 'getx' || map.conventions.di === 'getx')) {
235
+ createGetxTranslations(root, map, ['en'], files, summary, warnings);
236
+ // Re-read from the planned file.
237
+ const file = [...files.keys()].find((f) => f.endsWith('app_translations.dart'));
238
+ set = { kind: 'getx', defaultLang: 'en', files: new Map([['en', file]]), keys: new Map([['en', new Map()]]), translationsFile: file };
239
+ }
240
+ else {
241
+ throw new ToolError('no_locales', 'No locale files were found.', 'Create a language first with: codz locale add en');
242
+ }
243
+ }
244
+ let finalKey = key;
245
+ if (set.kind === 'arb' && !/^[a-z][A-Za-z0-9]*$/.test(key)) {
246
+ finalKey = camel(key);
247
+ warnings.push(`ARB keys must be Dart identifiers, so "${key}" is saved as "${finalKey}".`);
248
+ }
249
+ const exists = [...set.keys.values()].some((k) => k.has(finalKey));
250
+ if ([...set.keys.values()].some((k) => [...k.keys()].some((x) => x.startsWith(`${finalKey}.`)))) {
251
+ throw new ToolError('key_conflict', `${finalKey} is a group of keys (${finalKey}.…), so it cannot hold a text.`, 'Pick another key name.');
252
+ }
253
+ if (exists && !opts.force)
254
+ throw new ToolError('key_exists', `The key ${finalKey} already exists.`, 'Pass --force to replace its text.');
255
+ const untranslated = [];
256
+ const valueFor = (lang) => {
257
+ const v = opts.values?.get(lang);
258
+ if (v !== undefined)
259
+ return v;
260
+ if (lang !== set.defaultLang)
261
+ untranslated.push(lang);
262
+ return text;
263
+ };
264
+ for (const lang of opts.values?.keys() ?? [])
265
+ if (!set.files.has(lang))
266
+ throw new ToolError('bad_lang', `--value ${lang}=... names a language the project does not have.`, `Languages: ${[...set.files.keys()].join(', ')}`);
267
+ if (set.kind === 'json' || set.kind === 'arb') {
268
+ const placeholders = [...text.matchAll(/\{(\w+)\}/g)].map((m) => m[1]);
269
+ for (const [lang, file] of set.files) {
270
+ const raw = readText(join(root, file));
271
+ let next;
272
+ if (set.kind === 'arb') {
273
+ next = jsonSet(raw, [finalKey], valueFor(lang));
274
+ if (lang === set.defaultLang && placeholders.length) {
275
+ next = jsonSet(next, [`@${finalKey}`], { placeholders: Object.fromEntries(placeholders.map((p) => [p, { type: 'String' }])) });
276
+ }
277
+ }
278
+ else
279
+ next = jsonSet(raw, finalKey.split('.'), valueFor(lang));
280
+ files.set(file, withEol(next.replace(/\r\n/g, '\n'), detectEol(raw)));
281
+ summary.push(`${exists ? 'Update' : 'Add'} ${finalKey} in ${file}`);
282
+ }
283
+ }
284
+ else {
285
+ // GetX: one map per language, maybe in different files.
286
+ const pending = new Map(files);
287
+ const byFile = new Map();
288
+ for (const [lang, file] of set.files) {
289
+ const t = pending.get(file) ?? readText(join(root, file));
290
+ const blocks = file === set.translationsFile && pending.has(file) ? getxBlocksFromText(t, file) : getxBlocks(root, set.translationsFile).filter((b) => b.file === file);
291
+ const b = blocks.find((x) => x.lang === lang);
292
+ if (b)
293
+ byFile.set(file, [...(byFile.get(file) ?? []), { lang, open: b.open, close: b.close }]);
294
+ }
295
+ for (const [file, blocks] of byFile) {
296
+ let t = (pending.get(file) ?? readText(join(root, file))).replace(/\r\n/g, '\n');
297
+ // Edit from the end so earlier offsets stay valid.
298
+ for (const b of [...blocks].sort((a, z) => z.close - a.close)) {
299
+ const inner = t.slice(b.open, b.close + 1);
300
+ const value = valueFor(b.lang);
301
+ const existingEntry = new RegExp(`(['"])${finalKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\1\\s*:\\s*(['"])(?:\\\\.|(?!\\2).)*\\2,?`).exec(inner);
302
+ if (existingEntry) {
303
+ t = t.slice(0, b.open) + inner.replace(existingEntry[0], `${dartString(finalKey)}: ${dartString(value)},`) + t.slice(b.close + 1);
304
+ }
305
+ else {
306
+ const lineStart = t.lastIndexOf('\n', b.close) + 1;
307
+ const closeIndent = t.slice(lineStart, b.close).match(/^[ \t]*/)[0];
308
+ const before = t.slice(0, b.close).replace(/\s*$/, '');
309
+ t = `${before}${before.endsWith('{') || before.endsWith(',') ? '' : ','}\n${closeIndent} ${dartString(finalKey)}: ${dartString(value)},\n${closeIndent}${t.slice(b.close)}`;
310
+ }
311
+ }
312
+ const orig = readText(join(root, file));
313
+ files.set(file, orig ? withEol(t, detectEol(orig)) : t);
314
+ summary.push(`${exists ? 'Update' : 'Add'} '${finalKey}' in ${file} (${blocks.map((b) => getxKey(b.lang)).join(', ')})`);
315
+ }
316
+ summary.push(`Use it in a widget: Text('${finalKey}'.tr)`);
317
+ }
318
+ if (untranslated.length)
319
+ warnings.push(`${finalKey} uses the ${set.defaultLang} text for ${untranslated.join(', ')}; translate it there, or pass --value ${untranslated[0]}="...".`);
320
+ return buildChangeSet(root, 'locale key', { files, summary, warnings, generated: [] });
321
+ }
322
+ function getxBlocksFromText(t, file) {
323
+ const out = [];
324
+ const g = /get\s+keys\s*=>\s*(?:<[^>]*>\s*)?\{/.exec(t);
325
+ if (!g)
326
+ return out;
327
+ const mapOpen = g.index + g[0].length - 1;
328
+ const body = t.slice(mapOpen, matchBrace(t, mapOpen, 'dart'));
329
+ for (const m of body.matchAll(/['"]([A-Za-z]{2,3}(?:[_-][A-Za-z]{2,4})?)['"]\s*:\s*\{/g)) {
330
+ const open = mapOpen + m.index + m[0].length - 1;
331
+ out.push({ lang: fromGetxKey(m[1]), open, close: matchBrace(t, open, 'dart') });
332
+ }
333
+ void file;
334
+ return out;
335
+ }
@@ -0,0 +1,175 @@
1
+ // codz mcp: a Model Context Protocol server on stdio, so AI agents and
2
+ // editors can call codz as tools.
3
+ //
4
+ // Every command in the command table becomes a tool (api add → api_add) whose
5
+ // input is its options plus the positional arguments. Tools run the codz
6
+ // CLI itself with --json, in the project folder, so they behave exactly like
7
+ // the command line. Commands that edit code never write: they return the diff
8
+ // and a plan id, and the `apply` tool applies that plan exactly as shown.
9
+ //
10
+ // Resources: codz://project (what codz knows about the project) and
11
+ // codz://commands (every command and option, as markdown).
12
+ import { spawnSync } from 'node:child_process';
13
+ import { createInterface } from 'node:readline';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { CHANGE_FLAGS, COMMAND_LIST, FLAGS, GLOBAL_FLAGS, renderMarkdown } from "../core/commands.js";
16
+ const PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05'];
17
+ /** Commands that make no sense as tools: they need a terminal, or set up codz itself. */
18
+ const NOT_TOOLS = new Set(['completion', 'init', 'mcp', 'login', 'logout']);
19
+ export function toolName(spec) {
20
+ return `${spec.cmd}${spec.sub ? `_${spec.sub}` : ''}`.replace(/-/g, '_');
21
+ }
22
+ export function mcpTools() {
23
+ const tools = [];
24
+ for (const spec of COMMAND_LIST) {
25
+ if (NOT_TOOLS.has(spec.cmd))
26
+ continue;
27
+ const properties = {};
28
+ if (spec.usage.replace(/\[?--[^\]]*\]?/g, '').trim())
29
+ properties.args = { type: 'array', items: { type: 'string' }, description: `Positional arguments: ${spec.usage}` };
30
+ for (const f of spec.flags) {
31
+ if (CHANGE_FLAGS.includes(f) || GLOBAL_FLAGS.includes(f) || f.length === 1)
32
+ continue;
33
+ const def = FLAGS[f];
34
+ const description = def.desc + (def.choices ? ` (${def.choices.join(', ')})` : '');
35
+ properties[f] = def.value ? (def.repeat ? { type: 'array', items: { type: 'string' }, description } : { type: 'string', description }) : { type: 'boolean', description };
36
+ }
37
+ if (spec.cmd === 'apply') {
38
+ properties['allow-dirty'] = { type: 'boolean', description: FLAGS['allow-dirty'].desc };
39
+ properties['no-verify'] = { type: 'boolean', description: FLAGS['no-verify'].desc };
40
+ }
41
+ const writes = spec.change && !(spec.cmd === 'doctor');
42
+ tools.push({
43
+ name: toolName(spec),
44
+ description: `${spec.summary}.${writes || spec.cmd === 'new' ? ' Returns the diff and a plan id; nothing is written until you call apply with that id.' : ''}${spec.cmd === 'new' ? ' Paid starters are never downloaded from here: the user downloads one once in a terminal (codz new or starters show), then it can be planned.' : ''}${spec.cmd === 'doctor' ? ' With fix: true it returns a plan to apply instead of reporting.' : ''}${spec.notes?.length ? ` ${spec.notes.join(' ')}` : ''}`,
45
+ inputSchema: { type: 'object', properties, additionalProperties: false },
46
+ spec,
47
+ });
48
+ }
49
+ return tools;
50
+ }
51
+ /** The CLI arguments for a tool call. Change commands always get --plan, so a tool never writes by itself. */
52
+ export function cliArgs(tool, input) {
53
+ const { spec } = tool;
54
+ const out = [spec.cmd, ...(spec.sub ? [spec.sub] : [])];
55
+ const args = Array.isArray(input.args) ? input.args.map(String) : [];
56
+ // A positional argument that looks like an option (--cwd, --yes, --force, --root) would let a tool call widen what it
57
+ // may do. Options only come through the schema's named properties, which are checked below.
58
+ const flagLike = args.find((a) => a.startsWith('-'));
59
+ if (flagLike)
60
+ throw new Error(`Positional arguments cannot start with "-" (got "${flagLike.slice(0, 40)}"); pass options as named properties.`);
61
+ out.push(...args);
62
+ for (const [k, v] of Object.entries(input)) {
63
+ if (k === 'args' || v === undefined || v === null || v === false)
64
+ continue;
65
+ if (!(k in tool.inputSchema.properties))
66
+ throw new Error(`Unknown argument "${k}" for ${tool.name}.`);
67
+ if (v === true)
68
+ out.push(`--${k}`);
69
+ else if (Array.isArray(v))
70
+ for (const x of v)
71
+ out.push(`--${k}`, String(x));
72
+ else
73
+ out.push(`--${k}`, String(v));
74
+ }
75
+ // codz new only ever plans from an agent; a person applies (and a paid starter must already be downloaded).
76
+ if (spec.cmd === 'new')
77
+ out.push('--plan');
78
+ if (spec.change && spec.cmd !== 'apply' && !(spec.cmd === 'doctor' && input.fix !== true) && !(spec.cmd === 'api' && spec.sub === 'sync' && input.check === true))
79
+ out.push('--plan');
80
+ out.push('--json', '--no-color');
81
+ return out;
82
+ }
83
+ const BIN = fileURLToPath(new URL('../../bin/codz.js', import.meta.url));
84
+ function runCli(args, cwd) {
85
+ const r = spawnSync(process.execPath, [BIN, ...args], { cwd, encoding: 'utf8', input: '', env: { ...process.env, NO_COLOR: '1', CODZ_MCP: '1' }, maxBuffer: 64 * 1024 * 1024, timeout: 120_000 });
86
+ return { code: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
87
+ }
88
+ /** Handles one message; returns the response, or null for notifications. */
89
+ export function handleMessage(msg, cwd, version, tools = mcpTools()) {
90
+ const reply = (result) => ({ jsonrpc: '2.0', id: msg.id, result });
91
+ const fail = (code, message) => ({ jsonrpc: '2.0', id: msg.id, error: { code, message } });
92
+ if (msg.id === undefined)
93
+ return null; // notifications (initialized, cancelled) need no answer
94
+ switch (msg.method) {
95
+ case 'initialize': {
96
+ const asked = String(msg.params?.protocolVersion ?? '');
97
+ return reply({
98
+ protocolVersion: PROTOCOLS.includes(asked) ? asked : PROTOCOLS[0],
99
+ capabilities: { tools: { listChanged: false }, resources: { listChanged: false } },
100
+ serverInfo: { name: 'codz', version },
101
+ instructions: 'codz understands this Next.js or Flutter project and changes it deterministically. Start with explain. Tools that edit code return a diff and a planId; review it, then call apply with the planId. Use dry runs freely: nothing is written until apply.',
102
+ });
103
+ }
104
+ case 'ping':
105
+ return reply({});
106
+ case 'tools/list':
107
+ return reply({ tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
108
+ case 'tools/call': {
109
+ const tool = tools.find((t) => t.name === msg.params?.name);
110
+ if (!tool)
111
+ return fail(-32602, `Unknown tool: ${String(msg.params?.name)}`);
112
+ let args;
113
+ try {
114
+ args = cliArgs(tool, msg.params?.arguments ?? {});
115
+ }
116
+ catch (e) {
117
+ return reply({ content: [{ type: 'text', text: e.message }], isError: true });
118
+ }
119
+ const r = runCli(args, cwd);
120
+ let isError = r.code !== 0;
121
+ let text = r.stdout.trim();
122
+ try {
123
+ const json = JSON.parse(text);
124
+ // A failed check (exit 1) is still a useful answer; only {"ok": false} is an error.
125
+ isError = json.ok === false;
126
+ text = JSON.stringify(json, null, 2);
127
+ }
128
+ catch {
129
+ text = [text, r.stderr.trim()].filter(Boolean).join('\n') || `codz exited with code ${r.code}`;
130
+ }
131
+ return reply({ content: [{ type: 'text', text }], isError });
132
+ }
133
+ case 'resources/list':
134
+ return reply({
135
+ resources: [
136
+ { uri: 'codz://project', name: 'Project map', description: 'Framework, conventions, endpoints, models, routes and locales codz found', mimeType: 'application/json' },
137
+ { uri: 'codz://commands', name: 'codz commands', description: 'Every command and option', mimeType: 'text/markdown' },
138
+ ],
139
+ });
140
+ case 'resources/read': {
141
+ const uri = String(msg.params?.uri ?? '');
142
+ if (uri === 'codz://commands')
143
+ return reply({ contents: [{ uri, mimeType: 'text/markdown', text: renderMarkdown(version) }] });
144
+ if (uri === 'codz://project') {
145
+ const r = runCli(['explain', '--json', '--no-color'], cwd);
146
+ return reply({ contents: [{ uri, mimeType: 'application/json', text: r.stdout.trim() || r.stderr.trim() }] });
147
+ }
148
+ return fail(-32002, `Unknown resource: ${uri}`);
149
+ }
150
+ default:
151
+ return fail(-32601, `Method not found: ${msg.method}`);
152
+ }
153
+ }
154
+ /** Runs the server until stdin closes. Logs go to stderr; stdout carries only protocol messages. */
155
+ export function serveMcp(cwd, version) {
156
+ const tools = mcpTools();
157
+ process.stderr.write(`codz mcp: serving ${tools.length} tools for ${cwd}\n`);
158
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
159
+ rl.on('line', (line) => {
160
+ if (!line.trim())
161
+ return;
162
+ let msg;
163
+ try {
164
+ msg = JSON.parse(line);
165
+ }
166
+ catch {
167
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }) + '\n');
168
+ return;
169
+ }
170
+ const res = handleMessage(msg, cwd, version, tools);
171
+ if (res)
172
+ process.stdout.write(JSON.stringify(res) + '\n');
173
+ });
174
+ return new Promise((resolve) => rl.on('close', () => resolve(0)));
175
+ }