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,239 @@
1
+ // Every command and flag codz accepts, in one table. Help, per-command
2
+ // help, shell completion, the flag parser (which flags take a value, which
3
+ // repeat), unknown-flag errors and docs/COMMANDS.md are all generated from it,
4
+ // so they cannot drift apart. A test checks that every command the CLI
5
+ // handles is listed here.
6
+ export const FLAGS = {
7
+ // Global
8
+ json: { desc: 'Machine-readable output on stdout (for scripts and AI agents)' },
9
+ cwd: { value: '<folder>', desc: 'Run as if started in <folder>', path: true },
10
+ root: { value: '<folder>', desc: 'Project root (monorepos: which project)', path: true },
11
+ 'no-color': { desc: 'Plain output' },
12
+ help: { desc: 'Show help (for one command: codz <command> --help)' },
13
+ h: { desc: 'Same as --help' },
14
+ version: { desc: 'Print the version' },
15
+ v: { desc: 'Same as --version' },
16
+ // Change options
17
+ 'dry-run': { desc: 'Show the diff only' },
18
+ plan: { desc: 'Save the diff as a plan and print its id (apply later with codz apply)' },
19
+ yes: { desc: 'Apply without asking' },
20
+ y: { desc: 'Same as --yes' },
21
+ 'allow-dirty': { desc: 'Apply even if git has uncommitted changes' },
22
+ 'no-format': { desc: "Do not run the project's formatter" },
23
+ 'no-verify': { desc: "Do not run the project's analyzer after applying" },
24
+ commit: { desc: 'Commit the applied files' },
25
+ 'commit-message': { value: '<text>', desc: 'Message for --commit (default: one from the summary)' },
26
+ open: { desc: 'Open the diff in $EDITOR before asking' },
27
+ // api add input and options
28
+ curl: { value: "'<curl command>'", desc: 'A cURL command; secrets in it are removed, never written' },
29
+ url: { value: "'<url>'", desc: 'A URL (or "POST https://...")' },
30
+ method: { value: '<GET|POST|...>', desc: 'HTTP method (api add), or only these methods (api import)', choices: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] },
31
+ clipboard: { desc: 'Read the cURL command (and response) from the clipboard' },
32
+ sample: { value: '<file.json>', desc: 'Response sample; repeat for several samples', repeat: true, path: true },
33
+ 'request-sample': { value: '<file.json>', desc: 'Request body sample', path: true },
34
+ name: { value: '<functionName>', desc: 'Override the derived function name' },
35
+ path: { value: '</users/{id}>', desc: 'Path parameters (api add), route path (screen/route add), or a path filter (import/sync)' },
36
+ 'model-name': { value: '<Name>', desc: 'Name the response model yourself' },
37
+ 'no-unwrap': { desc: 'Keep a {"data": ...} envelope instead of returning data' },
38
+ into: { value: '<module>', desc: "Call the endpoint from that module's controller (GetX)" },
39
+ // import / sync
40
+ tag: { value: '<tag>', desc: 'Only operations with this tag (folder in Postman, host in HAR)', repeat: true },
41
+ only: { value: '<name>', desc: 'Only these operations (import), or only these checks (doctor)', repeat: true },
42
+ max: { value: '<n>', desc: 'Import at most n operations' },
43
+ list: { desc: 'Show what would be imported and stop' },
44
+ 'base-url': { value: '<url>', desc: "The API's base URL (setup), or only HAR requests under it (import/sync)" },
45
+ 'include-errors': { desc: 'HAR: keep failed (non-2xx) responses' },
46
+ 'skip-conflicts': { desc: 'Import what works and report endpoints that cannot be added' },
47
+ add: { desc: 'Also add operations the project does not have yet' },
48
+ remove: { desc: 'Also delete endpoints the spec no longer has' },
49
+ check: { desc: 'Report drift without writing; exit 1 when out of sync' },
50
+ // module / wire / screen / route
51
+ api: { value: '<endpoint>', desc: 'Endpoint to load (repeat for several in module add)', repeat: true },
52
+ initial: { desc: 'Make it the first screen' },
53
+ 'no-load': { desc: 'Do not call the load method from onInit' },
54
+ form: { desc: "A form that sends the endpoint's request body (POST, PUT, PATCH)" },
55
+ // feature add
56
+ spec: { value: '<file>', desc: 'OpenAPI, Swagger, Postman or HAR file to take the endpoints from', path: true },
57
+ 'no-forms': { desc: 'No create or edit form screens' },
58
+ 'no-tests': { desc: 'Do not generate tests' },
59
+ // api diff
60
+ format: { value: '<text|md|json|sarif>', desc: 'Report format (default text; md for a pull request comment, sarif for code scanning)', choices: ['text', 'md', 'json', 'sarif'] },
61
+ report: { value: '<file>', desc: 'Write the report to a file', path: true },
62
+ 'fail-on': { value: '<breaking|warning|any|none>', desc: 'Exit 1 at this level (default breaking)', choices: ['breaking', 'warning', 'any', 'none'] },
63
+ // auth add
64
+ 'login-path': { value: '</auth/login>', desc: 'Sign-in endpoint: POST { email, password }, answers with the token (default /auth/login)' },
65
+ 'me-path': { value: '</auth/me>', desc: 'Endpoint that returns the signed-in user (default /auth/me)' },
66
+ 'logout-path': { value: '</auth/logout>', desc: 'Sign-out endpoint, called on sign out (default /auth/logout)' },
67
+ 'refresh-path': { value: '</auth/refresh>', desc: 'Refresh endpoint: POST { refreshToken }; with it a 401 tries one refresh first (Flutter, React Native)' },
68
+ 'token-field': { value: '<name>', desc: 'Field of the login answer with the token (default token)' },
69
+ 'refresh-field': { value: '<name>', desc: 'Field with the refresh token (default refreshToken)' },
70
+ public: { value: '</about>', desc: 'Next.js: a page that stays public (besides /login)', repeat: true },
71
+ // mock / test
72
+ all: { desc: 'Every endpoint (mock add, test add), or every plan (plans clean)' },
73
+ 'keep-values': { desc: "Write --sample values exactly as given (no secret or personal-data scrubbing)" },
74
+ // locale
75
+ from: { value: '<lang>', desc: 'Language to copy keys from (default: the default language)' },
76
+ value: { value: 'lang="text"', desc: 'Text for one more language; repeat for several', repeat: true },
77
+ // setup / init
78
+ architecture: { value: '<id>', desc: 'Flutter: getx, riverpod, bloc, get_it. Next.js: fetch-helper, plain-fetch. React Native and React: fetch-helper, axios, axios-react-query. Android: retrofit-hilt, retrofit-koin, retrofit. iOS: urlsession' },
79
+ // create
80
+ type: { value: '<type>', desc: 'Project type: flutter, nextjs, react-native, android', choices: ['flutter', 'nextjs', 'react-native', 'android'] },
81
+ flavor: { value: '<expo|bare>', desc: 'React Native: Expo (default) or the React Native CLI', choices: ['expo', 'bare'] },
82
+ org: { value: '<com.example>', desc: 'Organization in reverse-domain form, for app ids (default com.example)' },
83
+ tailwind: { desc: 'Next.js: use Tailwind CSS' },
84
+ primary: { value: '<#rrggbb>', desc: "The theme's primary color" },
85
+ offline: { desc: "Do not run the platform's creator; write codz's minimal project" },
86
+ 'no-install': { desc: 'Do not install dependencies' },
87
+ 'no-git': { desc: 'Do not run git init' },
88
+ // doctor
89
+ strict: { desc: 'Exit 1 on warnings too' },
90
+ setup: { desc: 'Check that codz can run here: Node, git, config, formatter, analyzer' },
91
+ fix: { desc: 'Repair what is safe as one previewed change; list the rest' },
92
+ // explain / undo / config
93
+ rescan: { desc: 'Scan again instead of using the cached project map' },
94
+ force: { desc: 'Do it even when codz would normally refuse (see the command)' },
95
+ to: { value: '<id>', desc: 'Undo every change back to and including <id>' },
96
+ // codz new and the starter catalog
97
+ template: { value: '<id[@version]|folder>', desc: 'The starter: a catalog id (pin a version with @x.y.z) or a template folder' },
98
+ set: { value: '<key=value>', desc: "Answer one of the starter's questions; repeat for more", repeat: true },
99
+ dir: { value: '<folder>', desc: 'Parent folder for the new project (default: the current folder)', path: true },
100
+ framework: { value: '<name>', desc: 'Only starters for this framework', choices: ['flutter', 'nextjs', 'react-native', 'android'] },
101
+ state: { value: '<name>', desc: 'Only starters with this state management (riverpod, get_it, ...)' },
102
+ router: { value: '<name>', desc: 'Only starters with this router (go_router, ...)' },
103
+ http: { value: '<name>', desc: 'Only starters with this HTTP client (dio, fetch, ...)' },
104
+ cached: { desc: 'Use only starters and the catalog already downloaded on this machine (no network)' },
105
+ 'no-steps': { desc: 'Skip the setup steps after creating (flutter pub get, dart format, npm install)' },
106
+ owned: { desc: 'Only starters your account owns (and free ones)' },
107
+ free: { desc: 'Only free starters' },
108
+ refresh: { desc: 'Download the catalog again even if it has not changed' },
109
+ clear: { desc: 'Delete every downloaded starter' },
110
+ 'no-browser': { desc: 'Do not open the login page; print it only' },
111
+ };
112
+ export const GLOBAL_FLAGS = ['json', 'cwd', 'root', 'no-color', 'help', 'h', 'version', 'v'];
113
+ export const CHANGE_FLAGS = ['dry-run', 'plan', 'yes', 'y', 'allow-dirty', 'no-format', 'no-verify', 'commit', 'commit-message', 'open'];
114
+ export const COMMAND_LIST = [
115
+ { group: 'Start', cmd: 'create', usage: '[name] [--type <type>] [--architecture <id>]', summary: 'Create a new project and set it up: pick the type, architecture, name and options, then codz runs the platform creator and applies the architecture', flags: ['type', 'architecture', 'base-url', 'org', 'tailwind', 'flavor', 'primary', 'offline', 'no-install', 'no-git', 'dry-run', 'yes', 'y'], notes: ['Asks for anything not given as an option. Architectures: Flutter riverpod, getx, bloc, get_it, none; Next.js fetch-helper, plain-fetch, none; React Native fetch-helper, axios-react-query, axios, none (--flavor expo or bare); Android retrofit-hilt, retrofit-koin, retrofit, none (Kotlin, Jetpack Compose, version catalog). Without the platform creator (flutter, npx) or with --offline, codz writes a minimal project itself; Android always uses the built-in template (the Android Studio Empty Activity project plus Navigation Compose).'] },
116
+ { group: 'Start', cmd: 'init', usage: '[--root <folder>] [--force]', summary: 'Detect the framework, scan, write codz.config.yaml; offers to set up an API layer when there is none', flags: ['force', 'architecture', 'base-url', 'yes', 'y'] },
117
+ { group: 'Start', cmd: 'setup', usage: '[--architecture <id>] [--base-url <url>]', summary: 'Create the API layer for a fresh project', flags: ['architecture', 'base-url'], change: true },
118
+ { group: 'Start', cmd: 'explain', usage: '[--rescan]', summary: 'Show routes, endpoints, models, locales and conventions', flags: ['rescan'] },
119
+ { group: 'Endpoints', cmd: 'api', sub: 'add', usage: '[input] [options]', summary: 'Add an endpoint: model, function and registration', flags: ['curl', 'url', 'method', 'clipboard', 'sample', 'request-sample', 'name', 'path', 'model-name', 'no-unwrap', 'into', 'no-load'], change: true, notes: ['Input: --curl, --url, --clipboard, or a cURL command or URL pasted on stdin, optionally followed by "response:" and JSON (and "request:" and JSON). A response sample is strongly recommended.'] },
120
+ { group: 'Endpoints', cmd: 'api', sub: 'import', usage: '<file> [filters]', summary: 'Add every endpoint from an OpenAPI 3, Swagger 2, Postman or HAR file', flags: ['tag', 'only', 'path', 'method', 'max', 'list', 'base-url', 'include-errors', 'skip-conflicts'], change: true, args: 'file' },
121
+ { group: 'Endpoints', cmd: 'api', sub: 'sync', usage: '<file> [--add] [--remove] [--check]', summary: 'Bring endpoints in line with an API spec: update changed ones, report breaking changes', flags: ['add', 'remove', 'check', 'tag', 'path', 'base-url'], change: true, args: 'file' },
122
+ { group: 'Endpoints', cmd: 'api', sub: 'diff', usage: '<old spec> <new spec> [--format md] [--fail-on breaking]', summary: 'What changed between two API specs, what breaks, and the lines in this project that use it', flags: ['format', 'report', 'fail-on', 'tag', 'path'], notes: ['Breaking: an operation removed, a response field removed or retyped, a new path parameter, a new required request field. Warning: a field that can now be null, a removed query parameter, a retyped request field.', 'Inside a project each change lists the files and lines that use it. Works outside a project too.', 'CI: codz api diff main.yaml pr.yaml --format md --report api-diff.md (exit 1 on breaking changes); --format sarif for GitHub code scanning.'] },
123
+ { group: 'Endpoints', cmd: 'api', sub: 'update', usage: '<endpoint> --sample <file>', summary: 'Rebuild the response model from new samples', flags: ['sample', 'no-unwrap'], change: true, args: 'endpoint' },
124
+ { group: 'Endpoints', cmd: 'api', sub: 'remove', usage: '<endpoint>', summary: 'Remove an endpoint and list callers that break', flags: [], change: true, args: 'endpoint' },
125
+ { group: 'Endpoints', cmd: 'api', sub: 'rename', usage: '<endpoint> <newName>', summary: 'Rename an endpoint and its call sites', flags: [], change: true, args: 'endpoint' },
126
+ { group: 'Endpoints', cmd: 'api', sub: 'wire', usage: '<endpoint> --into <module>', summary: "Call an endpoint from a module's controller (GetX)", flags: ['into', 'no-load'], change: true, args: 'endpoint' },
127
+ { group: 'Endpoints', cmd: 'api', sub: 'list', usage: '[filter]', summary: 'Every endpoint: method, path, function, response, file', flags: [] },
128
+ { group: 'Endpoints', cmd: 'api', sub: 'show', usage: '<endpoint>', summary: 'One endpoint: signature, models, every place that uses it', flags: [], args: 'endpoint' },
129
+ { group: 'Endpoints', cmd: 'mock', sub: 'add', usage: '<endpoint>... | --all', summary: 'Mock responses for building screens before the backend', flags: ['all', 'sample', 'keep-values', 'force'], change: true, args: 'endpoints', notes: ['Next.js, React Native and Flutter. Not yet on Android.'] },
130
+ { group: 'Endpoints', cmd: 'test', sub: 'add', usage: '<endpoint>... | --all', summary: 'Unit tests for API functions with the network faked', flags: ['all'], change: true, args: 'endpoints', notes: ['Next.js: Vitest or Jest. React Native: Jest. Flutter: flutter_test with a fake Dio adapter. Android: JUnit with MockWebServer (adds the test dependencies).'] },
131
+ { group: 'Screens and app', cmd: 'screen', sub: 'add', usage: '<name> --api <endpoint> [--path /x/:id] [--form]', summary: 'A working screen (route, page, the call, loading, error, data), or a form with --form', flags: ['api', 'path', 'initial', 'form'], change: true, notes: ['Android: a Compose screen plus a ViewModel (Hilt, Koin or the service locator) with a UiState StateFlow, registered in the NavHost.'] },
132
+ { group: 'Screens and app', cmd: 'route', sub: 'add', usage: '<name> [--path /x/:id]', summary: 'Add a screen and its route (GetX module, GoRoute, Next.js page, React Navigation or Expo Router screen, Compose NavHost entry)', flags: ['path', 'initial'], change: true },
133
+ { group: 'Screens and app', cmd: 'feature', sub: 'add', usage: '<name> --spec <file> | --api <endpoint>...', summary: 'A whole feature in one change: endpoints, list, detail, create and edit screens, routes and tests', flags: ['spec', 'tag', 'path', 'api', 'base-url', 'architecture', 'initial', 'no-forms', 'no-tests', 'skip-conflicts', 'no-install'], change: true, notes: ['A project with no API layer yet is set up first, in the same change: the recommended architecture (or --architecture), with the base URL from --base-url or the spec.', 'Without --tag or --path, the endpoints are those whose path or tag names the feature (orders: /orders, /orders/{id}, /users/{id}/orders).', 'Screens: a list for a GET that returns a list, a detail for a GET with an id, a create form for a POST with a body, an edit form for a PUT or PATCH with a body. Other endpoints are added and listed.', 'Everything is planned in a private copy of the project and shown as one diff: one apply, one undo.'] },
134
+ { group: 'Screens and app', cmd: 'auth', sub: 'add', usage: '[--login-path /auth/login] [--me-path /auth/me]', summary: 'Sign-in for this project: secure token storage, the token on every request, sign out on 401, a login screen and a guard', flags: ['login-path', 'me-path', 'logout-path', 'refresh-path', 'token-field', 'refresh-field', 'public'], change: true, notes: ['Next.js: server actions, an HttpOnly session cookie, middleware that sends signed-out visitors to /login, withAuth() for server calls.', 'React Native: expo-secure-store (or react-native-keychain), the fetch helper or axios sends the token, AuthProvider and useAuth().', 'Flutter: flutter_secure_storage, a Dio interceptor (refresh or sign out on 401), AuthService, a go_router redirect.', 'Android (Hilt): EncryptedSharedPreferences, an OkHttp interceptor, AuthApi, AuthRepository, a login screen with a ViewModel.'] },
135
+ { group: 'Screens and app', cmd: 'module', sub: 'add', usage: '<name> [--api <endpoint>]', summary: 'Create a GetX module: binding, controller, view, route', flags: ['api', 'initial', 'no-load'], change: true },
136
+ { group: 'Screens and app', cmd: 'locale', sub: 'add', usage: '<lang> [--from en]', summary: 'Add a language: copies every key from the default one', flags: ['from'], change: true },
137
+ { group: 'Screens and app', cmd: 'locale', sub: 'key', usage: '<key> "<text>" [--value hi="..."]', summary: 'Add a text key to every language file', flags: ['value', 'force'], change: true },
138
+ { group: 'Screens and app', cmd: 'theme', sub: 'set', usage: '<name> <value> [...]', summary: 'Set a color (#hex), size (number) or font in the app theme', flags: [], change: true, args: 'theme', notes: ['Example: codz theme set primary "#6750A4" radius 12 font Inter'] },
139
+ { group: 'Screens and app', cmd: 'templates', sub: 'list', usage: '', summary: 'Which templates the project customizes', flags: [] },
140
+ { group: 'Screens and app', cmd: 'templates', sub: 'init', usage: '[id...]', summary: "Start custom templates from files that reproduce codz's output", flags: [], change: true, args: 'template' },
141
+ { group: 'Health', cmd: 'report', usage: '[--json]', summary: 'Usage summary for beta feedback: commands run, changes applied and undone, errors; no code, file names or URLs', flags: [], notes: ['Built from .codz/history.json and .codz/events.jsonl, which stay on this machine. CODZ_NO_EVENTS=1 stops recording commands.'] },
142
+ { group: 'Health', cmd: 'doctor', usage: '[--only locales,api,urls,models] [--strict] [--fix] [--setup]', summary: 'Check locales, unhandled API errors, untyped responses, hardcoded URLs and unused models', flags: ['only', 'strict', 'fix', 'setup', 'force'], change: true, notes: ['Exit 1 on errors (--strict: on warnings too). --fix repairs what is safe (missing locale keys, base-URL literals, unused models) as one previewed change. --setup checks Node, git, the config, the formatter and the analyzer instead.'] },
143
+ { group: 'Health', cmd: 'check', usage: '', summary: 'CI gate: config check + doctor; exit 1 on any error or warning', flags: [] },
144
+ { group: 'Setup', cmd: 'config', sub: 'show', usage: '', summary: 'The conventions codz uses, and where each came from', flags: [] },
145
+ { group: 'Setup', cmd: 'config', sub: 'get', usage: '<key>', summary: 'One convention', flags: [], args: 'configKey' },
146
+ { group: 'Setup', cmd: 'config', sub: 'set', usage: '<key> <value>', summary: "Change one convention in codz.config.yaml ('' = detect)", flags: ['force'], change: true, args: 'configKey' },
147
+ { group: 'Setup', cmd: 'config', sub: 'check', usage: '', summary: 'Find problems in codz.config.yaml (exit 1 on errors)', flags: [] },
148
+ { group: 'Setup', cmd: 'mcp', usage: '[--cwd <project>]', summary: 'Serve codz to AI agents and editors over the Model Context Protocol (stdio)', flags: [], notes: ['Every command becomes a tool (api add → api_add). Tools that edit code return a diff and a plan id; the apply tool applies it. Resources: codz://project and codz://commands.'] },
149
+ { group: 'Setup', cmd: 'completion', usage: 'bash|zsh|fish', summary: 'Print the shell completion script', flags: [], args: 'shell' },
150
+ { group: 'Starters (online)', cmd: 'new', usage: '[name] --template <id[@version]|folder>', summary: 'Create a project from a ready-made starter: from the catalog (free or bought) or a template folder', flags: ['template', 'org', 'dir', 'set', 'framework', 'state', 'router', 'http', 'cached', 'no-steps', 'dry-run', 'plan', 'yes', 'y'], notes: ['Shows every file and step first, then creates the folder in one step; codz undo inside the new project removes it again while it is unchanged. Catalog starters are checked against the signed catalog, and template scripts never run. Next: cd <name> && codz init.', 'This and the other starter commands are the only ones that use the network, and only to reach the starter catalog.'] },
151
+ { group: 'Starters (online)', cmd: 'starters', sub: 'list', usage: '[--framework flutter] [--free] [--owned]', summary: 'Starters in the catalog: id, latest version, framework, price, api add support', flags: ['framework', 'state', 'router', 'http', 'owned', 'free', 'refresh', 'cached'] },
152
+ { group: 'Starters (online)', cmd: 'starters', sub: 'show', usage: '<id>[@version]', summary: 'One starter: what it includes beyond codz create, tested SDKs, license, versions', flags: ['refresh', 'cached'] },
153
+ { group: 'Starters (online)', cmd: 'starters', sub: 'cache', usage: '[--clear]', summary: 'Starters downloaded on this machine (--clear deletes them)', flags: ['clear'] },
154
+ { group: 'Starters (online)', cmd: 'login', usage: '[--no-browser]', summary: 'Log in to the starter catalog (prints a code to approve in the browser); needed only for paid starters', flags: ['no-browser'], notes: ['The token is kept in ~/.codz/auth.json, readable only by you. In CI, set CODZ_TOKEN instead.'] },
155
+ { group: 'Starters (online)', cmd: 'logout', usage: '', summary: 'Log out: delete the token here and revoke it on the server', flags: [] },
156
+ { group: 'Starters (online)', cmd: 'whoami', usage: '', summary: 'The logged-in account and the starters it owns', flags: [] },
157
+ { group: 'Changes', cmd: 'plans', sub: 'list', usage: '', summary: 'Plans saved with --plan: ready, applied or stale', flags: [] },
158
+ { group: 'Changes', cmd: 'plans', sub: 'show', usage: '<id>', summary: "One plan's summary and diff", flags: [], args: 'plan' },
159
+ { group: 'Changes', cmd: 'plans', sub: 'clean', usage: '[--all]', summary: 'Delete applied and stale plans (--all: every plan)', flags: ['all'] },
160
+ { group: 'Changes', cmd: 'apply', usage: '<plan-id>', summary: 'Apply a plan saved with --plan, exactly as previewed', flags: ['allow-dirty', 'no-format', 'no-verify', 'commit', 'commit-message'], args: 'plan' },
161
+ { group: 'Changes', cmd: 'undo', usage: '[--to <id>] [--force]', summary: 'Revert the last applied change (or every change back to <id>)', flags: ['to', 'force', 'yes', 'y'] },
162
+ { group: 'Changes', cmd: 'history', usage: '[show <id>]', summary: "List applied changes, or show one change's files and diff", flags: [] },
163
+ ];
164
+ export const HISTORY_SUBS = ['show'];
165
+ /** The commands that take subcommands, and those subcommands. */
166
+ export function subcommands(cmd) {
167
+ return COMMAND_LIST.filter((c) => c.cmd === cmd && c.sub).map((c) => c.sub);
168
+ }
169
+ export function findCommand(cmd, sub) {
170
+ return COMMAND_LIST.find((c) => c.cmd === cmd && c.sub === sub) ?? COMMAND_LIST.find((c) => c.cmd === cmd && !c.sub) ?? (sub === undefined ? COMMAND_LIST.find((c) => c.cmd === cmd) : undefined);
171
+ }
172
+ /** Flags a command accepts, including global and change options. */
173
+ export function allowedFlags(spec) {
174
+ return [...new Set([...spec.flags, ...(spec.change ? CHANGE_FLAGS : []), ...GLOBAL_FLAGS])];
175
+ }
176
+ export const BOOLEAN_FLAGS = new Set(Object.entries(FLAGS).filter(([, f]) => !f.value).map(([n]) => n));
177
+ export const REPEATABLE_FLAGS = new Set(Object.entries(FLAGS).filter(([, f]) => f.repeat).map(([n]) => n));
178
+ const flagLabel = (n) => `--${n}${FLAGS[n]?.value ? ` ${FLAGS[n].value}` : ''}`;
179
+ const line = (left, right, width = 46) => (left.length < width ? ` ${left.padEnd(width)} ${right}` : ` ${left}\n ${' '.repeat(width)} ${right}`);
180
+ export function renderHelp(version, configFile) {
181
+ const groups = [...new Set(COMMAND_LIST.map((c) => c.group))];
182
+ const out = [`codz ${version}: understands your project, then changes it the same way every time.`, '', 'Usage: codz <command> [options] (codz <command> --help for its options)'];
183
+ for (const g of groups) {
184
+ out.push('', g);
185
+ for (const c of COMMAND_LIST.filter((x) => x.group === g))
186
+ out.push(line(`codz ${c.cmd}${c.sub ? ` ${c.sub}` : ''}${c.usage ? ` ${c.usage}` : ''}`, c.summary.replace('codz.config.yaml', configFile)));
187
+ }
188
+ out.push('', 'Change options (commands that edit code)');
189
+ for (const f of CHANGE_FLAGS.filter((n) => n !== 'y'))
190
+ out.push(line(flagLabel(f), FLAGS[f].desc, 24));
191
+ out.push('', 'Global');
192
+ for (const f of GLOBAL_FLAGS.filter((n) => !['h', 'v'].includes(n)))
193
+ out.push(line(flagLabel(f), FLAGS[f].desc, 24));
194
+ return out.join('\n') + '\n';
195
+ }
196
+ export function renderCommandHelp(spec) {
197
+ const name = `codz ${spec.cmd}${spec.sub ? ` ${spec.sub}` : ''}`;
198
+ const out = [`${name}${spec.usage ? ` ${spec.usage}` : ''}`, '', spec.summary];
199
+ const own = spec.flags.filter((f) => !CHANGE_FLAGS.includes(f) && !GLOBAL_FLAGS.includes(f));
200
+ if (own.length) {
201
+ out.push('', 'Options');
202
+ for (const f of own)
203
+ out.push(line(flagLabel(f), FLAGS[f].desc, 28));
204
+ }
205
+ for (const n of spec.notes ?? [])
206
+ out.push('', n);
207
+ if (spec.change)
208
+ out.push('', `Change options: ${CHANGE_FLAGS.filter((n) => n !== 'y').map((f) => `--${f}`).join(', ')}`);
209
+ out.push(`Global: ${GLOBAL_FLAGS.filter((n) => !['h', 'v'].includes(n)).map((f) => `--${f}`).join(', ')}`);
210
+ return out.join('\n') + '\n';
211
+ }
212
+ /** docs/COMMANDS.md. */
213
+ export function renderMarkdown(version) {
214
+ const out = ['# codz commands', '', `Generated from \`src/core/commands.ts\` (codz ${version}). Do not edit by hand; run \`npm run docs\`.`, ''];
215
+ for (const g of [...new Set(COMMAND_LIST.map((c) => c.group))]) {
216
+ out.push(`## ${g}`, '');
217
+ for (const c of COMMAND_LIST.filter((x) => x.group === g)) {
218
+ out.push(`### \`codz ${c.cmd}${c.sub ? ` ${c.sub}` : ''}${c.usage ? ` ${c.usage}` : ''}\``, '', c.summary + '.', '');
219
+ for (const n of c.notes ?? [])
220
+ out.push(n, '');
221
+ const own = c.flags.filter((f) => !CHANGE_FLAGS.includes(f) && !GLOBAL_FLAGS.includes(f));
222
+ if (own.length) {
223
+ out.push('| Option | |', '| --- | --- |');
224
+ for (const f of own)
225
+ out.push(`| \`${flagLabel(f).replace(/\|/g, '\\|')}\` | ${FLAGS[f].desc.replace(/\|/g, '\\|')}${FLAGS[f].repeat ? ' (repeatable)' : ''} |`);
226
+ out.push('');
227
+ }
228
+ if (c.change)
229
+ out.push('Accepts the change options.', '');
230
+ }
231
+ }
232
+ out.push('## Change options', '', '| Option | |', '| --- | --- |');
233
+ for (const f of CHANGE_FLAGS)
234
+ out.push(`| \`${flagLabel(f)}\` | ${FLAGS[f].desc} |`);
235
+ out.push('', '## Global options', '', '| Option | |', '| --- | --- |');
236
+ for (const f of GLOBAL_FLAGS)
237
+ out.push(`| \`${flagLabel(f)}\` | ${FLAGS[f].desc} |`);
238
+ return out.join('\n') + '\n';
239
+ }
@@ -0,0 +1,45 @@
1
+ // codz.config.yaml: the only file codz asks you to commit. It holds the
2
+ // conventions you confirmed, which override what the scanner detects.
3
+ import { writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { readText } from "./fsutil.js";
6
+ import { ToolError } from "./types.js";
7
+ import { parse, stringify } from "./yaml.js";
8
+ export const CONFIG_FILE = 'codz.config.yaml';
9
+ export function loadConfig(root) {
10
+ const text = readText(join(root, CONFIG_FILE));
11
+ if (text == null)
12
+ return { conventions: {}, exists: false };
13
+ let data;
14
+ try {
15
+ data = parse(text);
16
+ }
17
+ catch (e) {
18
+ throw new ToolError('bad_config', e.message, 'Fix that line, or see every problem with: codz config check');
19
+ }
20
+ const conventions = {};
21
+ const raw = data.conventions;
22
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
23
+ for (const [k, v] of Object.entries(raw))
24
+ if (typeof v === 'string' && v !== '')
25
+ conventions[k] = v;
26
+ }
27
+ const framework = typeof data.framework === 'string' ? data.framework : undefined;
28
+ return { framework, conventions, exists: true };
29
+ }
30
+ export function renderConfig(framework, conventions, descriptions) {
31
+ const header = [
32
+ '# codz configuration. Commit this file.',
33
+ '# Values below were detected by `codz init` (or chosen in `codz setup`).',
34
+ '# Correct any that are wrong: codz always uses these values instead of guessing again.',
35
+ '',
36
+ ].join('\n');
37
+ const comments = {};
38
+ for (const [k, d] of Object.entries(descriptions))
39
+ comments[`conventions.${k}`] = d;
40
+ const body = { framework, conventions: { ...conventions } };
41
+ return header + stringify(body, comments);
42
+ }
43
+ export function writeConfig(root, framework, conventions, descriptions) {
44
+ writeFileSync(join(root, CONFIG_FILE), renderConfig(framework, conventions, descriptions));
45
+ }
@@ -0,0 +1,143 @@
1
+ // Line-based unified diff (Myers algorithm), no dependencies.
2
+ function splitLines(text) {
3
+ if (text === '')
4
+ return [];
5
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
6
+ if (lines[lines.length - 1] === '')
7
+ lines.pop();
8
+ return lines;
9
+ }
10
+ export function diffLines(a, b) {
11
+ const n = a.length;
12
+ const m = b.length;
13
+ const max = n + m;
14
+ const offset = max;
15
+ const v = new Int32Array(2 * max + 2);
16
+ const trace = [];
17
+ let found = false;
18
+ for (let d = 0; d <= max && !found; d++) {
19
+ trace.push(v.slice());
20
+ for (let k = -d; k <= d; k += 2) {
21
+ let x;
22
+ if (k === -d || (k !== d && v[offset + k - 1] < v[offset + k + 1]))
23
+ x = v[offset + k + 1];
24
+ else
25
+ x = v[offset + k - 1] + 1;
26
+ let y = x - k;
27
+ while (x < n && y < m && a[x] === b[y]) {
28
+ x++;
29
+ y++;
30
+ }
31
+ v[offset + k] = x;
32
+ if (x >= n && y >= m) {
33
+ found = true;
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ // Backtrack.
39
+ const ops = [];
40
+ let x = n;
41
+ let y = m;
42
+ for (let d = trace.length - 1; d >= 0; d--) {
43
+ const vd = trace[d];
44
+ const k = x - y;
45
+ let prevK;
46
+ if (k === -d || (k !== d && vd[offset + k - 1] < vd[offset + k + 1]))
47
+ prevK = k + 1;
48
+ else
49
+ prevK = k - 1;
50
+ const prevX = vd[offset + prevK];
51
+ const prevY = prevX - prevK;
52
+ while (x > prevX && y > prevY) {
53
+ ops.push({ type: 'equal', line: a[x - 1] });
54
+ x--;
55
+ y--;
56
+ }
57
+ if (d > 0) {
58
+ if (x === prevX)
59
+ ops.push({ type: 'add', line: b[y - 1] });
60
+ else
61
+ ops.push({ type: 'del', line: a[x - 1] });
62
+ }
63
+ x = prevX;
64
+ y = prevY;
65
+ }
66
+ return ops.reverse();
67
+ }
68
+ const C = {
69
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
70
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
71
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
72
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
73
+ };
74
+ /** Returns a unified diff for one file; `before` null means a new file, `after` null a deleted one. */
75
+ export function unifiedDiff(path, before, after, opts = {}) {
76
+ const ctx = opts.context ?? 3;
77
+ const color = opts.color ?? false;
78
+ const paint = (fn, s) => (color ? fn(s) : s);
79
+ const a = splitLines(before ?? '');
80
+ const b = splitLines(after ?? '');
81
+ const ops = diffLines(a, b);
82
+ if (!ops.some((o) => o.type !== 'equal'))
83
+ return '';
84
+ const header = [paint(C.bold, `--- ${before === null ? '/dev/null' : `a/${path}`}`), paint(C.bold, `+++ ${after === null ? '/dev/null' : `b/${path}`}`)];
85
+ const hunks = [];
86
+ // Group changes into hunks with context.
87
+ let i = 0;
88
+ let aLine = 1;
89
+ let bLine = 1;
90
+ const positions = [];
91
+ for (const op of ops) {
92
+ positions.push({ aLine, bLine });
93
+ if (op.type !== 'add')
94
+ aLine++;
95
+ if (op.type !== 'del')
96
+ bLine++;
97
+ }
98
+ while (i < ops.length) {
99
+ if (ops[i].type === 'equal') {
100
+ i++;
101
+ continue;
102
+ }
103
+ let start = Math.max(0, i - ctx);
104
+ let end = i;
105
+ // Extend the hunk while changes are within 2*ctx of each other.
106
+ while (end < ops.length) {
107
+ if (ops[end].type !== 'equal') {
108
+ end++;
109
+ continue;
110
+ }
111
+ let run = 0;
112
+ while (end + run < ops.length && ops[end + run].type === 'equal')
113
+ run++;
114
+ if (end + run >= ops.length || run > ctx * 2) {
115
+ end = Math.min(ops.length, end + Math.min(run, ctx));
116
+ break;
117
+ }
118
+ end += run;
119
+ }
120
+ const slice = ops.slice(start, end);
121
+ const aStart = positions[start].aLine;
122
+ const bStart = positions[start].bLine;
123
+ const aCount = slice.filter((o) => o.type !== 'add').length;
124
+ const bCount = slice.filter((o) => o.type !== 'del').length;
125
+ const lines = [paint(C.cyan, `@@ -${aCount === 0 ? aStart - 1 : aStart},${aCount} +${bCount === 0 ? bStart - 1 : bStart},${bCount} @@`)];
126
+ for (const op of slice) {
127
+ if (op.type === 'equal')
128
+ lines.push(` ${op.line}`);
129
+ else if (op.type === 'add')
130
+ lines.push(paint(C.green, `+${op.line}`));
131
+ else
132
+ lines.push(paint(C.red, `-${op.line}`));
133
+ }
134
+ hunks.push(lines.join('\n'));
135
+ i = end;
136
+ start = end;
137
+ }
138
+ return [...header, ...hunks].join('\n') + '\n';
139
+ }
140
+ export function diffStats(before, after) {
141
+ const ops = diffLines(splitLines(before ?? ''), splitLines(after ?? ''));
142
+ return { added: ops.filter((o) => o.type === 'add').length, removed: ops.filter((o) => o.type === 'del').length };
143
+ }
@@ -0,0 +1,41 @@
1
+ // A local log of which codz commands ran and how they ended, for `codz
2
+ // report` (beta feedback). It holds the command name, success or the error
3
+ // code, and the time taken: never arguments, code, file names or URLs. It stays
4
+ // in the project's .codz/ folder and is never sent anywhere. It is written
5
+ // only where codz already has its .codz/ folder, and
6
+ // CODZ_NO_EVENTS=1 turns it off.
7
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { TOOL_DIR } from "./cache.js";
10
+ const MAX_EVENTS = 5000;
11
+ const eventsFile = (root) => join(root, TOOL_DIR, 'events.jsonl');
12
+ export function recordEvent(root, ev) {
13
+ if (process.env.CODZ_NO_EVENTS === '1')
14
+ return;
15
+ try {
16
+ // Only where codz already keeps its folder (made by the first scan or change); never a new folder.
17
+ if (!existsSync(join(root, TOOL_DIR)))
18
+ return;
19
+ const file = eventsFile(root);
20
+ appendFileSync(file, `${JSON.stringify(ev)}\n`);
21
+ // Keep the newest events only.
22
+ const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
23
+ if (lines.length > MAX_EVENTS * 1.2)
24
+ writeFileSync(file, `${lines.slice(-MAX_EVENTS).join('\n')}\n`);
25
+ }
26
+ catch {
27
+ // The log is a convenience; it never makes a command fail.
28
+ }
29
+ }
30
+ export function readEvents(root) {
31
+ try {
32
+ return readFileSync(eventsFile(root), 'utf8')
33
+ .split('\n')
34
+ .filter(Boolean)
35
+ .map((l) => JSON.parse(l))
36
+ .filter((e) => e && typeof e.cmd === 'string');
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ }
@@ -0,0 +1,122 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
3
+ import { join, relative, sep } from 'node:path';
4
+ export const IGNORED_DIRS = new Set([
5
+ 'node_modules',
6
+ '.git',
7
+ '.codz',
8
+ '.next',
9
+ '.dart_tool',
10
+ 'build',
11
+ 'dist',
12
+ 'out',
13
+ '.idea',
14
+ '.vscode',
15
+ 'coverage',
16
+ 'Pods',
17
+ '.gradle',
18
+ '.turbo',
19
+ '.vercel',
20
+ ]);
21
+ /** Files the operating system or editors drop into folders; never part of a project. */
22
+ export const IGNORED_FILES = new Set(['.DS_Store', 'Thumbs.db', 'ehthumbs.db', 'desktop.ini', 'Icon\r', '.directory']);
23
+ const isJunkFile = (name) => IGNORED_FILES.has(name) || name.startsWith('._') || name.endsWith('~') || /\.sw[po]$/.test(name);
24
+ export function hash(content) {
25
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
26
+ }
27
+ export function toPosix(p) {
28
+ return p.split(sep).join('/');
29
+ }
30
+ export function rel(root, abs) {
31
+ return toPosix(relative(root, abs));
32
+ }
33
+ export function readText(path) {
34
+ try {
35
+ return readFileSync(path, 'utf8');
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export function readJson(path) {
42
+ const text = readText(path);
43
+ if (text == null)
44
+ return null;
45
+ try {
46
+ return JSON.parse(text);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ export function isDir(path) {
53
+ try {
54
+ return statSync(path).isDirectory();
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ export function exists(path) {
61
+ return existsSync(path);
62
+ }
63
+ /**
64
+ * Walks a directory and returns relative posix paths of files, sorted so scans
65
+ * are deterministic. `extraIgnored` lets adapters skip more folders.
66
+ */
67
+ export function walk(root, start = '.', opts = {}) {
68
+ const out = [];
69
+ const ignored = new Set([...IGNORED_DIRS, ...(opts.extraIgnored ?? [])]);
70
+ const max = opts.maxFiles ?? 20000;
71
+ const base = join(root, start);
72
+ if (!isDir(base))
73
+ return out;
74
+ const stack = [base];
75
+ while (stack.length) {
76
+ const dir = stack.pop();
77
+ let entries;
78
+ try {
79
+ entries = readdirSync(dir).sort();
80
+ }
81
+ catch {
82
+ continue;
83
+ }
84
+ // Push directories in reverse so traversal order is alphabetical.
85
+ const dirs = [];
86
+ for (const name of entries) {
87
+ const abs = join(dir, name);
88
+ let st;
89
+ try {
90
+ st = statSync(abs);
91
+ }
92
+ catch {
93
+ continue;
94
+ }
95
+ if (st.isDirectory()) {
96
+ if (!ignored.has(name) && !name.startsWith('.'))
97
+ dirs.push(abs);
98
+ }
99
+ else if (isJunkFile(name)) {
100
+ continue;
101
+ }
102
+ else if (!opts.exts || opts.exts.some((e) => name.endsWith(e))) {
103
+ out.push(rel(root, abs));
104
+ if (out.length >= max)
105
+ return out.sort();
106
+ }
107
+ }
108
+ for (let i = dirs.length - 1; i >= 0; i--)
109
+ stack.push(dirs[i]);
110
+ }
111
+ return out.sort();
112
+ }
113
+ export function detectEol(text) {
114
+ const crlf = (text.match(/\r\n/g) ?? []).length;
115
+ const lf = (text.match(/\n/g) ?? []).length - crlf;
116
+ return crlf > lf ? '\r\n' : '\n';
117
+ }
118
+ /** Normalizes generated text (always \n) to the file's own line endings. */
119
+ export function withEol(text, eol) {
120
+ const normalized = text.replace(/\r\n/g, '\n');
121
+ return eol === '\n' ? normalized : normalized.replace(/\n/g, '\r\n');
122
+ }
@@ -0,0 +1,36 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ export function isGitRepo(root) {
3
+ const r = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: root, encoding: 'utf8' });
4
+ return r.status === 0 && r.stdout.trim() === 'true';
5
+ }
6
+ /**
7
+ * Files with uncommitted changes under `root`, relative to `root` (not to the
8
+ * repository), excluding codz's own folder. Works when the project is a
9
+ * subfolder of a larger repository, and with renames and unusual file names.
10
+ */
11
+ export function dirtyFiles(root) {
12
+ const prefix = spawnSync('git', ['rev-parse', '--show-prefix'], { cwd: root, encoding: 'utf8' });
13
+ if (prefix.status !== 0)
14
+ return [];
15
+ const sub = prefix.stdout.trim(); // "" at the repo root, "apps/web/" in a subfolder
16
+ const r = spawnSync('git', ['status', '--porcelain', '-z', '--untracked-files=normal', '--', '.'], { cwd: root, encoding: 'utf8' });
17
+ if (r.status !== 0)
18
+ return [];
19
+ const out = [];
20
+ const parts = r.stdout.split('\0');
21
+ for (let i = 0; i < parts.length; i++) {
22
+ const entry = parts[i];
23
+ if (entry.length < 4)
24
+ continue;
25
+ const status = entry.slice(0, 2);
26
+ const path = entry.slice(3);
27
+ // A rename or copy is followed by its original path; skip that.
28
+ if (status[0] === 'R' || status[0] === 'C')
29
+ i++;
30
+ const rel = sub && path.startsWith(sub) ? path.slice(sub.length) : path;
31
+ if (rel === '.codz' || rel.startsWith('.codz/'))
32
+ continue;
33
+ out.push(rel);
34
+ }
35
+ return out;
36
+ }