codz-cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/BETA.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +523 -2
  4. package/assets/gradle-wrapper/README.txt +2 -0
  5. package/assets/gradle-wrapper/gradle-wrapper.jar +0 -0
  6. package/assets/gradle-wrapper/gradlew +251 -0
  7. package/assets/gradle-wrapper/gradlew.bat +94 -0
  8. package/bin/codz.js +33 -0
  9. package/dist/adapters/android.js +321 -0
  10. package/dist/adapters/androidSetup.js +277 -0
  11. package/dist/adapters/common.js +114 -0
  12. package/dist/adapters/dartGen.js +83 -0
  13. package/dist/adapters/flutter.js +291 -0
  14. package/dist/adapters/flutterGen.js +204 -0
  15. package/dist/adapters/flutterScan.js +312 -0
  16. package/dist/adapters/flutterSetup.js +252 -0
  17. package/dist/adapters/flutterStyles.js +130 -0
  18. package/dist/adapters/generic.js +20 -0
  19. package/dist/adapters/getx.js +423 -0
  20. package/dist/adapters/index.js +28 -0
  21. package/dist/adapters/ios.js +440 -0
  22. package/dist/adapters/kotlinGen.js +110 -0
  23. package/dist/adapters/models.js +116 -0
  24. package/dist/adapters/nextjs.js +243 -0
  25. package/dist/adapters/nextjsGen.js +285 -0
  26. package/dist/adapters/nextjsScan.js +364 -0
  27. package/dist/adapters/nextjsSetup.js +119 -0
  28. package/dist/adapters/react.js +89 -0
  29. package/dist/adapters/reactNative.js +109 -0
  30. package/dist/adapters/reactNativeSetup.js +236 -0
  31. package/dist/adapters/reactQuery.js +89 -0
  32. package/dist/adapters/reactSetup.js +150 -0
  33. package/dist/adapters/source.js +165 -0
  34. package/dist/cli.js +1175 -0
  35. package/dist/core/cache.js +76 -0
  36. package/dist/core/changeset.js +404 -0
  37. package/dist/core/commands.js +239 -0
  38. package/dist/core/config.js +45 -0
  39. package/dist/core/diff.js +143 -0
  40. package/dist/core/events.js +41 -0
  41. package/dist/core/fsutil.js +122 -0
  42. package/dist/core/git.js +36 -0
  43. package/dist/core/jsonEdit.js +113 -0
  44. package/dist/core/merge.js +159 -0
  45. package/dist/core/proc.js +23 -0
  46. package/dist/core/safe.js +75 -0
  47. package/dist/core/templates.js +100 -0
  48. package/dist/core/tsAst.js +112 -0
  49. package/dist/core/types.js +12 -0
  50. package/dist/core/validateConfig.js +172 -0
  51. package/dist/core/workflow.js +72 -0
  52. package/dist/core/workspace.js +80 -0
  53. package/dist/core/yaml.js +126 -0
  54. package/dist/core/yamlDoc.js +367 -0
  55. package/dist/detector.js +95 -0
  56. package/dist/input/apiSpec.js +395 -0
  57. package/dist/input/curl.js +259 -0
  58. package/dist/input/endpoint.js +220 -0
  59. package/dist/input/har.js +120 -0
  60. package/dist/input/infer.js +174 -0
  61. package/dist/input/naming.js +145 -0
  62. package/dist/input/paste.js +130 -0
  63. package/dist/skills/androidSkeleton.js +292 -0
  64. package/dist/skills/androidTests.js +221 -0
  65. package/dist/skills/androidUi.js +561 -0
  66. package/dist/skills/androidViews.js +536 -0
  67. package/dist/skills/apiAdd.js +99 -0
  68. package/dist/skills/apiDiff.js +271 -0
  69. package/dist/skills/apiEdit.js +547 -0
  70. package/dist/skills/apiImport.js +127 -0
  71. package/dist/skills/apiList.js +35 -0
  72. package/dist/skills/apiSync.js +221 -0
  73. package/dist/skills/auth.js +1304 -0
  74. package/dist/skills/completion.js +142 -0
  75. package/dist/skills/config.js +93 -0
  76. package/dist/skills/create.js +358 -0
  77. package/dist/skills/doctor.js +304 -0
  78. package/dist/skills/doctorFix.js +214 -0
  79. package/dist/skills/explain.js +61 -0
  80. package/dist/skills/feature.js +418 -0
  81. package/dist/skills/featureEdit.js +225 -0
  82. package/dist/skills/featureLinks.js +179 -0
  83. package/dist/skills/init.js +66 -0
  84. package/dist/skills/iosUi.js +468 -0
  85. package/dist/skills/locale.js +335 -0
  86. package/dist/skills/mcp.js +175 -0
  87. package/dist/skills/mock.js +482 -0
  88. package/dist/skills/module.js +23 -0
  89. package/dist/skills/plans.js +66 -0
  90. package/dist/skills/prefill.js +84 -0
  91. package/dist/skills/reactNativeUi.js +388 -0
  92. package/dist/skills/reactUi.js +352 -0
  93. package/dist/skills/report.js +75 -0
  94. package/dist/skills/route.js +230 -0
  95. package/dist/skills/screen.js +597 -0
  96. package/dist/skills/screenForm.js +410 -0
  97. package/dist/skills/setup.js +28 -0
  98. package/dist/skills/setupDoctor.js +226 -0
  99. package/dist/skills/templatesCmd.js +32 -0
  100. package/dist/skills/testgen.js +331 -0
  101. package/dist/skills/theme.js +388 -0
  102. package/dist/starters/auth.js +144 -0
  103. package/dist/starters/commands.js +283 -0
  104. package/dist/starters/engine.js +315 -0
  105. package/dist/starters/home.js +42 -0
  106. package/dist/starters/http.js +93 -0
  107. package/dist/starters/manifest.js +285 -0
  108. package/dist/starters/paths.js +70 -0
  109. package/dist/starters/publish.js +150 -0
  110. package/dist/starters/registry.js +258 -0
  111. package/dist/starters/sign.js +72 -0
  112. package/dist/starters/source.js +98 -0
  113. package/dist/starters/strictJson.js +148 -0
  114. package/dist/starters/tar.js +180 -0
  115. package/docs/COMMANDS.md +527 -0
  116. package/examples/openapi.yaml +319 -0
  117. package/package.json +57 -4
