@syncular/cli 0.0.0-101

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.
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ describeDefaultInstallPath,
4
+ describeReleaseArtifactName,
5
+ ensureInstalledBinary,
6
+ shouldSkipInstall,
7
+ } from './shared.mjs';
8
+
9
+ async function main() {
10
+ if (shouldSkipInstall()) {
11
+ return;
12
+ }
13
+
14
+ await ensureInstalledBinary({ verbose: true });
15
+ console.error(
16
+ `[syncular] installed ${describeReleaseArtifactName()} -> ${describeDefaultInstallPath()}`
17
+ );
18
+ }
19
+
20
+ main().catch((error) => {
21
+ const message =
22
+ error instanceof Error ? error.message : `Unknown error: ${String(error)}`;
23
+ console.error(`[syncular] install warning: ${message}`);
24
+ console.error('[syncular] binary will be downloaded on first CLI execution');
25
+ process.exit(0);
26
+ });
package/npm/run.mjs ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { ensureInstalledBinary, resolvePackageVersion } from './shared.mjs';
4
+
5
+ async function main() {
6
+ const binaryPath = await ensureInstalledBinary({ verbose: false });
7
+ const packageVersion = resolvePackageVersion();
8
+ const result = spawnSync(binaryPath, process.argv.slice(2), {
9
+ env: {
10
+ ...process.env,
11
+ SYNCULAR_CLI_NPM_VERSION: packageVersion,
12
+ },
13
+ stdio: 'inherit',
14
+ });
15
+
16
+ if (result.error) {
17
+ throw result.error;
18
+ }
19
+
20
+ if (typeof result.status === 'number') {
21
+ process.exit(result.status);
22
+ }
23
+
24
+ process.exit(1);
25
+ }
26
+
27
+ main().catch((error) => {
28
+ const message =
29
+ error instanceof Error ? error.message : `Unknown error: ${String(error)}`;
30
+ console.error(`[syncular] failed to start CLI: ${message}`);
31
+ process.exit(1);
32
+ });
package/npm/shared.mjs ADDED
@@ -0,0 +1,271 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { createWriteStream, existsSync, readFileSync } from 'node:fs';
3
+ import { chmod, mkdir, rename, rm } from 'node:fs/promises';
4
+ import { arch, homedir, platform } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { Readable } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+
9
+ const RELEASE_REPOSITORY = 'syncular/syncular-cli-releases';
10
+ const PACKAGE_JSON_URL = new URL('../package.json', import.meta.url);
11
+ const USER_AGENT = '@syncular/cli';
12
+
13
+ function readPackageMetadata() {
14
+ const raw = readFileSync(PACKAGE_JSON_URL, 'utf8');
15
+ return JSON.parse(raw);
16
+ }
17
+
18
+ function resolveExecutableTarget() {
19
+ const key = `${platform()}/${arch()}`;
20
+ switch (key) {
21
+ case 'darwin/arm64':
22
+ return 'bun-darwin-arm64';
23
+ case 'darwin/x64':
24
+ return 'bun-darwin-x64';
25
+ case 'linux/arm64':
26
+ return 'bun-linux-arm64';
27
+ case 'linux/x64':
28
+ return 'bun-linux-x64';
29
+ case 'win32/x64':
30
+ return 'bun-windows-x64';
31
+ default:
32
+ throw new Error(
33
+ `Unsupported platform/arch "${key}". No prebuilt Syncular CLI binary is available.`
34
+ );
35
+ }
36
+ }
37
+
38
+ function resolveCacheDirectory() {
39
+ const override = process.env.SYNCULAR_CLI_CACHE_DIR?.trim();
40
+ if (override) {
41
+ return override;
42
+ }
43
+
44
+ const xdg = process.env.XDG_CACHE_HOME?.trim();
45
+ if (xdg) {
46
+ return join(xdg, 'syncular', 'cli');
47
+ }
48
+
49
+ const osPlatform = platform();
50
+ if (osPlatform === 'darwin') {
51
+ return join(homedir(), 'Library', 'Caches', 'syncular', 'cli');
52
+ }
53
+
54
+ if (osPlatform === 'win32') {
55
+ const localAppData = process.env.LOCALAPPDATA?.trim();
56
+ if (localAppData && localAppData.length > 0) {
57
+ return join(localAppData, 'syncular', 'cli');
58
+ }
59
+ return join(homedir(), 'AppData', 'Local', 'syncular', 'cli');
60
+ }
61
+
62
+ return join(homedir(), '.cache', 'syncular', 'cli');
63
+ }
64
+
65
+ function releaseBaseUrl(version) {
66
+ return `https://github.com/${RELEASE_REPOSITORY}/releases/download/v${version}`;
67
+ }
68
+
69
+ function binaryFileName(version, target) {
70
+ const base = `syncular-${version}-${target}`;
71
+ return target.includes('windows') ? `${base}.exe` : base;
72
+ }
73
+
74
+ function installedBinaryName() {
75
+ return platform() === 'win32' ? 'syncular.exe' : 'syncular';
76
+ }
77
+
78
+ function resolveInstalledBinaryPath(version, target) {
79
+ return join(
80
+ resolveCacheDirectory(),
81
+ version,
82
+ target,
83
+ installedBinaryName()
84
+ );
85
+ }
86
+
87
+ function sha256File(path) {
88
+ const digest = createHash('sha256');
89
+ digest.update(readFileSync(path));
90
+ return digest.digest('hex');
91
+ }
92
+
93
+ function wait(delayMs) {
94
+ return new Promise((resolve) => {
95
+ setTimeout(resolve, delayMs);
96
+ });
97
+ }
98
+
99
+ async function fetchResponse(url) {
100
+ const maxAttempts = 3;
101
+ let lastError = null;
102
+
103
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
104
+ try {
105
+ const response = await fetch(url, {
106
+ headers: { 'user-agent': USER_AGENT },
107
+ redirect: 'follow',
108
+ });
109
+
110
+ if (response.status >= 500 && attempt < maxAttempts) {
111
+ await wait(attempt * 300);
112
+ continue;
113
+ }
114
+
115
+ return response;
116
+ } catch (error) {
117
+ lastError = error;
118
+ if (attempt < maxAttempts) {
119
+ await wait(attempt * 300);
120
+ continue;
121
+ }
122
+ }
123
+ }
124
+
125
+ if (lastError instanceof Error) {
126
+ throw lastError;
127
+ }
128
+
129
+ throw new Error(`Failed to fetch ${url}`);
130
+ }
131
+
132
+ async function fetchJson(url) {
133
+ const response = await fetchResponse(url);
134
+
135
+ if (!response.ok) {
136
+ throw new Error(
137
+ `Failed to download ${url} (HTTP ${response.status} ${response.statusText})`
138
+ );
139
+ }
140
+
141
+ return response.json();
142
+ }
143
+
144
+ async function downloadFile(url, destinationPath) {
145
+ const response = await fetchResponse(url);
146
+
147
+ if (!response.ok || !response.body) {
148
+ throw new Error(
149
+ `Failed to download ${url} (HTTP ${response.status} ${response.statusText})`
150
+ );
151
+ }
152
+
153
+ const tempPath = `${destinationPath}.tmp-${process.pid}-${Date.now()}`;
154
+ await mkdir(dirname(destinationPath), { recursive: true });
155
+
156
+ try {
157
+ await pipeline(
158
+ Readable.fromWeb(response.body),
159
+ createWriteStream(tempPath, { mode: 0o755 })
160
+ );
161
+
162
+ try {
163
+ await rename(tempPath, destinationPath);
164
+ } catch (error) {
165
+ if (!existsSync(destinationPath)) {
166
+ throw error;
167
+ }
168
+ await rm(tempPath, { force: true });
169
+ }
170
+
171
+ if (platform() !== 'win32') {
172
+ await chmod(destinationPath, 0o755);
173
+ }
174
+ } catch (error) {
175
+ await rm(tempPath, { force: true });
176
+ throw error;
177
+ }
178
+ }
179
+
180
+ export function resolvePackageVersion() {
181
+ const metadata = readPackageMetadata();
182
+ const version =
183
+ typeof metadata.version === 'string' ? metadata.version.trim() : '';
184
+ if (version.length === 0) {
185
+ throw new Error('Package version is missing in package.json');
186
+ }
187
+ return version;
188
+ }
189
+
190
+ function resolveManifestEntry(manifest, target) {
191
+ const binaries = Array.isArray(manifest?.binaries) ? manifest.binaries : [];
192
+ const entry = binaries.find(
193
+ (candidate) => candidate && candidate.target === target
194
+ );
195
+
196
+ if (!entry || typeof entry.file !== 'string') {
197
+ const available = binaries
198
+ .map((candidate) =>
199
+ candidate && typeof candidate.target === 'string'
200
+ ? candidate.target
201
+ : ''
202
+ )
203
+ .filter((value) => value.length > 0)
204
+ .join(', ');
205
+ throw new Error(
206
+ `No CLI binary entry for target "${target}". Available targets: ${available}`
207
+ );
208
+ }
209
+
210
+ return entry;
211
+ }
212
+
213
+ export function shouldSkipInstall() {
214
+ if (process.env.SYNCULAR_CLI_SKIP_DOWNLOAD === '1') {
215
+ return true;
216
+ }
217
+
218
+ return resolvePackageVersion() === '0.0.0';
219
+ }
220
+
221
+ export async function ensureInstalledBinary(options = {}) {
222
+ const verbose = options.verbose === true;
223
+ const version = resolvePackageVersion();
224
+ const target = resolveExecutableTarget();
225
+ const destinationPath = resolveInstalledBinaryPath(version, target);
226
+
227
+ if (existsSync(destinationPath)) {
228
+ return destinationPath;
229
+ }
230
+
231
+ const baseUrl = releaseBaseUrl(version);
232
+ const manifestUrl = `${baseUrl}/manifest.json`;
233
+ const manifest = await fetchJson(manifestUrl);
234
+ const entry = resolveManifestEntry(manifest, target);
235
+ const expectedSha256 =
236
+ typeof entry.sha256 === 'string' ? entry.sha256.trim().toLowerCase() : '';
237
+ const artifactName = entry.file;
238
+ const artifactUrl = `${baseUrl}/${artifactName}`;
239
+
240
+ if (verbose) {
241
+ console.error(`[syncular] downloading ${artifactName}`);
242
+ console.error(`[syncular] source ${artifactUrl}`);
243
+ console.error(`[syncular] install path ${destinationPath}`);
244
+ }
245
+
246
+ await downloadFile(artifactUrl, destinationPath);
247
+
248
+ if (expectedSha256.length > 0) {
249
+ const actualSha256 = sha256File(destinationPath);
250
+ if (actualSha256 !== expectedSha256) {
251
+ await rm(destinationPath, { force: true });
252
+ throw new Error(
253
+ `Checksum mismatch for ${artifactName}: expected ${expectedSha256}, got ${actualSha256}`
254
+ );
255
+ }
256
+ }
257
+
258
+ return destinationPath;
259
+ }
260
+
261
+ export function describeDefaultInstallPath() {
262
+ const version = resolvePackageVersion();
263
+ const target = resolveExecutableTarget();
264
+ return resolveInstalledBinaryPath(version, target);
265
+ }
266
+
267
+ export function describeReleaseArtifactName() {
268
+ const version = resolvePackageVersion();
269
+ const target = resolveExecutableTarget();
270
+ return binaryFileName(version, target);
271
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@syncular/cli",
3
+ "version": "0.0.0-101",
4
+ "description": "Unified CLI for Syncular OSS and Spaces workflows",
5
+ "license": "MIT",
6
+ "author": "Benjamin Kniffler",
7
+ "homepage": "https://syncular.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/syncular/syncular-spaces.git",
11
+ "directory": "cli/bin"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/syncular/syncular-spaces/issues"
15
+ },
16
+ "private": false,
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "type": "module",
21
+ "bin": {
22
+ "syncular": "./npm/run.mjs",
23
+ "synclr": "./npm/run.mjs"
24
+ },
25
+ "files": [
26
+ "npm"
27
+ ],
28
+ "scripts": {
29
+ "postinstall": "node ./npm/install.mjs",
30
+ "tsgo": "tsgo --noEmit",
31
+ "test": "bun test test",
32
+ "doctor": "bun src/index.ts doctor",
33
+ "console": "bun src/index.ts console",
34
+ "login": "bun src/index.ts login",
35
+ "logout": "bun src/index.ts logout",
36
+ "whoami": "bun src/index.ts whoami",
37
+ "create:space": "bun src/index.ts create-space",
38
+ "create": "bun src/index.ts create",
39
+ "dev": "bun src/index.ts dev",
40
+ "typegen": "bun src/index.ts typegen",
41
+ "migrate:status": "bun src/index.ts migrate-status",
42
+ "migrate:up": "bun src/index.ts migrate-up",
43
+ "build": "bun src/index.ts build",
44
+ "eject": "bun src/index.ts eject",
45
+ "target": "bun src/index.ts target",
46
+ "deploy": "bun src/index.ts deploy",
47
+ "verify": "bun src/index.ts verify",
48
+ "deployments:list": "bun src/index.ts deployments",
49
+ "rollback": "bun src/index.ts rollback",
50
+ "release": "bun ../config/bin/publish.ts"
51
+ },
52
+ "devDependencies": {
53
+ "@syncular/cli-buildpack-contract-worker": "0.0.0-101",
54
+ "@syncular/cli-core": "0.0.0-101",
55
+ "@syncular/cli-runtime-dev": "0.0.0-101",
56
+ "@syncular/cli-template-spaces-app": "0.0.0-101",
57
+ "@syncular/cli-template-spaces-demo": "0.0.0-101",
58
+ "@syncular/cli-template-syncular-demo": "0.0.0-101",
59
+ "@syncular/cli-template-syncular-libraries": "0.0.0-101",
60
+ "cross-keychain": "^1.1.0",
61
+ "ink": "^6.8.0",
62
+ "react": "^19.2.4",
63
+ "syncular": "*",
64
+ "@types/bun": "^1.3.9",
65
+ "@types/react": "^19.2.14"
66
+ }
67
+ }