syncular 0.0.6-96 → 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 (186) hide show
  1. package/README.md +21 -7
  2. package/dist/cli.js +473 -0
  3. package/package.json +12 -297
  4. package/dist/client-plugin-encryption.d.ts +0 -2
  5. package/dist/client-plugin-encryption.d.ts.map +0 -1
  6. package/dist/client-plugin-encryption.js +0 -2
  7. package/dist/client-plugin-encryption.js.map +0 -1
  8. package/dist/client-react.d.ts +0 -2
  9. package/dist/client-react.d.ts.map +0 -1
  10. package/dist/client-react.js +0 -2
  11. package/dist/client-react.js.map +0 -1
  12. package/dist/client.d.ts +0 -2
  13. package/dist/client.d.ts.map +0 -1
  14. package/dist/client.js +0 -2
  15. package/dist/client.js.map +0 -1
  16. package/dist/console-server.d.ts +0 -2
  17. package/dist/console-server.d.ts.map +0 -1
  18. package/dist/console-server.js +0 -2
  19. package/dist/console-server.js.map +0 -1
  20. package/dist/console.d.ts +0 -2
  21. package/dist/console.d.ts.map +0 -1
  22. package/dist/console.js +0 -2
  23. package/dist/console.js.map +0 -1
  24. package/dist/core.d.ts +0 -2
  25. package/dist/core.d.ts.map +0 -1
  26. package/dist/core.js +0 -2
  27. package/dist/core.js.map +0 -1
  28. package/dist/dialect-better-sqlite3.d.ts +0 -2
  29. package/dist/dialect-better-sqlite3.d.ts.map +0 -1
  30. package/dist/dialect-better-sqlite3.js +0 -2
  31. package/dist/dialect-better-sqlite3.js.map +0 -1
  32. package/dist/dialect-bun-sqlite.d.ts +0 -2
  33. package/dist/dialect-bun-sqlite.d.ts.map +0 -1
  34. package/dist/dialect-bun-sqlite.js +0 -2
  35. package/dist/dialect-bun-sqlite.js.map +0 -1
  36. package/dist/dialect-d1.d.ts +0 -2
  37. package/dist/dialect-d1.d.ts.map +0 -1
  38. package/dist/dialect-d1.js +0 -2
  39. package/dist/dialect-d1.js.map +0 -1
  40. package/dist/dialect-electron-sqlite.d.ts +0 -2
  41. package/dist/dialect-electron-sqlite.d.ts.map +0 -1
  42. package/dist/dialect-electron-sqlite.js +0 -2
  43. package/dist/dialect-electron-sqlite.js.map +0 -1
  44. package/dist/dialect-expo-sqlite.d.ts +0 -2
  45. package/dist/dialect-expo-sqlite.d.ts.map +0 -1
  46. package/dist/dialect-expo-sqlite.js +0 -2
  47. package/dist/dialect-expo-sqlite.js.map +0 -1
  48. package/dist/dialect-libsql.d.ts +0 -2
  49. package/dist/dialect-libsql.d.ts.map +0 -1
  50. package/dist/dialect-libsql.js +0 -2
  51. package/dist/dialect-libsql.js.map +0 -1
  52. package/dist/dialect-neon.d.ts +0 -2
  53. package/dist/dialect-neon.d.ts.map +0 -1
  54. package/dist/dialect-neon.js +0 -2
  55. package/dist/dialect-neon.js.map +0 -1
  56. package/dist/dialect-pglite.d.ts +0 -2
  57. package/dist/dialect-pglite.d.ts.map +0 -1
  58. package/dist/dialect-pglite.js +0 -2
  59. package/dist/dialect-pglite.js.map +0 -1
  60. package/dist/dialect-react-native-nitro-sqlite.d.ts +0 -2
  61. package/dist/dialect-react-native-nitro-sqlite.d.ts.map +0 -1
  62. package/dist/dialect-react-native-nitro-sqlite.js +0 -2
  63. package/dist/dialect-react-native-nitro-sqlite.js.map +0 -1
  64. package/dist/dialect-sqlite3.d.ts +0 -2
  65. package/dist/dialect-sqlite3.d.ts.map +0 -1
  66. package/dist/dialect-sqlite3.js +0 -2
  67. package/dist/dialect-sqlite3.js.map +0 -1
  68. package/dist/dialect-wa-sqlite.d.ts +0 -2
  69. package/dist/dialect-wa-sqlite.d.ts.map +0 -1
  70. package/dist/dialect-wa-sqlite.js +0 -2
  71. package/dist/dialect-wa-sqlite.js.map +0 -1
  72. package/dist/encryption.d.ts +0 -2
  73. package/dist/encryption.d.ts.map +0 -1
  74. package/dist/encryption.js +0 -2
  75. package/dist/encryption.js.map +0 -1
  76. package/dist/index.d.ts +0 -2
  77. package/dist/index.d.ts.map +0 -1
  78. package/dist/index.js +0 -2
  79. package/dist/index.js.map +0 -1
  80. package/dist/migrations.d.ts +0 -2
  81. package/dist/migrations.d.ts.map +0 -1
  82. package/dist/migrations.js +0 -2
  83. package/dist/migrations.js.map +0 -1
  84. package/dist/observability-sentry.d.ts +0 -2
  85. package/dist/observability-sentry.d.ts.map +0 -1
  86. package/dist/observability-sentry.js +0 -2
  87. package/dist/observability-sentry.js.map +0 -1
  88. package/dist/relay.d.ts +0 -2
  89. package/dist/relay.d.ts.map +0 -1
  90. package/dist/relay.js +0 -2
  91. package/dist/relay.js.map +0 -1
  92. package/dist/server-cloudflare.d.ts +0 -2
  93. package/dist/server-cloudflare.d.ts.map +0 -1
  94. package/dist/server-cloudflare.js +0 -3
  95. package/dist/server-cloudflare.js.map +0 -1
  96. package/dist/server-dialect-postgres.d.ts +0 -2
  97. package/dist/server-dialect-postgres.d.ts.map +0 -1
  98. package/dist/server-dialect-postgres.js +0 -2
  99. package/dist/server-dialect-postgres.js.map +0 -1
  100. package/dist/server-dialect-sqlite.d.ts +0 -2
  101. package/dist/server-dialect-sqlite.d.ts.map +0 -1
  102. package/dist/server-dialect-sqlite.js +0 -2
  103. package/dist/server-dialect-sqlite.js.map +0 -1
  104. package/dist/server-hono.d.ts +0 -2
  105. package/dist/server-hono.d.ts.map +0 -1
  106. package/dist/server-hono.js +0 -2
  107. package/dist/server-hono.js.map +0 -1
  108. package/dist/server-service-worker.d.ts +0 -2
  109. package/dist/server-service-worker.d.ts.map +0 -1
  110. package/dist/server-service-worker.js +0 -2
  111. package/dist/server-service-worker.js.map +0 -1
  112. package/dist/server-storage-filesystem.d.ts +0 -2
  113. package/dist/server-storage-filesystem.d.ts.map +0 -1
  114. package/dist/server-storage-filesystem.js +0 -2
  115. package/dist/server-storage-filesystem.js.map +0 -1
  116. package/dist/server-storage-s3.d.ts +0 -2
  117. package/dist/server-storage-s3.d.ts.map +0 -1
  118. package/dist/server-storage-s3.js +0 -2
  119. package/dist/server-storage-s3.js.map +0 -1
  120. package/dist/server.d.ts +0 -2
  121. package/dist/server.d.ts.map +0 -1
  122. package/dist/server.js +0 -2
  123. package/dist/server.js.map +0 -1
  124. package/dist/testkit.d.ts +0 -2
  125. package/dist/testkit.d.ts.map +0 -1
  126. package/dist/testkit.js +0 -2
  127. package/dist/testkit.js.map +0 -1
  128. package/dist/transport-http.d.ts +0 -2
  129. package/dist/transport-http.d.ts.map +0 -1
  130. package/dist/transport-http.js +0 -2
  131. package/dist/transport-http.js.map +0 -1
  132. package/dist/transport-ws.d.ts +0 -2
  133. package/dist/transport-ws.d.ts.map +0 -1
  134. package/dist/transport-ws.js +0 -2
  135. package/dist/transport-ws.js.map +0 -1
  136. package/dist/typegen.d.ts +0 -2
  137. package/dist/typegen.d.ts.map +0 -1
  138. package/dist/typegen.js +0 -2
  139. package/dist/typegen.js.map +0 -1
  140. package/dist/ui-observable-universe.d.ts +0 -2
  141. package/dist/ui-observable-universe.d.ts.map +0 -1
  142. package/dist/ui-observable-universe.js +0 -2
  143. package/dist/ui-observable-universe.js.map +0 -1
  144. package/dist/ui.d.ts +0 -2
  145. package/dist/ui.d.ts.map +0 -1
  146. package/dist/ui.js +0 -2
  147. package/dist/ui.js.map +0 -1
  148. package/src/client-plugin-encryption.ts +0 -1
  149. package/src/client-react.ts +0 -1
  150. package/src/client.ts +0 -1
  151. package/src/console-server.ts +0 -1
  152. package/src/console-styles.css +0 -1
  153. package/src/console-styles.source.css +0 -1
  154. package/src/console.ts +0 -1
  155. package/src/core.ts +0 -1
  156. package/src/dialect-better-sqlite3.ts +0 -1
  157. package/src/dialect-bun-sqlite.ts +0 -1
  158. package/src/dialect-d1.ts +0 -1
  159. package/src/dialect-electron-sqlite.ts +0 -1
  160. package/src/dialect-expo-sqlite.ts +0 -1
  161. package/src/dialect-libsql.ts +0 -1
  162. package/src/dialect-neon.ts +0 -1
  163. package/src/dialect-pglite.ts +0 -1
  164. package/src/dialect-react-native-nitro-sqlite.ts +0 -1
  165. package/src/dialect-sqlite3.ts +0 -1
  166. package/src/dialect-wa-sqlite.ts +0 -1
  167. package/src/encryption.ts +0 -1
  168. package/src/index.ts +0 -1
  169. package/src/migrations.ts +0 -1
  170. package/src/observability-sentry.ts +0 -1
  171. package/src/relay.ts +0 -1
  172. package/src/server-cloudflare.ts +0 -2
  173. package/src/server-dialect-postgres.ts +0 -1
  174. package/src/server-dialect-sqlite.ts +0 -1
  175. package/src/server-hono.ts +0 -1
  176. package/src/server-service-worker.ts +0 -1
  177. package/src/server-storage-filesystem.ts +0 -1
  178. package/src/server-storage-s3.ts +0 -1
  179. package/src/server.ts +0 -1
  180. package/src/testkit.ts +0 -1
  181. package/src/transport-http.ts +0 -1
  182. package/src/transport-ws.ts +0 -1
  183. package/src/typegen.ts +0 -1
  184. package/src/ui-observable-universe.ts +0 -1
  185. package/src/ui-styles.css +0 -1
  186. package/src/ui.ts +0 -1