@@ -0,0 +1,142 @@
1
+ // Shell completion. `codz completion bash|zsh|fish` prints a script that calls
2
+ // the hidden `codz __complete <words...>` command, so all three shells share
3
+ // one implementation, including endpoint names, plan ids and config keys.
4
+ import { readdirSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { loadMap, setCacheWrites, TOOL_DIR } from "../core/cache.js";
7
+ import { CONFIG_FILE, loadConfig } from "../core/config.js";
8
+ import { exists } from "../core/fsutil.js";
9
+ import { ALLOWED, knownKeys } from "../core/validateConfig.js";
10
+ import { CHANGE_FLAGS, COMMAND_LIST, FLAGS, GLOBAL_FLAGS } from "../core/commands.js";
11
+ import { TEMPLATES } from "../core/templates.js";
12
+ export const SHELLS = ['bash', 'zsh', 'fish'];
13
+ /** Commands, their subcommands and the flags each takes, from the command table. */
14
+ export const COMMANDS = {};
15
+ for (const c of COMMAND_LIST) {
16
+ const flags = [...c.flags, ...(c.change ? CHANGE_FLAGS : [])].filter((f) => f.length > 1).map((f) => `--${f}`);
17
+ const entry = (COMMANDS[c.cmd] ??= {});
18
+ if (c.sub)
19
+ (entry.subs ??= {})[c.sub] = flags;
20
+ else
21
+ entry.flags = flags;
22
+ }
23
+ const GLOBAL = GLOBAL_FLAGS.filter((f) => f.length > 1).map((f) => `--${f}`);
24
+ /** Flags whose value is a file or folder (the shell completes paths). */
25
+ const PATH_FLAGS = new Set(Object.entries(FLAGS).filter(([, f]) => f.path).map(([n]) => `--${n}`));
26
+ const VALUE_FLAGS = new Set(Object.entries(FLAGS).filter(([, f]) => f.value).map(([n]) => `--${n}`));
27
+ export const FILES = '__files__';
28
+ function endpointNames(root, adapter) {
29
+ return loadMap(root, adapter, loadConfig(root).conventions)
30
+ .then(({ map }) => map.endpoints.map((e) => e.name))
31
+ .catch(() => []);
32
+ }
33
+ function planIds(root) {
34
+ try {
35
+ return readdirSync(join(root, TOOL_DIR, 'plans')).filter((n) => n.endsWith('.json')).map((n) => n.slice(0, -5)).sort().reverse();
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ }
41
+ /**
42
+ * Candidates for the last word. `words` are the words after "codz", the last
43
+ * one being the word under the cursor (possibly empty).
44
+ */
45
+ export async function complete(words, root, adapter) {
46
+ setCacheWrites(false);
47
+ const cur = words[words.length - 1] ?? '';
48
+ const prev = words[words.length - 2];
49
+ const positional = words.slice(0, -1).filter((w, i, a) => !w.startsWith('-') && !(i > 0 && VALUE_FLAGS.has(a[i - 1])));
50
+ const [cmd, sub] = positional;
51
+ const spec = cmd ? COMMANDS[cmd] : undefined;
52
+ const hasProject = exists(join(root, CONFIG_FILE));
53
+ if (prev && VALUE_FLAGS.has(prev)) {
54
+ if (PATH_FLAGS.has(prev))
55
+ return [FILES];
56
+ const choices = FLAGS[prev.slice(2)]?.choices;
57
+ if (choices)
58
+ return choices;
59
+ if (prev === '--architecture')
60
+ return adapter.id === 'flutter' ? ['getx', 'riverpod', 'get_it'] : ['fetch-helper', 'plain-fetch'];
61
+ if (prev === '--only' && cmd === 'doctor')
62
+ return ['locales', 'api', 'urls', 'models'];
63
+ if (prev === '--api' && hasProject)
64
+ return endpointNames(root, adapter);
65
+ return [];
66
+ }
67
+ if (cur.startsWith('-')) {
68
+ const own = spec?.subs ? (sub ? spec.subs[sub] ?? [] : []) : spec?.flags ?? [];
69
+ return [...new Set([...own, ...GLOBAL])];
70
+ }
71
+ if (!cmd)
72
+ return Object.keys(COMMANDS);
73
+ if (spec?.subs && !sub)
74
+ return Object.keys(spec.subs);
75
+ const argIndex = positional.length - (spec?.subs ? 2 : 1); // 0 = first argument after the (sub)command
76
+ const kind = COMMAND_LIST.find((c) => c.cmd === cmd && c.sub === (spec?.subs ? sub : undefined))?.args;
77
+ if (kind === 'template')
78
+ return TEMPLATES.map((t) => t.id);
79
+ if (kind === 'shell')
80
+ return argIndex === 0 ? ['bash', 'zsh', 'fish'] : [];
81
+ if (kind === 'endpoints' && hasProject)
82
+ return endpointNames(root, adapter);
83
+ if (cmd === 'api' && ['show', 'remove', 'rename', 'update', 'wire'].includes(sub) && argIndex === 0 && hasProject)
84
+ return endpointNames(root, adapter);
85
+ if (cmd === 'api' && (sub === 'import' || sub === 'sync') && argIndex === 0)
86
+ return [FILES];
87
+ if (cmd === 'mock' && sub === 'add' && hasProject)
88
+ return endpointNames(root, adapter);
89
+ if ((cmd === 'plans' && sub === 'show') || cmd === 'apply')
90
+ return argIndex === 0 ? planIds(root) : [];
91
+ if (cmd === 'config' && (sub === 'get' || sub === 'set') && argIndex === 0)
92
+ return Object.keys(knownKeys(adapter.id));
93
+ if (cmd === 'config' && sub === 'set' && argIndex === 1)
94
+ return ALLOWED[adapter.id]?.[positional[2]] ?? [];
95
+ if (cmd === 'theme' && sub === 'set' && argIndex % 2 === 0)
96
+ return ['primary', 'secondary', 'accent', 'background', 'surface', 'error', 'radius', 'spacing', 'font'];
97
+ return [];
98
+ }
99
+ export function completionScript(shell) {
100
+ if (shell === 'bash') {
101
+ return `# codz completion for bash. Add to ~/.bashrc: eval "$(codz completion bash)"
102
+ _codz() {
103
+ local cur="\${COMP_WORDS[COMP_CWORD]}" out
104
+ out=$(codz __complete "\${COMP_WORDS[@]:1:COMP_CWORD}" 2>/dev/null)
105
+ if [ "$out" = "${FILES}" ]; then
106
+ COMPREPLY=($(compgen -f -- "$cur"))
107
+ else
108
+ local IFS=$'\\n'
109
+ COMPREPLY=($(compgen -W "$out" -- "$cur"))
110
+ fi
111
+ }
112
+ complete -o default -F _codz codz
113
+ `;
114
+ }
115
+ if (shell === 'zsh') {
116
+ return `#compdef codz
117
+ # codz completion for zsh. Add to ~/.zshrc (after compinit): source <(codz completion zsh)
118
+ _codz() {
119
+ local -a out
120
+ out=("\${(@f)$(codz __complete "\${words[@]:1:$((CURRENT-1))}" 2>/dev/null)}")
121
+ if [[ "\${out[1]}" == "${FILES}" ]]; then
122
+ _files
123
+ else
124
+ compadd -a out
125
+ fi
126
+ }
127
+ compdef _codz codz
128
+ `;
129
+ }
130
+ return `# codz completion for fish. Save as ~/.config/fish/completions/codz.fish:
131
+ # codz completion fish > ~/.config/fish/completions/codz.fish
132
+ function __codz_complete
133
+ set -l out (codz __complete (commandline -opc)[2..-1] (commandline -ct) 2>/dev/null)
134
+ if test "$out" = "${FILES}"
135
+ __fish_complete_path (commandline -ct)
136
+ else
137
+ printf '%s\\n' $out
138
+ end
139
+ end
140
+ complete -c codz -f -a '(__codz_complete)'
141
+ `;
142
+ }
@@ -0,0 +1,93 @@
1
+ // codz config: view, get and set conventions, and check the config file.
2
+ // `config set` edits codz.config.yaml in place (comments and order stay) and
3
+ // goes through the usual preview → apply → undo pipeline.
4
+ import { join } from 'node:path';
5
+ import { loadMap } from "../core/cache.js";
6
+ import { buildChangeSet } from "../core/changeset.js";
7
+ import { CONFIG_FILE, loadConfig } from "../core/config.js";
8
+ import { readText } from "../core/fsutil.js";
9
+ import { ToolError } from "../core/types.js";
10
+ import { ALLOWED, checkValue, closest, knownKeys, validateConfig } from "../core/validateConfig.js";
11
+ import { scalar } from "../core/yaml.js";
12
+ export async function configView(root, adapter) {
13
+ const cfg = loadConfig(root);
14
+ const { map } = await loadMap(root, adapter, cfg.conventions);
15
+ const docs = knownKeys(adapter.id);
16
+ const keys = [...new Set([...Object.keys(docs), ...Object.keys(map.conventions)])];
17
+ const rows = keys
18
+ .filter((k) => k in docs || map.conventions[k] !== undefined)
19
+ .map((key) => ({
20
+ key,
21
+ value: cfg.conventions[key] ?? map.conventions[key] ?? '',
22
+ source: (key in cfg.conventions ? 'config' : 'detected'),
23
+ description: docs[key] ?? '',
24
+ allowed: ALLOWED[adapter.id]?.[key],
25
+ }));
26
+ return { framework: adapter.id, file: cfg.exists ? CONFIG_FILE : null, rows };
27
+ }
28
+ export function configCheck(root) {
29
+ const text = readText(join(root, CONFIG_FILE));
30
+ if (text === null)
31
+ throw new ToolError('no_config', `There is no ${CONFIG_FILE} here.`, 'Create one with: codz init');
32
+ return { issues: validateConfig(text, root), file: CONFIG_FILE };
33
+ }
34
+ export function planConfigSet(root, adapter, key, value, opts = {}) {
35
+ const text = readText(join(root, CONFIG_FILE));
36
+ if (text === null)
37
+ throw new ToolError('no_config', `There is no ${CONFIG_FILE} here.`, 'Create one with: codz init');
38
+ const docs = knownKeys(adapter.id);
39
+ if (!(key in docs) && !opts.force) {
40
+ const close = closest(key, Object.keys(docs));
41
+ throw new ToolError('unknown_key', `"${key}" is not a ${adapter.id} convention.`, close ? `Did you mean ${close}?` : `Known keys: ${Object.keys(docs).join(', ')}. Pass --force to set it anyway.`);
42
+ }
43
+ const issue = checkValue(adapter.id, key, value, root);
44
+ if (issue?.level === 'error')
45
+ throw new ToolError('bad_value', issue.message, issue.hint);
46
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
47
+ const lines = text.split(/\r?\n/);
48
+ let convAt = lines.findIndex((l) => /^conventions:\s*(#.*)?$/.test(l) || /^conventions:\s*\{\}\s*$/.test(l));
49
+ if (convAt === -1) {
50
+ // No conventions block yet: add one at the end.
51
+ if (lines[lines.length - 1] === '')
52
+ lines.pop();
53
+ lines.push('conventions:');
54
+ convAt = lines.length - 1;
55
+ lines.push('');
56
+ }
57
+ else if (/\{\}/.test(lines[convAt]))
58
+ lines[convAt] = 'conventions:';
59
+ // The block runs while lines are indented (or blank/comment lines inside it).
60
+ let end = convAt + 1;
61
+ let found = -1;
62
+ let indent = ' ';
63
+ while (end < lines.length && (/^\s+\S/.test(lines[end]) || lines[end].trim() === '')) {
64
+ const m = /^(\s+)([^#\s][^:]*):/.exec(lines[end]);
65
+ if (m) {
66
+ indent = m[1];
67
+ if (m[2].trim() === key)
68
+ found = end;
69
+ }
70
+ end++;
71
+ }
72
+ // Do not swallow blank lines that follow the block.
73
+ while (end > convAt + 1 && lines[end - 1].trim() === '')
74
+ end--;
75
+ const rendered = `${indent}${key}: ${scalar(value)}`;
76
+ let before = null;
77
+ if (found !== -1) {
78
+ const old = /:\s*(.*)$/.exec(lines[found])?.[1] ?? '';
79
+ before = old.replace(/^'(.*)'$/, '$1');
80
+ const trailing = /\s+#.*$/.exec(lines[found].slice(lines[found].indexOf(':') + 1));
81
+ lines[found] = rendered + (trailing && !/['"]/.test(trailing[0]) ? trailing[0] : '');
82
+ }
83
+ else {
84
+ const insert = docs[key] ? [`${indent}# ${docs[key]}`, rendered] : [rendered];
85
+ lines.splice(end, 0, ...insert);
86
+ }
87
+ const next = lines.join(eol);
88
+ const summary = [found !== -1 ? `Set ${key}: ${before || "''"} → ${value || "'' (detect)"} in ${CONFIG_FILE}` : `Add ${key}: ${value || "''"} to ${CONFIG_FILE}`];
89
+ const warnings = issue ? [issue.message + (issue.hint ? ` ${issue.hint}` : '')] : [];
90
+ if (value === '')
91
+ warnings.push(`An empty ${key} means codz detects it from the code again.`);
92
+ return buildChangeSet(root, 'config set', { files: new Map([[CONFIG_FILE, next]]), summary, warnings, generated: [] });
93
+ }
@@ -0,0 +1,358 @@
1
+ // codz create: a new project, set up the way codz would set it up.
2
+ //
3
+ // 1. project type (Flutter, Next.js, ...) and its architecture
4
+ // 2. name and type-specific options (organization, Tailwind, ...)
5
+ // 3. the platform's own creator (flutter create, create-next-app) when it is
6
+ // installed and reachable, otherwise a minimal project codz writes itself
7
+ // 4. the architecture applied with `setup`, codz.config.yaml written,
8
+ // an optional theme color, dependencies installed, a first git commit
9
+ //
10
+ // Each type is one entry in PROJECT_TYPES, so more platforms plug in the same way.
11
+ import { checkBaseUrl } from "../core/safe.js";
12
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
13
+ import { dirname, join, resolve } from 'node:path';
14
+ import { addGradleWrapper, androidPackage, androidSkeleton } from "./androidSkeleton.js";
15
+ import { ADAPTERS } from "../adapters/index.js";
16
+ import { applyChangeSet } from "../core/changeset.js";
17
+ import { ToolError } from "../core/types.js";
18
+ import { DART_RESERVED } from "../input/naming.js";
19
+ import { detectProject, initFiles, writeInitFiles } from "./init.js";
20
+ import { planSetup } from "./setup.js";
21
+ import { planThemeSet } from "./theme.js";
22
+ import { runTool, toolMissing } from "../core/proc.js";
23
+ const NONE = { id: 'none', label: 'None', description: 'No API layer yet; set one up later with codz setup' };
24
+ function flutterSkeleton(o) {
25
+ const files = new Map();
26
+ files.set('pubspec.yaml', `name: ${o.name}\ndescription: "A new Flutter project."\npublish_to: 'none'\n\nversion: 1.0.0+1\n\nenvironment:\n sdk: ^3.5.0\n\ndependencies:\n flutter:\n sdk: flutter\n\n cupertino_icons: ^1.0.8\n\ndev_dependencies:\n flutter_test:\n sdk: flutter\n\n flutter_lints: ^4.0.0\n\nflutter:\n uses-material-design: true\n`);
27
+ files.set('analysis_options.yaml', 'include: package:flutter_lints/flutter.yaml\n');
28
+ files.set('.gitignore', '.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\nbuild/\n.idea/\n*.iml\n');
29
+ const title = o.name.split('_').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
30
+ files.set('lib/main.dart', [
31
+ "import 'package:flutter/material.dart';",
32
+ '',
33
+ 'void main() {',
34
+ ' runApp(const MyApp());',
35
+ '}',
36
+ '',
37
+ 'class MyApp extends StatelessWidget {',
38
+ ' const MyApp({super.key});',
39
+ '',
40
+ ' @override',
41
+ ' Widget build(BuildContext context) {',
42
+ ' return MaterialApp(',
43
+ ` title: '${title}',`,
44
+ ' theme: ThemeData(',
45
+ ' colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),',
46
+ ' useMaterial3: true,',
47
+ ' ),',
48
+ ` home: const HomePage(title: '${title}'),`,
49
+ ' );',
50
+ ' }',
51
+ '}',
52
+ '',
53
+ 'class HomePage extends StatelessWidget {',
54
+ ' const HomePage({super.key, required this.title});',
55
+ '',
56
+ ' final String title;',
57
+ '',
58
+ ' @override',
59
+ ' Widget build(BuildContext context) {',
60
+ ' return Scaffold(',
61
+ ' appBar: AppBar(title: Text(title)),',
62
+ " body: const Center(child: Text('Hello')),",
63
+ ' );',
64
+ ' }',
65
+ '}',
66
+ '',
67
+ ].join('\n'));
68
+ files.set('test/widget_test.dart', `import 'package:flutter_test/flutter_test.dart';\n\nimport 'package:${o.name}/main.dart';\n\nvoid main() {\n testWidgets('shows the home page', (WidgetTester tester) async {\n await tester.pumpWidget(const MyApp());\n expect(find.text('Hello'), findsOneWidget);\n });\n}\n`);
69
+ return files;
70
+ }
71
+ function nextSkeleton(o) {
72
+ const files = new Map();
73
+ const pkg = {
74
+ name: o.name,
75
+ version: '0.1.0',
76
+ private: true,
77
+ scripts: { dev: 'next dev', build: 'next build', start: 'next start', lint: 'next lint' },
78
+ dependencies: { next: '^15.0.0', react: '^19.0.0', 'react-dom': '^19.0.0' },
79
+ devDependencies: { '@types/node': '^20', '@types/react': '^19', '@types/react-dom': '^19', typescript: '^5', ...(o.tailwind ? { tailwindcss: '^4', '@tailwindcss/postcss': '^4' } : {}) },
80
+ };
81
+ files.set('package.json', JSON.stringify(pkg, null, 2) + '\n');
82
+ files.set('tsconfig.json', JSON.stringify({
83
+ compilerOptions: { target: 'ES2017', lib: ['dom', 'dom.iterable', 'esnext'], allowJs: true, skipLibCheck: true, strict: true, noEmit: true, esModuleInterop: true, module: 'esnext', moduleResolution: 'bundler', resolveJsonModule: true, isolatedModules: true, jsx: 'preserve', incremental: true, plugins: [{ name: 'next' }], paths: { '@/*': ['./*'] } },
84
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
85
+ exclude: ['node_modules'],
86
+ }, null, 2) + '\n');
87
+ files.set('next.config.mjs', '/** @type {import(\'next\').NextConfig} */\nconst nextConfig = {};\n\nexport default nextConfig;\n');
88
+ files.set('next-env.d.ts', '/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n');
89
+ files.set('.gitignore', '/node_modules\n/.next/\n/out/\n.env*.local\nnext-env.d.ts\n*.tsbuildinfo\n');
90
+ if (o.tailwind) {
91
+ files.set('postcss.config.mjs', 'const config = {\n plugins: { "@tailwindcss/postcss": {} },\n};\n\nexport default config;\n');
92
+ files.set('app/globals.css', '@import "tailwindcss";\n');
93
+ }
94
+ else
95
+ files.set('app/globals.css', ':root {\n font-family: system-ui, sans-serif;\n}\n');
96
+ files.set('app/layout.tsx', `import type { Metadata } from 'next';\nimport './globals.css';\n\nexport const metadata: Metadata = {\n title: '${o.name}',\n};\n\nexport default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {\n return (\n <html lang="en">\n <body>{children}</body>\n </html>\n );\n}\n`);
97
+ files.set('app/page.tsx', `export default function Home() {\n return (\n <main>\n <h1>${o.name}</h1>\n </main>\n );\n}\n`);
98
+ return files;
99
+ }
100
+ /** React Native: Expo (blank TypeScript template) or the React Native CLI template, without native folders. */
101
+ function reactNativeSkeleton(o) {
102
+ const files = new Map();
103
+ const expo = o.flavor !== 'bare';
104
+ const display = o.name.replace(/[-_.]+(\w)/g, (_m, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
105
+ const pkg = expo
106
+ ? {
107
+ name: o.name,
108
+ version: '1.0.0',
109
+ main: 'index.ts',
110
+ scripts: { start: 'expo start', android: 'expo start --android', ios: 'expo start --ios', test: 'jest' },
111
+ dependencies: { expo: '~52.0.0', 'expo-status-bar': '~2.0.0', react: '18.3.1', 'react-native': '0.76.5' },
112
+ devDependencies: { '@babel/core': '^7.25.0', '@types/react': '~18.3.12', typescript: '^5.3.3', jest: '^29.7.0', 'jest-expo': '~52.0.0' },
113
+ jest: { preset: 'jest-expo' },
114
+ private: true,
115
+ }
116
+ : {
117
+ name: o.name,
118
+ version: '0.0.1',
119
+ private: true,
120
+ scripts: { android: 'react-native run-android', ios: 'react-native run-ios', start: 'react-native start', test: 'jest', lint: 'eslint .' },
121
+ dependencies: { react: '18.3.1', 'react-native': '0.76.5' },
122
+ devDependencies: { '@babel/core': '^7.25.2', '@react-native/babel-preset': '0.76.5', '@react-native/metro-config': '0.76.5', '@react-native/typescript-config': '0.76.5', '@types/react': '^18.2.6', jest: '^29.6.3', typescript: '5.0.4' },
123
+ jest: { preset: 'react-native' },
124
+ engines: { node: '>=18' },
125
+ };
126
+ files.set('package.json', JSON.stringify(pkg, null, 2) + '\n');
127
+ files.set('tsconfig.json', JSON.stringify(expo ? { extends: 'expo/tsconfig.base', compilerOptions: { strict: true, paths: { '@/*': ['./src/*'] } } } : { extends: '@react-native/typescript-config/tsconfig.json' }, null, 2) + '\n');
128
+ files.set('app.json', JSON.stringify(expo ? { expo: { name: display, slug: o.name, version: '1.0.0', orientation: 'portrait', ios: { bundleIdentifier: `${o.org}.${o.name.replace(/[^a-zA-Z0-9]/g, '')}` }, android: { package: `${o.org}.${o.name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()}` } } } : { name: display, displayName: display }, null, 2) + '\n');
129
+ files.set('App.tsx', `import { StyleSheet, Text, View } from 'react-native';\n\nexport default function App() {\n return (\n <View style={styles.container}>\n <Text>${display}</Text>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n container: { flex: 1, alignItems: 'center', justifyContent: 'center' },\n});\n`);
130
+ if (expo) {
131
+ files.set('index.ts', "import { registerRootComponent } from 'expo';\n\nimport App from './App';\n\nregisterRootComponent(App);\n");
132
+ files.set('.gitignore', 'node_modules/\n.expo/\ndist/\nweb-build/\n.env*.local\n*.jks\n*.p8\n*.p12\n*.key\n*.mobileprovision\n');
133
+ }
134
+ else {
135
+ files.set('index.js', `import { AppRegistry } from 'react-native';\nimport App from './App';\nimport { name as appName } from './app.json';\n\nAppRegistry.registerComponent(appName, () => App);\n`);
136
+ files.set('babel.config.js', "module.exports = {\n presets: ['module:@react-native/babel-preset'],\n};\n");
137
+ files.set('metro.config.js', "const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');\n\nmodule.exports = mergeConfig(getDefaultConfig(__dirname), {});\n");
138
+ files.set('.gitignore', 'node_modules/\n.DS_Store\nbuild/\n*.log\nios/Pods/\n.gradle/\nlocal.properties\n*.keystore\n!debug.keystore\n');
139
+ }
140
+ return files;
141
+ }
142
+ const npmName = (name) => (/^[a-z0-9][a-z0-9._-]*$/.test(name) && name.length <= 214 ? null : 'Use lowercase letters, digits, dots, dashes and underscores, starting with a letter or digit (for example my-app).');
143
+ export const PROJECT_TYPES = [
144
+ {
145
+ id: 'flutter',
146
+ label: 'Flutter',
147
+ description: 'Android, iOS, web and desktop from one Dart codebase',
148
+ architectures: [
149
+ { id: 'riverpod', label: 'Riverpod', description: 'Dio through dioProvider; a provider per repository' },
150
+ { id: 'getx', label: 'GetX', description: 'GetX modules (binding, controller, view), Dio registered in an InitialBinding' },
151
+ { id: 'bloc', label: 'Bloc', description: 'Cubits for screens, get_it for Dio and repositories' },
152
+ { id: 'get_it', label: 'get_it', description: 'Service locator for Dio and repositories, no state library' },
153
+ { ...NONE, label: 'Plain Flutter', description: 'No state library or API layer; add one later with codz setup' },
154
+ ],
155
+ checkName: (n) => (!/^[a-z][a-z0-9_]*$/.test(n) ? 'Use lowercase letters, digits and underscores, starting with a letter (for example my_app).' : DART_RESERVED.has(n) ? `"${n}" is a Dart keyword.` : null),
156
+ needsOrg: true,
157
+ options: [],
158
+ creator: (o) => ({ cmd: 'flutter', args: ['create', '--org', o.org, '--project-name', o.name, o.name], label: 'flutter create' }),
159
+ skeleton: flutterSkeleton,
160
+ install: () => ({ cmd: 'flutter', args: ['pub', 'get'], label: 'flutter pub get' }),
161
+ framework: 'flutter',
162
+ },
163
+ {
164
+ id: 'nextjs',
165
+ label: 'Next.js',
166
+ description: 'React web app with the App Router and TypeScript',
167
+ architectures: [
168
+ { id: 'fetch-helper', label: 'Fetch helper', description: 'lib/api/client.ts with apiFetch<T>() and ApiError; API functions call it' },
169
+ { id: 'plain-fetch', label: 'Plain fetch', description: 'Each API function calls fetch with the base URL from the environment' },
170
+ NONE,
171
+ ],
172
+ checkName: npmName,
173
+ needsOrg: false,
174
+ options: ['tailwind'],
175
+ creator: (o) => ({
176
+ cmd: process.platform === 'win32' ? 'npx.cmd' : 'npx',
177
+ args: ['--yes', 'create-next-app@latest', o.name, '--ts', '--app', '--eslint', o.tailwind ? '--tailwind' : '--no-tailwind', '--no-src-dir', '--import-alias', '@/*', '--use-npm', '--skip-install', '--yes'],
178
+ label: 'create-next-app',
179
+ }),
180
+ skeleton: nextSkeleton,
181
+ install: () => ({ cmd: process.platform === 'win32' ? 'npm.cmd' : 'npm', args: ['install'], label: 'npm install' }),
182
+ framework: 'nextjs',
183
+ },
184
+ {
185
+ id: 'react-native',
186
+ label: 'React Native',
187
+ description: 'Android and iOS apps in TypeScript, with Expo or the React Native CLI',
188
+ architectures: [
189
+ { id: 'fetch-helper', label: 'Fetch helper', description: 'apiFetch<T>() in src/api, API URL in src/config/env.ts, React Navigation' },
190
+ { id: 'axios-react-query', label: 'axios + TanStack Query', description: 'An axios instance, a query hook per endpoint, React Navigation' },
191
+ { id: 'axios', label: 'axios', description: 'An axios instance in src/api, React Navigation' },
192
+ NONE,
193
+ ],
194
+ checkName: (n) => (/^[a-z][a-z0-9-]*$/.test(n) ? null : 'Use lowercase letters, digits and dashes, starting with a letter (for example shop-app).'),
195
+ needsOrg: true,
196
+ options: ['flavor'],
197
+ creator: (o) => o.flavor === 'bare'
198
+ ? { cmd: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['--yes', '@react-native-community/cli@latest', 'init', o.name.replace(/-(\w)/g, (_m, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase()), '--directory', o.name, '--package-name', `${o.org}.${o.name.replace(/-/g, '')}`, '--skip-install', '--skip-git-init'], label: 'React Native CLI (init)' }
199
+ : { cmd: process.platform === 'win32' ? 'npx.cmd' : 'npx', args: ['--yes', 'create-expo-app@latest', o.name, '--template', 'blank-typescript', '--no-install'], label: 'create-expo-app' },
200
+ skeleton: reactNativeSkeleton,
201
+ install: () => ({ cmd: process.platform === 'win32' ? 'npm.cmd' : 'npm', args: ['install'], label: 'npm install' }),
202
+ framework: 'react-native',
203
+ },
204
+ {
205
+ id: 'android',
206
+ label: 'Android (Kotlin)',
207
+ description: 'Native Android app in Kotlin with Jetpack Compose and Navigation Compose',
208
+ architectures: [
209
+ { id: 'retrofit-hilt', label: 'Retrofit + Hilt', description: 'Retrofit, kotlinx.serialization and OkHttp provided by a Hilt NetworkModule' },
210
+ { id: 'retrofit-koin', label: 'Retrofit + Koin', description: 'Retrofit and API interfaces in a Koin module, started in the Application class' },
211
+ { id: 'retrofit', label: 'Retrofit only', description: 'A ServiceLocator object holds Retrofit and the API interfaces; no DI library' },
212
+ { ...NONE, label: 'Plain Compose', description: 'Compose and Navigation only; add an API layer later with codz setup' },
213
+ ],
214
+ checkName: (n) => (/^[a-z][a-z0-9-]*$/.test(n) ? null : 'Use lowercase letters, digits and dashes, starting with a letter (for example shop-app).'),
215
+ needsOrg: true,
216
+ options: [],
217
+ // Android has no command-line project creator; codz writes the Empty Activity template itself.
218
+ creator: () => null,
219
+ skeleton: (o) => androidSkeleton(o),
220
+ install: () => null,
221
+ framework: 'android',
222
+ },
223
+ ];
224
+ export function projectType(id) {
225
+ const t = PROJECT_TYPES.find((x) => x.id === id);
226
+ if (!t)
227
+ throw new ToolError('bad_type', `Unknown project type "${id}".`, `Types: ${PROJECT_TYPES.map((x) => x.id).join(', ')}`);
228
+ return t;
229
+ }
230
+ /** What create will do, without doing it (--dry-run). */
231
+ export function describeCreate(o) {
232
+ const t = projectType(o.type);
233
+ const dir = resolve(o.parent, o.name);
234
+ const c = t.creator(o);
235
+ const steps = [`Create ${dir}`, o.offline || !c ? `Write a minimal ${t.label} project (codz's own files)` : `Run ${c.label}: ${c.cmd} ${c.args.join(' ')} (falls back to codz's minimal project if it is not available)`];
236
+ if (o.architecture !== 'none')
237
+ steps.push(`Set up the ${t.architectures.find((a) => a.id === o.architecture)?.label ?? o.architecture} architecture with base URL ${o.baseUrl || 'https://api.example.com'}`);
238
+ steps.push('Write codz.config.yaml');
239
+ if (o.primary)
240
+ steps.push(`Set the theme's primary color to ${o.primary}`);
241
+ if (o.install !== false)
242
+ steps.push(`Install dependencies (${t.install(o)?.label ?? 'none'})`);
243
+ if (o.git !== false)
244
+ steps.push('git init and a first commit');
245
+ return steps;
246
+ }
247
+ function validate(o, t) {
248
+ const bad = t.checkName(o.name);
249
+ if (bad)
250
+ throw new ToolError('bad_name', `"${o.name}" cannot be a ${t.label} project name. ${bad}`);
251
+ if (!t.architectures.some((a) => a.id === o.architecture))
252
+ throw new ToolError('bad_args', `Unknown architecture "${o.architecture}" for ${t.label}.`, `Choose one of: ${t.architectures.map((a) => a.id).join(', ')}`);
253
+ if (t.needsOrg && !/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(o.org))
254
+ throw new ToolError('bad_org', `"${o.org}" is not an organization id.`, 'Use reverse-domain form, for example com.example');
255
+ if (o.baseUrl)
256
+ o = { ...o, baseUrl: checkBaseUrl(o.baseUrl) };
257
+ if (o.primary && !/^#?[0-9a-fA-F]{6}$/.test(o.primary))
258
+ throw new ToolError('bad_color', `"${o.primary}" is not a #rrggbb color.`);
259
+ }
260
+ function run(cmd, args, cwd) {
261
+ const r = runTool(cmd, args, { cwd, timeout: 15 * 60_000 });
262
+ const missing = toolMissing(r);
263
+ return { ok: r.status === 0, missing, output: `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() };
264
+ }
265
+ export async function createProject(o, log = () => { }) {
266
+ const t = projectType(o.type);
267
+ validate(o, t);
268
+ const parent = resolve(o.parent);
269
+ const dir = join(parent, o.name);
270
+ if (existsSync(dir) && readdirSync(dir).length)
271
+ throw new ToolError('dir_not_empty', `${dir} already exists and is not empty.`, 'Pick another name, or remove that folder.');
272
+ mkdirSync(parent, { recursive: true });
273
+ const steps = [];
274
+ const warnings = [];
275
+ // 1. The project itself.
276
+ let created = 'skeleton';
277
+ const creator = o.offline ? null : t.creator(o);
278
+ if (creator) {
279
+ log(`Running ${creator.label}...`);
280
+ const r = run(creator.cmd, creator.args, parent);
281
+ if (r.ok) {
282
+ created = 'creator';
283
+ steps.push(`Created ${dir} with ${creator.label}`);
284
+ }
285
+ else {
286
+ warnings.push(r.missing ? `${creator.label} is not installed, so codz wrote a minimal ${t.label} project instead.` : `${creator.label} failed (${r.output.split('\n').slice(-1)[0] || 'no output'}), so codz wrote a minimal ${t.label} project instead.`);
287
+ }
288
+ }
289
+ if (created === 'skeleton') {
290
+ for (const [path, content] of t.skeleton(o)) {
291
+ mkdirSync(dirname(join(dir, path)), { recursive: true });
292
+ writeFileSync(join(dir, path), content);
293
+ }
294
+ steps.push(`Created ${dir} (codz's minimal ${t.label} project)`);
295
+ if (t.id === 'react-native')
296
+ warnings.push(o.flavor === 'bare' ? `There are no android/ or ios/ folders yet. With network access, create the project with the React Native CLI instead (codz create without --offline), or copy them from a fresh npx @react-native-community/cli init.` : 'Expo creates the android/ and ios/ folders when you need them: npx expo prebuild (or just run npx expo start with Expo Go).');
297
+ if (t.id === 'android') {
298
+ if (addGradleWrapper(dir))
299
+ steps.push('Added the Gradle wrapper (gradlew)');
300
+ else
301
+ warnings.push(`Gradle is not installed here, so there is no gradlew yet. Open the project in Android Studio (it adds the wrapper), or run gradle wrapper in ${o.name}/.`);
302
+ steps.push(`Package ${androidPackage(o.org, o.name)}`);
303
+ }
304
+ if (t.id === 'flutter')
305
+ warnings.push(`There are no android/ or ios/ folders yet. With Flutter installed, run this in ${o.name}/ to add them: flutter create --org ${o.org} --project-name ${o.name} .`);
306
+ }
307
+ // 2. Architecture and config.
308
+ const d = await detectProject(dir, {});
309
+ const adapter = ADAPTERS[t.framework];
310
+ if (o.architecture !== 'none') {
311
+ const { changeSet, plan } = planSetup(dir, adapter, d.map, { architecture: o.architecture, baseUrl: o.baseUrl });
312
+ applyChangeSet(dir, changeSet, { allowDirty: true, verify: false, format: false, adapter });
313
+ steps.push(...plan.summary.filter((s) => !/^Save conventions/.test(s)));
314
+ warnings.push(...plan.warnings);
315
+ }
316
+ else {
317
+ writeInitFiles(dir, initFiles(dir, adapter, d.map.conventions), false);
318
+ }
319
+ steps.push('Wrote codz.config.yaml');
320
+ // 3. Theme.
321
+ if (o.primary && t.id === 'android')
322
+ steps.push(`Theme: primary color ${o.primary}`);
323
+ else if (o.primary) {
324
+ const cs = await planThemeSet(dir, adapter, [{ name: 'primary', value: o.primary.startsWith('#') ? o.primary : `#${o.primary}` }]);
325
+ applyChangeSet(dir, cs, { allowDirty: true, verify: false, format: false, adapter });
326
+ steps.push(`Theme: primary color ${o.primary}`);
327
+ warnings.push(...cs.warnings);
328
+ }
329
+ // 4. Dependencies.
330
+ if (o.install !== false) {
331
+ const cmd = t.install(o);
332
+ if (cmd) {
333
+ log(`Running ${cmd.label}...`);
334
+ const r = run(cmd.cmd, cmd.args, dir);
335
+ if (r.ok)
336
+ steps.push(`Installed dependencies (${cmd.label})`);
337
+ else
338
+ warnings.push(`${cmd.label} ${r.missing ? 'is not available' : 'failed'}; run it yourself in ${o.name}/.`);
339
+ }
340
+ }
341
+ // 5. Git.
342
+ if (o.git !== false) {
343
+ const g = (...a) => run('git', a, dir);
344
+ if (existsSync(join(dir, '.git')) || g('init', '-q').ok) {
345
+ g('add', '-A');
346
+ // Your own git identity when one is set; a neutral one only when git has none.
347
+ const identity = g('config', 'user.email').ok ? [] : ['-c', 'user.name=codz', '-c', 'user.email=codz@localhost'];
348
+ const c = g(...identity, 'commit', '-qm', `Create ${o.name} with codz (${t.label}, ${o.architecture})`);
349
+ if (c.ok)
350
+ steps.push('git init and a first commit');
351
+ else
352
+ warnings.push('git init worked, but the first commit failed; commit yourself.');
353
+ }
354
+ else
355
+ warnings.push('git is not available, so the project is not a repository yet.');
356
+ }
357
+ return { dir, type: t.id, architecture: o.architecture, created, steps, warnings };
358
+ }