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,113 @@
1
+ // Small edits to JSON text that keep the rest of the file byte for byte:
2
+ // set one (possibly nested) key without reformatting anything else.
3
+ import { ToolError } from "./types.js";
4
+ function ws(t, i) {
5
+ while (i < t.length && /\s/.test(t[i]))
6
+ i++;
7
+ return i;
8
+ }
9
+ function parseString(t, i) {
10
+ let j = i + 1;
11
+ while (j < t.length && t[j] !== '"')
12
+ j += t[j] === '\\' ? 2 : 1;
13
+ return [JSON.parse(t.slice(i, j + 1)), j + 1];
14
+ }
15
+ function parseValue(t, i) {
16
+ i = ws(t, i);
17
+ const c = t[i];
18
+ if (c === '{') {
19
+ const object = parseObject(t, i);
20
+ return { end: object.close + 1, object };
21
+ }
22
+ if (c === '"')
23
+ return { end: parseString(t, i)[1] };
24
+ if (c === '[') {
25
+ let j = ws(t, i + 1);
26
+ if (t[j] === ']')
27
+ return { end: j + 1 };
28
+ for (;;) {
29
+ j = ws(t, parseValue(t, j).end);
30
+ if (t[j] === ',')
31
+ j++;
32
+ else if (t[j] === ']')
33
+ return { end: j + 1 };
34
+ else
35
+ throw new ToolError('bad_json', `Unexpected "${t[j]}" at offset ${j}.`);
36
+ }
37
+ }
38
+ const m = /^(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/.exec(t.slice(i));
39
+ if (!m)
40
+ throw new ToolError('bad_json', `Unexpected "${c}" at offset ${i}.`);
41
+ return { end: i + m[0].length };
42
+ }
43
+ function parseObject(t, open) {
44
+ const members = [];
45
+ let i = ws(t, open + 1);
46
+ if (t[i] === '}')
47
+ return { open, close: i, members };
48
+ for (;;) {
49
+ if (t[i] !== '"')
50
+ throw new ToolError('bad_json', `Expected a key at offset ${i}.`);
51
+ const [key, afterKey] = parseString(t, i);
52
+ let j = ws(t, afterKey);
53
+ if (t[j] !== ':')
54
+ throw new ToolError('bad_json', `Expected ":" at offset ${j}.`);
55
+ const valueStart = ws(t, j + 1);
56
+ const v = parseValue(t, valueStart);
57
+ members.push({ key, start: i, valueStart, valueEnd: v.end, object: v.object });
58
+ j = ws(t, v.end);
59
+ if (t[j] === ',')
60
+ i = ws(t, j + 1);
61
+ else if (t[j] === '}')
62
+ return { open, close: j, members };
63
+ else
64
+ throw new ToolError('bad_json', `Unexpected "${t[j]}" at offset ${j}.`);
65
+ }
66
+ }
67
+ const lineIndent = (t, pos) => t.slice(t.lastIndexOf('\n', pos - 1) + 1).match(/^[ \t]*/)[0];
68
+ function indentUnit(t) {
69
+ const m = /\n([ \t]+)"/.exec(t);
70
+ return m ? (m[1][0] === '\t' ? '\t' : ' '.repeat(Math.min(m[1].length, 8))) : ' ';
71
+ }
72
+ function render(value, indent, unit) {
73
+ return JSON.stringify(value, null, unit).replace(/\n/g, `\n${indent}`);
74
+ }
75
+ function insert(t, obj, key, value, unit) {
76
+ const oneLine = !t.slice(obj.open, obj.close).includes('\n') && obj.members.length > 0;
77
+ if (oneLine) {
78
+ const last = obj.members[obj.members.length - 1];
79
+ return `${t.slice(0, last.valueEnd)}, ${JSON.stringify(key)}: ${JSON.stringify(value)}${t.slice(last.valueEnd)}`;
80
+ }
81
+ const indent = obj.members.length ? lineIndent(t, obj.members[0].start) : lineIndent(t, obj.close) + unit;
82
+ const entry = `${JSON.stringify(key)}: ${render(value, indent, unit)}`;
83
+ if (!obj.members.length)
84
+ return `${t.slice(0, obj.open + 1)}\n${indent}${entry}\n${lineIndent(t, obj.close)}${t.slice(obj.close)}`;
85
+ const last = obj.members[obj.members.length - 1];
86
+ return `${t.slice(0, last.valueEnd)},\n${indent}${entry}${t.slice(last.valueEnd)}`;
87
+ }
88
+ /** Sets path (nested keys) to value, creating parent objects as needed. */
89
+ export function jsonSet(text, path, value) {
90
+ const start = ws(text, 0);
91
+ if (text[start] !== '{')
92
+ throw new ToolError('bad_json', 'The file is not a JSON object.');
93
+ const unit = indentUnit(text);
94
+ let obj = parseObject(text, start);
95
+ for (let i = 0; i < path.length; i++) {
96
+ const m = obj.members.find((x) => x.key === path[i]);
97
+ const last = i === path.length - 1;
98
+ if (!m) {
99
+ let v = value;
100
+ for (const k of path.slice(i + 1).reverse())
101
+ v = { [k]: v };
102
+ return insert(text, obj, path[i], v, unit);
103
+ }
104
+ if (last) {
105
+ const indent = lineIndent(text, m.start);
106
+ return text.slice(0, m.valueStart) + render(value, indent, unit) + text.slice(m.valueEnd);
107
+ }
108
+ if (!m.object)
109
+ throw new ToolError('key_conflict', `${path.slice(0, i + 1).join('.')} is a text, so it cannot hold ${path.join('.')}.`, 'Pick another key name.');
110
+ obj = m.object;
111
+ }
112
+ return text;
113
+ }
@@ -0,0 +1,159 @@
1
+ // Keeps hand-written code when codz regenerates a model.
2
+ //
3
+ // Dart: a regenerated class gets back every member codz does not generate
4
+ // (copyWith, ==, hashCode, toString, getters, other constructors and methods).
5
+ // TypeScript: doc comments above interface fields survive.
6
+ import { matchBrace, maskCode } from "../adapters/source.js";
7
+ function classBlocks(text, masked) {
8
+ const out = new Map();
9
+ for (const m of masked.matchAll(/\bclass\s+(\w+)[^{;]*\{/g)) {
10
+ const open = m.index + m[0].length - 1;
11
+ const close = matchBrace(text, open, 'dart');
12
+ if (close !== -1)
13
+ out.set(m[1], { open, close });
14
+ }
15
+ return out;
16
+ }
17
+ /** Splits a Dart class body into members, each with the comments and annotations above it. */
18
+ export function splitDartMembers(body) {
19
+ const masked = maskCode(body, 'dart');
20
+ const out = [];
21
+ let start = 0;
22
+ let i = 0;
23
+ let depth = 0;
24
+ const push = (end) => {
25
+ const text = body.slice(start, end);
26
+ if (text.trim()) {
27
+ const code = masked.slice(start, end);
28
+ out.push({ text, code, name: memberName(code) });
29
+ }
30
+ start = end;
31
+ };
32
+ while (i < body.length) {
33
+ const c = masked[i];
34
+ if (c === '(' || c === '[' || c === '{')
35
+ depth++;
36
+ else if (c === ')' || c === ']' || c === '}') {
37
+ depth--;
38
+ if (depth === 0 && c === '}') {
39
+ // A block body ends the member unless an expression continues (`= {...};`, `}.foo`, `},`).
40
+ let j = i + 1;
41
+ while (j < body.length && /\s/.test(masked[j]))
42
+ j++;
43
+ if (masked[j] === ';') {
44
+ i = j;
45
+ push(i + 1);
46
+ }
47
+ else if (!/[.,)?:]/.test(masked[j] ?? ''))
48
+ push(i + 1);
49
+ }
50
+ }
51
+ else if (c === ';' && depth === 0)
52
+ push(i + 1);
53
+ i++;
54
+ }
55
+ if (body.slice(start).trim())
56
+ push(body.length);
57
+ return out;
58
+ }
59
+ function memberName(code) {
60
+ const c = code.replace(/@\w+(\([^)]*\))?/g, '').trim();
61
+ const getter = /\b(?:get|set)\s+(\w+)/.exec(c);
62
+ if (getter)
63
+ return getter[1];
64
+ if (/\boperator\s*==/.test(c))
65
+ return 'operator ==';
66
+ const call = /([\w.]+)\s*(?:<[^>]*>)?\s*\(/.exec(c);
67
+ const field = /(\w+)\s*(?:=|;)/.exec(c);
68
+ if (call && (!field || call.index < field.index))
69
+ return call[1];
70
+ return field ? field[1] : c.split(/\s+/).slice(-1)[0];
71
+ }
72
+ /** True for members codz writes itself in a model class. */
73
+ function isGenerated(m, cls) {
74
+ const c = m.code.replace(/@\w+(\([^)]*\))?/g, '').trim();
75
+ if (new RegExp(`^(const\\s+)?${cls}\\s*\\(`).test(c))
76
+ return true; // unnamed constructor
77
+ if (new RegExp(`^factory\\s+${cls}\\.fromJson\\b`).test(c))
78
+ return true;
79
+ if (new RegExp(`^const\\s+factory\\s+${cls}\\s*\\(`).test(c))
80
+ return true; // freezed
81
+ if (/^(Map<String,\s*dynamic>|Map<String,\s*Object\?>)\s+toJson\s*\(/.test(c))
82
+ return true;
83
+ // A plain field: `final Type name;` (no initializer, no parentheses).
84
+ if (/^(final\s+|late\s+final\s+|late\s+)?[\w<>?, ]+\s+\w+\s*;$/.test(c.replace(/\s+/g, ' ')))
85
+ return true;
86
+ return false;
87
+ }
88
+ function fieldNames(members, cls) {
89
+ return new Set(members.filter((m) => isGenerated(m, cls) && /;\s*$/.test(m.code) && !/\(/.test(m.code)).map((m) => m.name));
90
+ }
91
+ export function mergeDart(oldText, newText) {
92
+ const report = { kept: new Map(), mentionsRemoved: new Map() };
93
+ const oldBlocks = classBlocks(oldText, maskCode(oldText, 'dart'));
94
+ let text = newText;
95
+ // From the last class down, so earlier offsets stay valid.
96
+ const newBlocks = [...classBlocks(text, maskCode(text, 'dart')).entries()].sort((a, b) => b[1].open - a[1].open);
97
+ for (const [cls, nb] of newBlocks) {
98
+ const ob = oldBlocks.get(cls);
99
+ if (!ob)
100
+ continue;
101
+ // freezed: @Default(...) values on factory parameters are yours; keep them.
102
+ const oldBody = oldText.slice(ob.open + 1, ob.close);
103
+ if (/\bconst\s+factory\b/.test(oldBody)) {
104
+ let body = text.slice(nb.open + 1, nb.close);
105
+ for (const d of oldBody.matchAll(/@Default\(((?:[^()]|\([^()]*\))*)\)\s+(?:required\s+)?[\w<>?, ]+?\s+(\w+)\s*[,)}]/g)) {
106
+ body = body.replace(new RegExp(`\\brequired\\s+([\\w<>, ]+\\s+${d[2]}\\s*,)`), `@Default(${d[1]}) $1`);
107
+ }
108
+ text = text.slice(0, nb.open + 1) + body + text.slice(nb.close);
109
+ nb.close = nb.open + 1 + body.length;
110
+ }
111
+ const oldMembers = splitDartMembers(oldText.slice(ob.open + 1, ob.close));
112
+ const newMembers = splitDartMembers(text.slice(nb.open + 1, nb.close));
113
+ const newNames = new Set(newMembers.map((m) => m.name));
114
+ const keep = oldMembers.filter((m) => !isGenerated(m, cls) && !newNames.has(m.name));
115
+ if (!keep.length)
116
+ continue;
117
+ const removed = [...fieldNames(oldMembers, cls)].filter((f) => !fieldNames(newMembers, cls).has(f));
118
+ const mentions = keep
119
+ .map((m) => ({ member: m.name, fields: removed.filter((f) => new RegExp(`\\b${f}\\b`).test(m.code)) }))
120
+ .filter((x) => x.fields.length);
121
+ if (mentions.length)
122
+ report.mentionsRemoved.set(cls, mentions);
123
+ report.kept.set(cls, keep.map((m) => m.name));
124
+ const before = text.slice(0, nb.close).replace(/\s*$/, '');
125
+ const block = keep.map((m) => m.text.replace(/^\n+/, '').replace(/\s+$/, '')).join('\n\n');
126
+ text = `${before}\n\n${block}\n${text.slice(nb.close)}`;
127
+ }
128
+ return { text, report };
129
+ }
130
+ /** TypeScript: comments above interface fields come back after regeneration. */
131
+ export function mergeTsDocs(oldText, newText) {
132
+ const fieldRe = /^([ \t]*)(?:readonly\s+)?(['"]?[\w$-]+['"]?)\??\s*:/;
133
+ const docs = new Map();
134
+ const oldLines = oldText.split('\n');
135
+ for (let i = 0; i < oldLines.length; i++) {
136
+ const m = fieldRe.exec(oldLines[i]);
137
+ if (!m)
138
+ continue;
139
+ const doc = [];
140
+ let j = i - 1;
141
+ while (j >= 0 && /^\s*(\/\/|\/\*\*|\*|\*\/)/.test(oldLines[j]))
142
+ doc.unshift(oldLines[j--].trim());
143
+ if (doc.length)
144
+ docs.set(m[2], doc);
145
+ }
146
+ if (!docs.size)
147
+ return newText;
148
+ const out = [];
149
+ for (const line of newText.split('\n')) {
150
+ const m = fieldRe.exec(line);
151
+ const prev = out[out.length - 1] ?? '';
152
+ if (m && docs.has(m[2]) && !/^\s*(\/\/|\*|\/\*\*)/.test(prev)) {
153
+ for (const d of docs.get(m[2]))
154
+ out.push(`${m[1]}${d.startsWith('*') ? ' ' : ''}${d}`);
155
+ }
156
+ out.push(line);
157
+ }
158
+ return out.join('\n');
159
+ }
@@ -0,0 +1,23 @@
1
+ // Runs a project tool (flutter, npx, gradle, dart, the formatter...). On Windows
2
+ // most of these are .cmd or .bat files, which only start through cmd.exe, and
3
+ // cmd.exe splits on spaces: so there the command line is built here with every
4
+ // argument quoted (paths like "C:\Users\Jo Doe\app" survive), instead of
5
+ // letting Node join the arguments unquoted. An absolute path to an .exe (node
6
+ // itself) runs directly, without a shell.
7
+ import { spawnSync } from 'node:child_process';
8
+ /** Quotes one argument for cmd.exe + the C runtime's argument parser. */
9
+ export function winQuote(arg) {
10
+ if (arg !== '' && !/[\s"&|<>^()%!,;=]/.test(arg))
11
+ return arg;
12
+ return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
13
+ }
14
+ export function runTool(cmd, args, opts = {}) {
15
+ const options = { ...opts, encoding: 'utf8' };
16
+ if (process.platform !== 'win32' || /^[a-zA-Z]:[\\/].*\.exe$/i.test(cmd))
17
+ return spawnSync(cmd, args, options);
18
+ return spawnSync([cmd, ...args].map(winQuote).join(' '), { ...options, shell: true });
19
+ }
20
+ /** True when a runTool result means the program is not installed (ENOENT, or cmd.exe's "not recognized"). */
21
+ export function toolMissing(r) {
22
+ return r.error?.code === 'ENOENT' || (process.platform === 'win32' && r.status !== 0 && /is not recognized as an internal or external command/i.test(r.stderr ?? ''));
23
+ }
@@ -0,0 +1,75 @@
1
+ // Guards between untrusted input (cURL, HAR, OpenAPI, JSON samples, flags)
2
+ // and generated source code. Every value that ends up inside a string
3
+ // literal, a comment or a URL in generated code goes through one of these.
4
+ //
5
+ // Two layers: inputs are narrowed to safe characters where the meaning
6
+ // allows it (URL paths are percent-encoded, hosts are checked), and string
7
+ // literals are escaped for the target language anyway, so neither layer has
8
+ // to be perfect on its own.
9
+ import { ToolError } from "./types.js";
10
+ /** Characters kept as they are in a URL path segment; everything else is percent-encoded. */
11
+ const PATH_SAFE = /[A-Za-z0-9\-._~!&+,;=:@]/;
12
+ /** Percent-encodes a decoded path segment so it holds no quote, backtick, $, \, *, braces or spaces. */
13
+ export function encodePathSegment(seg) {
14
+ let out = '';
15
+ for (const ch of seg)
16
+ out += PATH_SAFE.test(ch) ? ch : [...Buffer.from(ch, 'utf8')].map((b) => `%${b.toString(16).toUpperCase().padStart(2, '0')}`).join('');
17
+ return out;
18
+ }
19
+ /** A host as URLs allow it, narrowed to what DNS names and IP addresses use. */
20
+ const HOST = /^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$|^\[[0-9A-Fa-f:.]+\]$/;
21
+ /** Throws unless the URL's host is a plain DNS name or IP address (no quotes, backticks, $ or braces). */
22
+ export function checkHost(url, what = 'The URL') {
23
+ if (!HOST.test(url.hostname))
24
+ throw new ToolError('bad_url', `${what} has a host codz will not write into code: "${url.hostname.slice(0, 80)}".`, 'Use a normal host name such as api.example.com.');
25
+ }
26
+ /** A base URL that is safe to write into generated code and config files: http(s), plain host, optional port and plain path. */
27
+ const BASE_URL = /^https?:\/\/[^\s/?#]+(?::\d{1,5})?(?:\/[A-Za-z0-9\-._~!&+,;=:@%/]*)?$/;
28
+ /** Returns the base URL without a trailing slash, or throws a clear error. */
29
+ export function checkBaseUrl(value) {
30
+ const v = value.trim();
31
+ let u = null;
32
+ try {
33
+ u = new URL(v);
34
+ }
35
+ catch {
36
+ u = null;
37
+ }
38
+ if (!u || !BASE_URL.test(v) || (u.protocol !== 'https:' && u.protocol !== 'http:') || u.username || u.password) {
39
+ throw new ToolError('bad_url', `"${v.slice(0, 120)}" is not a base URL codz can use.`, 'Use something like https://api.example.com or https://api.example.com/v1 (no quotes, spaces, query or credentials).');
40
+ }
41
+ checkHost(u, 'The base URL');
42
+ return v.replace(/\/+$/, '');
43
+ }
44
+ /** Same check as checkBaseUrl, as an interactive prompt validator (null = fine). */
45
+ export function baseUrlProblem(value) {
46
+ if (value === '')
47
+ return null;
48
+ try {
49
+ checkBaseUrl(value);
50
+ return null;
51
+ }
52
+ catch (e) {
53
+ return e instanceof ToolError ? `${e.message} ${e.hint ?? ''}`.trim() : 'Not a valid URL.';
54
+ }
55
+ }
56
+ /** A JavaScript/TypeScript string literal in the given quote (' " or `). */
57
+ export function jsString(s, q) {
58
+ let body = s.replace(/\\/g, '\\\\').replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
59
+ body = body.split(q).join(`\\${q}`);
60
+ if (q === '`')
61
+ body = body.replace(/\$\{/g, '\\${');
62
+ return `${q}${body}${q}`;
63
+ }
64
+ /** The inside of a JavaScript template literal (no backticks around it): escapes \, ` and ${. */
65
+ export function jsTemplateText(s) {
66
+ return s.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
67
+ }
68
+ /** A Kotlin string literal in double quotes: escapes \, ", $ and line breaks. */
69
+ export function ktString(s) {
70
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/\r/g, '\\r').replace(/\n/g, '\\n')}"`;
71
+ }
72
+ /** Text that is safe inside a block or doc comment in TS, Dart and Kotlin: no comment end and one line. */
73
+ export function commentText(s) {
74
+ return s.replace(/\*\//g, '*\u200b/').replace(/[\r\n]+/g, ' ');
75
+ }
@@ -0,0 +1,100 @@
1
+ // Custom templates: a project can change how generated code looks by keeping
2
+ // template files in codz/templates/ (templatesFolder in the config).
3
+ //
4
+ // A template is the text to write, with {{placeholders}}. {{default}} is what
5
+ // codz would have written, so a template can wrap it (add a header, a log
6
+ // line, a decorator) or replace it using the other placeholders. Anything a
7
+ // template changes must still look like code codz can read back (an
8
+ // exported function, a class method), or later commands will not find it.
9
+ import { join } from 'node:path';
10
+ import { loadConfig } from "./config.js";
11
+ import { readText } from "./fsutil.js";
12
+ export const DEFAULT_TEMPLATES_FOLDER = 'codz/templates';
13
+ export const TEMPLATES = [
14
+ {
15
+ id: 'file-header.ts',
16
+ about: 'text at the top of every TypeScript file codz creates',
17
+ vars: { file: 'path of the new file' },
18
+ example: '// Generated by codz. Edit freely.\n',
19
+ },
20
+ {
21
+ id: 'file-header.dart',
22
+ about: 'text at the top of every Dart file codz creates',
23
+ vars: { file: 'path of the new file' },
24
+ example: '// Generated by codz. Edit freely.\n',
25
+ },
26
+ {
27
+ id: 'api-function.ts',
28
+ about: 'a Next.js API function',
29
+ vars: { default: "codz's function", name: 'function name', method: 'HTTP method', path: 'API path', params: 'parameter list', returnType: 'resolved type' },
30
+ example: '{{default}}\n',
31
+ },
32
+ {
33
+ id: 'api-method.dart',
34
+ about: 'a Flutter repository method or Retrofit method',
35
+ vars: { default: "codz's method", name: 'method name', method: 'HTTP method', path: 'API path', returnType: 'resolved type' },
36
+ example: '{{default}}\n',
37
+ },
38
+ {
39
+ id: 'model.ts',
40
+ about: 'a TypeScript interface for API data',
41
+ vars: { default: "codz's interface", name: 'interface name' },
42
+ example: '{{default}}\n',
43
+ },
44
+ {
45
+ id: 'model.dart',
46
+ about: 'a Dart model class (manual, json_serializable or freezed)',
47
+ vars: { default: "codz's class, with its imports and part lines", name: 'class name' },
48
+ example: '{{default}}\n',
49
+ },
50
+ ];
51
+ export function templatesFolder(root) {
52
+ return loadConfig(root).conventions.templatesFolder || DEFAULT_TEMPLATES_FOLDER;
53
+ }
54
+ const cache = new Map();
55
+ /** The project's template for `id`, or null when it has none. */
56
+ export function loadTemplate(root, id) {
57
+ const file = join(root, templatesFolder(root), `${id}.tmpl`);
58
+ if (!cache.has(file))
59
+ cache.set(file, readText(file));
60
+ return cache.get(file);
61
+ }
62
+ /** For tests: forget templates read earlier. */
63
+ export function clearTemplateCache() {
64
+ cache.clear();
65
+ }
66
+ /** Fills {{name}} placeholders; unknown ones are left as written and reported. */
67
+ export function render(template, vars) {
68
+ const unknown = [];
69
+ const text = template.replace(/\{\{\s*(\w+)\s*\}\}/g, (all, k) => {
70
+ if (k in vars)
71
+ return vars[k];
72
+ unknown.push(k);
73
+ return all;
74
+ });
75
+ return { text, unknown };
76
+ }
77
+ /**
78
+ * Runs `rendered` (codz's own output) through the project's template, if any.
79
+ * Trailing newlines follow codz's output so blocks still join cleanly.
80
+ */
81
+ export function applyTemplate(root, id, rendered, vars, warnings) {
82
+ const t = loadTemplate(root, id);
83
+ if (t === null)
84
+ return rendered;
85
+ const r = render(t, { ...vars, default: rendered });
86
+ if (r.unknown.length && warnings)
87
+ warnings.push(`Template ${id}.tmpl uses unknown placeholders: ${[...new Set(r.unknown)].map((u) => `{{${u}}}`).join(', ')}. Known: ${Object.keys(TEMPLATES.find((x) => x.id === id)?.vars ?? {}).map((v) => `{{${v}}}`).join(', ')}.`);
88
+ return r.text.replace(/\n+$/, '') + (rendered.match(/\n+$/)?.[0] ?? '');
89
+ }
90
+ /** The header template for a new file, by extension. */
91
+ export function fileHeader(root, path) {
92
+ const id = /\.(tsx?|jsx?|mjs)$/.test(path) ? 'file-header.ts' : /\.dart$/.test(path) ? 'file-header.dart' : null;
93
+ if (!id)
94
+ return '';
95
+ const t = loadTemplate(root, id);
96
+ if (t === null)
97
+ return '';
98
+ const text = render(t, { file: path }).text;
99
+ return text.endsWith('\n') ? text : `${text}\n`;
100
+ }
@@ -0,0 +1,112 @@
1
+ // TypeScript syntax trees for exact rename/remove/usage analysis, using the TypeScript compiler the
2
+ // project already has (node_modules/typescript). codz itself has no runtime
3
+ // dependencies, so when the project has no TypeScript (or CODZ_NO_AST=1),
4
+ // callers fall back to the text scanner.
5
+ import { createRequire } from 'node:module';
6
+ import { join } from 'node:path';
7
+ import { exists } from "./fsutil.js";
8
+ const cache = new Map();
9
+ /** The project's TypeScript module, or codz's own in development, or null. */
10
+ export function loadTypeScript(root) {
11
+ if (process.env.CODZ_NO_AST === '1')
12
+ return null;
13
+ if (cache.has(root))
14
+ return cache.get(root);
15
+ let ts = null;
16
+ for (const base of [join(root, 'package.json'), import.meta.url]) {
17
+ try {
18
+ if (base.endsWith('package.json') && !exists(join(root, 'node_modules', 'typescript')))
19
+ continue;
20
+ const mod = createRequire(base)('typescript');
21
+ if (mod?.createSourceFile) {
22
+ ts = mod;
23
+ break;
24
+ }
25
+ }
26
+ catch {
27
+ // Not installed there.
28
+ }
29
+ }
30
+ cache.set(root, ts);
31
+ return ts;
32
+ }
33
+ function parse(ts, file, text) {
34
+ const kind = /\.tsx$/.test(file) ? ts.ScriptKind.TSX : /\.jsx?$|\.mjs$/.test(file) ? ts.ScriptKind.JSX : ts.ScriptKind.TS;
35
+ return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
36
+ }
37
+ /**
38
+ * Every place an identifier refers to the module-level binding `name`: calls,
39
+ * references, import and export specifiers, and `ns.name` through a namespace
40
+ * import. Skips property names on other objects, object keys, strings,
41
+ * comments, and code where a local variable or parameter shadows the name.
42
+ */
43
+ export function tsIdentifierUses(ts, file, text, name, opts = {}) {
44
+ const sf = parse(ts, file, text);
45
+ const namespaces = new Set();
46
+ for (const st of sf.statements) {
47
+ if (ts.isImportDeclaration(st) && st.importClause?.namedBindings && ts.isNamespaceImport(st.importClause.namedBindings))
48
+ namespaces.add(st.importClause.namedBindings.name.text);
49
+ }
50
+ const out = [];
51
+ // Does this function or block declare its own `name` (a parameter, or a
52
+ // variable, function or class directly inside it)? Nested blocks and
53
+ // functions are their own scopes and are checked when the walk gets there.
54
+ const declares = (node) => {
55
+ const items = [];
56
+ if (ts.isFunctionLike(node)) {
57
+ items.push(...(node.parameters ?? []));
58
+ if (node.body && ts.isBlock(node.body))
59
+ items.push(...node.body.statements);
60
+ }
61
+ else if (ts.isBlock(node))
62
+ items.push(...node.statements);
63
+ let found = false;
64
+ const visit = (n) => {
65
+ if (found)
66
+ return;
67
+ if ((ts.isParameter(n) || ts.isVariableDeclaration(n) || ts.isBindingElement(n)) && n.name && ts.isIdentifier(n.name) && n.name.text === name)
68
+ found = true;
69
+ else if (ts.isBlock(n) || ts.isFunctionLike(n) || ts.isClassLike(n)) {
70
+ if ((ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) && n.name?.text === name)
71
+ found = true;
72
+ return;
73
+ }
74
+ else
75
+ ts.forEachChild(n, visit);
76
+ };
77
+ items.forEach(visit);
78
+ return found;
79
+ };
80
+ const walk = (n, shadowed) => {
81
+ const scopeShadows = !shadowed && (ts.isFunctionLike(n) || ts.isBlock(n)) && n !== sf && declares(n);
82
+ const sh = shadowed || scopeShadows;
83
+ if (ts.isIdentifier(n) && n.text === name) {
84
+ const p = n.parent;
85
+ const at = n.getStart(sf);
86
+ const line = sf.getLineAndCharacterOfPosition(at).line + 1;
87
+ if (opts.declStart !== undefined && at === opts.declStart)
88
+ return;
89
+ if (ts.isImportSpecifier(p) || ts.isImportClause(p))
90
+ out.push({ index: at, line, kind: 'import' });
91
+ else if (ts.isExportSpecifier(p))
92
+ out.push({ index: at, line, kind: 'export' });
93
+ else if (ts.isPropertyAccessExpression(p) && p.name === n) {
94
+ if (ts.isIdentifier(p.expression) && namespaces.has(p.expression.text) && !sh)
95
+ out.push({ index: at, line, kind: ts.isCallExpression(p.parent) ? 'call' : 'reference' });
96
+ }
97
+ else if ((ts.isPropertyAssignment(p) || ts.isPropertySignature(p) || ts.isMethodDeclaration(p) || ts.isPropertyDeclaration(p) || ts.isMethodSignature(p)) && p.name === n) {
98
+ // An object key or class member with the same name: not a use.
99
+ }
100
+ else if (ts.isShorthandPropertyAssignment(p) && !sh)
101
+ out.push({ index: at, line, kind: 'reference' });
102
+ else if ((ts.isParameter(p) || ts.isVariableDeclaration(p) || ts.isFunctionDeclaration(p)) && p.name === n) {
103
+ // A declaration (the endpoint itself, or a local that shadows it).
104
+ }
105
+ else if (!sh)
106
+ out.push({ index: at, line, kind: ts.isCallExpression(p) && p.expression === n ? 'call' : 'reference' });
107
+ }
108
+ ts.forEachChild(n, (c) => walk(c, sh));
109
+ };
110
+ walk(sf, false);
111
+ return out;
112
+ }
@@ -0,0 +1,12 @@
1
+ // Shared types: the framework-neutral project map, the universal endpoint
2
+ // object that skills build, change sets, and the adapter interface.
3
+ export const MAP_SCHEMA_VERSION = 1;
4
+ export class ToolError extends Error {
5
+ code;
6
+ hint;
7
+ constructor(code, message, hint) {
8
+ super(message);
9
+ this.code = code;
10
+ this.hint = hint;
11
+ }
12
+ }