package/README.md CHANGED
@@ -1,19 +1,34 @@
1
1
  # syncular
2
2
 
3
- Umbrella package that re-exports the `@syncular/*` packages under a single import namespace.
3
+ CLI for Syncular app code generation. The `syncular` package ships only the
4
+ `syncular` command; all runtime libraries live in the scoped `@syncular/*`
5
+ packages (for example `@syncular/client`, `@syncular/server`,
6
+ `@syncular/client/react`).
4
7
 
5
- If you prefer, you can write imports like `syncular/client` instead of `@syncular/client`. Both are supported.
8
+ ## Usage
6
9
 
7
- ## Install
10
+ Run the app-facing generator with your package runner — no install required:
8
11
 
9
12
  ```bash
10
- npm install syncular
13
+ npx syncular codegen install
14
+ npx syncular generate --manifest-dir .
15
+ npx syncular generate --manifest-dir . --check
11
16
  ```
12
17
 
18
+ `syncular generate` refreshes `generated/syncular.codegen.json` from
19
+ `syncular.app.ts` (via `syncular-typegen`) and then runs the Rust
20
+ `syncular-codegen` binary to generate language clients.
21
+
22
+ When `syncular.app.ts` is absent and `generated/syncular.codegen.json` does not
23
+ exist, the command initializes a starter config from migrations before
24
+ generating clients. `syncular generate` can also install the Rust generator on
25
+ demand when Cargo is available; `syncular codegen install` prewarms the same
26
+ tool cache explicitly.
27
+
13
28
  ## Documentation
14
29
 
15
- - Quick start: https://syncular.dev/docs/introduction/quick-start
16
- - Installation: https://syncular.dev/docs/introduction/installation
30
+ - Quick start: https://syncular.dev/docs/start/quick-start
31
+ - CLI reference: https://syncular.dev/docs/reference/cli/generate
17
32
 
18
33
  ## Links
19
34
 
@@ -21,4 +36,3 @@ npm install syncular
21
36
  - Issues: https://github.com/syncular/syncular/issues
22
37
 
23
38
  > Status: Alpha. APIs and storage layouts may change between releases.
24
-
package/dist/cli.js ADDED
@@ -0,0 +1,473 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { spawn } from "node:child_process";
5
+ import {
6
+ accessSync,
7
+ constants,
8
+ existsSync,
9
+ mkdirSync,
10
+ readFileSync
11
+ } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { delimiter, dirname, join, resolve, sep } from "node:path";
14
+ import { fileURLToPath, pathToFileURL } from "node:url";
15
+ var DEFAULT_APP_FILE = "syncular.app.ts";
16
+ var DEFAULT_CODEGEN_CONFIG_FILE = "generated/syncular.codegen.json";
17
+ var SYNCULAR_CODEGEN_BIN = "syncular-codegen";
18
+ function usage() {
19
+ return `usage: syncular <command>
20
+
21
+ commands:
22
+ generate [--check] [--manifest-dir <path>] [--migrations-dir <path>] [--rust-output-dir <path>] [--app <path>]
23
+ codegen install [--version <version>] [--root <path>] [--force]
24
+
25
+ examples:
26
+ syncular generate
27
+ syncular generate --check
28
+ syncular generate --manifest-dir ./syncular-app --app ./syncular.app.ts
29
+ syncular codegen install
30
+ `;
31
+ }
32
+ function generateUsage() {
33
+ return `usage: syncular generate [--check] [--manifest-dir <path>] [--migrations-dir <path>] [--rust-output-dir <path>] [--app <path>]
34
+
35
+ Generates the Syncular app handoff and language clients in one app-facing command.
36
+
37
+ When <manifest-dir>/syncular.app.ts exists, or --app is provided, the typed
38
+ TypeScript app contract is used to refresh generated/syncular.codegen.json.
39
+ Rust-only apps can omit syncular.app.ts; when generated/syncular.codegen.json
40
+ is missing, syncular generate initializes it from migrations before generating
41
+ clients.
42
+
43
+ Use --check in CI to verify generated outputs are current without rewriting
44
+ files.
45
+ `;
46
+ }
47
+ function codegenInstallUsage() {
48
+ return `usage: syncular codegen install [--version <version>] [--root <path>] [--force]
49
+
50
+ Installs the Rust syncular-codegen binary with Cargo into Syncular's tool cache.
51
+
52
+ By default the installed crate version matches the installed syncular npm
53
+ package version. Use --root to install into a custom Cargo root, or set
54
+ SYNCULAR_CODEGEN_BIN to point syncular generate at a custom binary.
55
+ `;
56
+ }
57
+ function readOptionValue(argv, index, arg, name) {
58
+ if (arg === name) {
59
+ const value = argv[index + 1];
60
+ if (!value || value.startsWith("-")) {
61
+ throw new Error(`${name} requires a value`);
62
+ }
63
+ return { value, nextIndex: index + 1 };
64
+ }
65
+ const prefix = `${name}=`;
66
+ if (arg.startsWith(prefix)) {
67
+ const value = arg.slice(prefix.length);
68
+ if (value.length === 0) {
69
+ throw new Error(`${name} requires a value`);
70
+ }
71
+ return { value, nextIndex: index };
72
+ }
73
+ return null;
74
+ }
75
+ function parseSyncularCliArgs(argv) {
76
+ const [command, ...rest] = argv;
77
+ if (!command || command === "--help" || command === "-h" || command === "help") {
78
+ return { kind: "help" };
79
+ }
80
+ if (command === "codegen") {
81
+ const [subcommand, ...codegenArgs] = rest;
82
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
83
+ return { kind: "help", topic: "codegen-install" };
84
+ }
85
+ if (subcommand !== "install") {
86
+ throw new Error(`Unknown syncular codegen command: ${subcommand}
87
+
88
+ ${usage()}`);
89
+ }
90
+ if (codegenArgs.includes("--help") || codegenArgs.includes("-h")) {
91
+ return { kind: "help", topic: "codegen-install" };
92
+ }
93
+ return {
94
+ kind: "codegen-install",
95
+ options: parseCodegenInstallArgs(codegenArgs)
96
+ };
97
+ }
98
+ if (command !== "generate") {
99
+ throw new Error(`Unknown syncular command: ${command}
100
+
101
+ ${usage()}`);
102
+ }
103
+ const options = {
104
+ check: false,
105
+ manifestDir: "."
106
+ };
107
+ for (let index = 0;index < rest.length; index += 1) {
108
+ const arg = rest[index];
109
+ if (arg === "--help" || arg === "-h") {
110
+ return { kind: "help", topic: "generate" };
111
+ }
112
+ if (arg === "--check") {
113
+ options.check = true;
114
+ continue;
115
+ }
116
+ const manifestDir = readOptionValue(rest, index, arg, "--manifest-dir");
117
+ if (manifestDir) {
118
+ options.manifestDir = manifestDir.value;
119
+ index = manifestDir.nextIndex;
120
+ continue;
121
+ }
122
+ const migrationsDir = readOptionValue(rest, index, arg, "--migrations-dir");
123
+ if (migrationsDir) {
124
+ options.migrationsDir = migrationsDir.value;
125
+ index = migrationsDir.nextIndex;
126
+ continue;
127
+ }
128
+ const rustOutputDir = readOptionValue(rest, index, arg, "--rust-output-dir");
129
+ if (rustOutputDir) {
130
+ options.rustOutputDir = rustOutputDir.value;
131
+ index = rustOutputDir.nextIndex;
132
+ continue;
133
+ }
134
+ const app = readOptionValue(rest, index, arg, "--app");
135
+ if (app) {
136
+ options.app = app.value;
137
+ index = app.nextIndex;
138
+ continue;
139
+ }
140
+ throw new Error(`Unknown syncular generate option: ${arg}
141
+
142
+ ${generateUsage()}`);
143
+ }
144
+ return { kind: "generate", options };
145
+ }
146
+ function parseCodegenInstallArgs(args) {
147
+ const options = {
148
+ force: false
149
+ };
150
+ for (let index = 0;index < args.length; index += 1) {
151
+ const arg = args[index];
152
+ if (arg === "--force") {
153
+ options.force = true;
154
+ continue;
155
+ }
156
+ const version = readOptionValue(args, index, arg, "--version");
157
+ if (version) {
158
+ options.version = version.value;
159
+ index = version.nextIndex;
160
+ continue;
161
+ }
162
+ const root = readOptionValue(args, index, arg, "--root");
163
+ if (root) {
164
+ options.root = root.value;
165
+ index = root.nextIndex;
166
+ continue;
167
+ }
168
+ throw new Error(`Unknown syncular codegen install option: ${arg}
169
+
170
+ ${codegenInstallUsage()}`);
171
+ }
172
+ return options;
173
+ }
174
+ function resolveFrom(cwd, path) {
175
+ return resolve(cwd, path);
176
+ }
177
+ function buildGenerateSteps(options, context = {}) {
178
+ const cwd = context.cwd ?? process.cwd();
179
+ const env = context.env ?? process.env;
180
+ const fileExists = context.fileExists ?? existsSync;
181
+ const typegenBin = env.SYNCULAR_TYPEGEN_BIN ?? "syncular-typegen";
182
+ const codegenBin = env.SYNCULAR_CODEGEN_BIN ?? SYNCULAR_CODEGEN_BIN;
183
+ const manifestDir = resolveFrom(cwd, options.manifestDir);
184
+ const appPath = options.app ? resolveFrom(cwd, options.app) : resolveFrom(manifestDir, DEFAULT_APP_FILE);
185
+ const codegenConfigPath = resolveFrom(manifestDir, DEFAULT_CODEGEN_CONFIG_FILE);
186
+ const hasAppDefinition = options.app !== undefined || fileExists(appPath);
187
+ if (options.app !== undefined && !fileExists(appPath)) {
188
+ throw new Error(`Syncular app definition not found: ${appPath}. Create syncular.app.ts, pass the correct --app path, or omit --app for a Rust-only project that already has generated/syncular.codegen.json.`);
189
+ }
190
+ const steps = [];
191
+ if (hasAppDefinition) {
192
+ steps.push({
193
+ label: "Generate Syncular codegen config",
194
+ command: typegenBin,
195
+ args: [
196
+ "codegen-config",
197
+ "--app",
198
+ appPath,
199
+ "--out",
200
+ codegenConfigPath,
201
+ ...options.check ? ["--check"] : []
202
+ ]
203
+ });
204
+ }
205
+ if (!hasAppDefinition && !fileExists(codegenConfigPath)) {
206
+ steps.push({
207
+ label: "Initialize Syncular codegen config",
208
+ command: codegenBin,
209
+ args: [
210
+ "init",
211
+ "--manifest-dir",
212
+ manifestDir,
213
+ ...options.migrationsDir ? ["--migrations-dir", resolveFrom(cwd, options.migrationsDir)] : [],
214
+ ...options.check ? ["--check"] : []
215
+ ]
216
+ });
217
+ }
218
+ steps.push({
219
+ label: "Generate Syncular app clients",
220
+ command: codegenBin,
221
+ args: [
222
+ "--manifest-dir",
223
+ manifestDir,
224
+ ...options.migrationsDir ? ["--migrations-dir", resolveFrom(cwd, options.migrationsDir)] : [],
225
+ ...options.rustOutputDir ? ["--rust-output-dir", resolveFrom(cwd, options.rustOutputDir)] : [],
226
+ ...options.check ? ["--check"] : []
227
+ ]
228
+ });
229
+ return steps;
230
+ }
231
+ function readPackageVersion() {
232
+ try {
233
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
234
+ const version = packageJson.version?.trim();
235
+ return version && version !== "0.0.0" ? version : undefined;
236
+ } catch {
237
+ return;
238
+ }
239
+ }
240
+ function defaultCacheDir() {
241
+ if (process.env.SYNCULAR_CACHE_DIR) {
242
+ return process.env.SYNCULAR_CACHE_DIR;
243
+ }
244
+ if (process.platform === "win32" && process.env.LOCALAPPDATA) {
245
+ return join(process.env.LOCALAPPDATA, "Syncular");
246
+ }
247
+ if (process.platform === "darwin") {
248
+ return join(homedir(), "Library", "Caches", "syncular");
249
+ }
250
+ return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "syncular");
251
+ }
252
+ function defaultCodegenVersion(version) {
253
+ const resolved = version?.trim() || process.env.SYNCULAR_CODEGEN_VERSION?.trim() || readPackageVersion();
254
+ return resolved && resolved !== "0.0.0" ? resolved : undefined;
255
+ }
256
+ function defaultCodegenInstallRoot(version) {
257
+ return join(defaultCacheDir(), "codegen", version ?? "latest");
258
+ }
259
+ function codegenBinaryPath(root) {
260
+ return join(root, "bin", process.platform === "win32" ? `${SYNCULAR_CODEGEN_BIN}.exe` : SYNCULAR_CODEGEN_BIN);
261
+ }
262
+ function buildCodegenInstallArgs(options) {
263
+ return [
264
+ "install",
265
+ SYNCULAR_CODEGEN_BIN,
266
+ ...options.version ? ["--version", options.version] : [],
267
+ "--locked",
268
+ "--root",
269
+ options.root,
270
+ ...options.force ? ["--force"] : []
271
+ ];
272
+ }
273
+ function hasPathSeparator(command) {
274
+ return command.includes("/") || command.includes("\\");
275
+ }
276
+ function executableCandidates(command, env = process.env) {
277
+ if (hasPathSeparator(command)) {
278
+ return [command];
279
+ }
280
+ const pathDirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
281
+ const extensions = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
282
+ return pathDirs.flatMap((dir) => extensions.map((extension) => join(dir, `${command}${extension}`)));
283
+ }
284
+ function findExecutable(command, env = process.env) {
285
+ for (const candidate of executableCandidates(command, env)) {
286
+ try {
287
+ accessSync(candidate, constants.X_OK);
288
+ return candidate;
289
+ } catch {
290
+ if (process.platform === "win32" && existsSync(candidate)) {
291
+ return candidate;
292
+ }
293
+ }
294
+ }
295
+ return null;
296
+ }
297
+ function localRepoCodegenManifest() {
298
+ const cliPath = fileURLToPath(import.meta.url);
299
+ let current = dirname(cliPath);
300
+ while (true) {
301
+ const cargoManifest = join(current, "rust/Cargo.toml");
302
+ const codegenManifest = join(current, "rust/crates/codegen/Cargo.toml");
303
+ const syncularPackageDir = join(current, "packages/syncular");
304
+ if (existsSync(cargoManifest) && existsSync(codegenManifest) && cliPath.startsWith(`${syncularPackageDir}${sep}`)) {
305
+ return cargoManifest;
306
+ }
307
+ const parent = dirname(current);
308
+ if (parent === current) {
309
+ return null;
310
+ }
311
+ current = parent;
312
+ }
313
+ }
314
+ function codegenAutoInstallEnabled() {
315
+ const value = (process.env.SYNCULAR_CODEGEN_AUTO_INSTALL ?? "1").trim().toLowerCase();
316
+ return !["0", "false", "no", "off"].includes(value);
317
+ }
318
+ function missingCodegenMessage(version, root) {
319
+ const installCommand = version ? `npx syncular codegen install --version ${version}` : "npx syncular codegen install";
320
+ const cargoCommand = version ? `cargo install ${SYNCULAR_CODEGEN_BIN} --version ${version} --locked` : `cargo install ${SYNCULAR_CODEGEN_BIN} --locked`;
321
+ return [
322
+ `Required generator command not found: ${SYNCULAR_CODEGEN_BIN}.`,
323
+ `Run \`${installCommand}\` to install it into ${root},`,
324
+ `run \`${cargoCommand}\`,`,
325
+ `or set SYNCULAR_CODEGEN_BIN to an existing ${SYNCULAR_CODEGEN_BIN} binary.`
326
+ ].join(" ");
327
+ }
328
+ async function runProcess(command, args) {
329
+ await new Promise((resolvePromise, reject) => {
330
+ const child = spawn(command, args, {
331
+ stdio: "inherit",
332
+ env: process.env
333
+ });
334
+ child.on("error", (error) => {
335
+ if (error.code === "ENOENT") {
336
+ reject(new Error(`Required generator command not found: ${command}. Install it and ensure it is on PATH before running syncular generate.`));
337
+ return;
338
+ }
339
+ reject(error);
340
+ });
341
+ child.on("exit", (code) => {
342
+ if (code === 0) {
343
+ resolvePromise();
344
+ return;
345
+ }
346
+ reject(new Error(`${command} exited with status ${code ?? "unknown"}`));
347
+ });
348
+ });
349
+ }
350
+ async function installSyncularCodegen(options) {
351
+ const cargo = findExecutable("cargo");
352
+ if (!cargo) {
353
+ throw new Error(missingCodegenMessage(options.version, options.root));
354
+ }
355
+ mkdirSync(options.root, { recursive: true });
356
+ const args = buildCodegenInstallArgs(options);
357
+ console.log(`[syncular] Installing ${SYNCULAR_CODEGEN_BIN}`);
358
+ console.log(`$ ${[cargo, ...args].join(" ")}`);
359
+ await runProcess(cargo, args);
360
+ const binary = codegenBinaryPath(options.root);
361
+ if (!findExecutable(binary)) {
362
+ throw new Error(`${SYNCULAR_CODEGEN_BIN} install completed but ${binary} is not executable`);
363
+ }
364
+ return binary;
365
+ }
366
+ async function resolveStep(step) {
367
+ const explicitCodegenBin = process.env.SYNCULAR_CODEGEN_BIN;
368
+ const isCodegenStep = step.command === SYNCULAR_CODEGEN_BIN || explicitCodegenBin !== undefined && step.command === explicitCodegenBin;
369
+ if (!isCodegenStep) {
370
+ return step;
371
+ }
372
+ if (explicitCodegenBin) {
373
+ const explicitBinary = findExecutable(explicitCodegenBin);
374
+ if (!explicitBinary) {
375
+ throw new Error(`SYNCULAR_CODEGEN_BIN points to a missing or non-executable command: ${explicitCodegenBin}`);
376
+ }
377
+ return { ...step, command: explicitBinary };
378
+ }
379
+ const repoManifest = localRepoCodegenManifest();
380
+ if (repoManifest) {
381
+ return {
382
+ ...step,
383
+ command: "cargo",
384
+ args: [
385
+ "run",
386
+ "--quiet",
387
+ "--manifest-path",
388
+ repoManifest,
389
+ "-p",
390
+ SYNCULAR_CODEGEN_BIN,
391
+ "--",
392
+ ...step.args
393
+ ]
394
+ };
395
+ }
396
+ const version = defaultCodegenVersion();
397
+ const root = defaultCodegenInstallRoot(version);
398
+ const cachedBinary = codegenBinaryPath(root);
399
+ if (findExecutable(cachedBinary)) {
400
+ return { ...step, command: cachedBinary };
401
+ }
402
+ const pathBinary = findExecutable(SYNCULAR_CODEGEN_BIN);
403
+ if (pathBinary) {
404
+ return { ...step, command: pathBinary };
405
+ }
406
+ if (codegenAutoInstallEnabled() && findExecutable("cargo")) {
407
+ const installedBinary = await installSyncularCodegen({ version, root });
408
+ return { ...step, command: installedBinary };
409
+ }
410
+ throw new Error(missingCodegenMessage(version, root));
411
+ }
412
+ async function runStep(step) {
413
+ const resolvedStep = await resolveStep(step);
414
+ console.log(`[syncular] ${step.label}`);
415
+ console.log(`$ ${[resolvedStep.command, ...resolvedStep.args].join(" ")}`);
416
+ await runProcess(resolvedStep.command, resolvedStep.args);
417
+ }
418
+ async function runGenerateCommand(options) {
419
+ const steps = buildGenerateSteps(options);
420
+ for (const step of steps) {
421
+ await runStep(step);
422
+ }
423
+ }
424
+ async function runCodegenInstallCommand(options) {
425
+ const version = defaultCodegenVersion(options.version);
426
+ const root = resolve(options.root ?? defaultCodegenInstallRoot(version));
427
+ const binary = await installSyncularCodegen({
428
+ version,
429
+ root,
430
+ force: options.force
431
+ });
432
+ console.log(`[syncular] ${SYNCULAR_CODEGEN_BIN} installed at ${binary}`);
433
+ }
434
+ async function runSyncularCli(argv = process.argv.slice(2)) {
435
+ try {
436
+ const parsed = parseSyncularCliArgs(argv);
437
+ if (parsed.kind === "help") {
438
+ if (parsed.topic === "generate") {
439
+ console.log(generateUsage());
440
+ } else if (parsed.topic === "codegen-install") {
441
+ console.log(codegenInstallUsage());
442
+ } else {
443
+ console.log(usage());
444
+ }
445
+ return 0;
446
+ }
447
+ if (parsed.kind === "generate") {
448
+ await runGenerateCommand(parsed.options);
449
+ } else {
450
+ await runCodegenInstallCommand(parsed.options);
451
+ }
452
+ return 0;
453
+ } catch (error) {
454
+ const message = error instanceof Error ? error.message : String(error);
455
+ console.error(`[syncular] ${message}`);
456
+ return 1;
457
+ }
458
+ }
459
+ function isMainModule() {
460
+ const entrypoint = process.argv[1];
461
+ return entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href;
462
+ }
463
+ if (isMainModule()) {
464
+ process.exitCode = await runSyncularCli();
465
+ }
466
+ export {
467
+ runSyncularCli,
468
+ runGenerateCommand,
469
+ runCodegenInstallCommand,
470
+ parseSyncularCliArgs,
471
+ buildGenerateSteps,
472
+ buildCodegenInstallArgs
473
+ };