@syncular/cli 0.0.0-44 → 0.0.0-48

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/cli",
3
- "version": "0.0.0-44",
3
+ "version": "0.0.0-48",
4
4
  "description": "Unified CLI for Syncular OSS and Spaces workflows",
5
5
  "license": "MIT",
6
6
  "author": "Benjamin Kniffler",
@@ -19,13 +19,14 @@
19
19
  },
20
20
  "type": "module",
21
21
  "bin": {
22
- "syncular": "./src/index.ts",
23
- "synclr": "./src/index.ts"
22
+ "syncular": "./npm/run.mjs",
23
+ "synclr": "./npm/run.mjs"
24
24
  },
25
25
  "files": [
26
- "src"
26
+ "npm"
27
27
  ],
28
28
  "scripts": {
29
+ "postinstall": "node ./npm/install.mjs",
29
30
  "tsgo": "tsgo --noEmit",
30
31
  "test": "bun test test",
31
32
  "doctor": "bun src/index.ts doctor",
@@ -48,20 +49,18 @@
48
49
  "rollback": "bun src/index.ts rollback",
49
50
  "release": "bun ../config/bin/publish.ts"
50
51
  },
51
- "dependencies": {
52
- "@syncular/cli-buildpack-contract-worker": "0.0.0-44",
53
- "@syncular/cli-core": "0.0.0-44",
54
- "@syncular/cli-runtime-dev": "0.0.0-44",
55
- "@syncular/cli-template-spaces-app": "0.0.0-44",
56
- "@syncular/cli-template-spaces-demo": "0.0.0-44",
57
- "@syncular/cli-template-syncular-demo": "0.0.0-44",
58
- "@syncular/cli-template-syncular-libraries": "0.0.0-44",
52
+ "devDependencies": {
53
+ "@syncular/cli-buildpack-contract-worker": "0.0.0-48",
54
+ "@syncular/cli-core": "0.0.0-48",
55
+ "@syncular/cli-runtime-dev": "0.0.0-48",
56
+ "@syncular/cli-template-spaces-app": "0.0.0-48",
57
+ "@syncular/cli-template-spaces-demo": "0.0.0-48",
58
+ "@syncular/cli-template-syncular-demo": "0.0.0-48",
59
+ "@syncular/cli-template-syncular-libraries": "0.0.0-48",
59
60
  "cross-keychain": "^1.1.0",
60
61
  "ink": "^6.8.0",
61
62
  "react": "^19.2.4",
62
- "syncular": "*"
63
- },
64
- "devDependencies": {
63
+ "syncular": "*",
65
64
  "@types/bun": "^1.3.9",
66
65
  "@types/react": "^19.2.14"
67
66
  }
package/src/args.ts DELETED
@@ -1,112 +0,0 @@
1
- import process from 'node:process';
2
- import type { ParsedArgs, RootCommand } from './types';
3
-
4
- const IMPLICIT_TRUE_FLAGS = new Set([
5
- '--help',
6
- '--version',
7
- '--interactive',
8
- '--no-interactive',
9
- '--forms',
10
- '--json',
11
- '--force',
12
- '--watch',
13
- '--open',
14
- '--dry-run',
15
- '--yes',
16
- ]);
17
-
18
- export function parseArgs(argv: string[]): ParsedArgs {
19
- const flags = new Set<string>();
20
- const flagValues = new Map<string, string>();
21
- const positionals: string[] = [];
22
-
23
- for (let index = 0; index < argv.length; index += 1) {
24
- const arg = argv[index];
25
- if (!arg) {
26
- continue;
27
- }
28
-
29
- if (!arg.startsWith('-')) {
30
- positionals.push(arg);
31
- continue;
32
- }
33
-
34
- if (arg.startsWith('--')) {
35
- const eqIndex = arg.indexOf('=');
36
- if (eqIndex > 0) {
37
- const key = arg.slice(0, eqIndex);
38
- const value = arg.slice(eqIndex + 1);
39
- flags.add(key);
40
- flagValues.set(key, value);
41
- continue;
42
- }
43
-
44
- flags.add(arg);
45
- const next = argv[index + 1];
46
- if (
47
- typeof next === 'string' &&
48
- next.length > 0 &&
49
- !next.startsWith('-')
50
- ) {
51
- flagValues.set(arg, next);
52
- index += 1;
53
- } else if (IMPLICIT_TRUE_FLAGS.has(arg)) {
54
- flagValues.set(arg, 'true');
55
- }
56
- continue;
57
- }
58
-
59
- flags.add(arg);
60
- }
61
-
62
- const commandCandidate = positionals[0] ?? null;
63
- const subcommand = positionals[1] ?? null;
64
-
65
- return {
66
- command: normalizeRootCommand(commandCandidate),
67
- subcommand,
68
- flags,
69
- flagValues,
70
- positionals,
71
- };
72
- }
73
-
74
- function normalizeRootCommand(value: string | null): RootCommand | null {
75
- if (!value) {
76
- return null;
77
- }
78
-
79
- if (value === 'help') return 'help';
80
- if (value === 'version') return 'version';
81
- if (value === 'doctor') return 'doctor';
82
- if (value === 'console') return 'console';
83
- if (value === 'login') return 'login';
84
- if (value === 'logout') return 'logout';
85
- if (value === 'whoami') return 'whoami';
86
- if (value === 'create-space') return 'create-space';
87
- if (value === 'create') return 'create';
88
- if (value === 'dev') return 'dev';
89
- if (value === 'typegen') return 'typegen';
90
- if (value === 'migrate-status') return 'migrate-status';
91
- if (value === 'migrate-up') return 'migrate-up';
92
- if (value === 'build') return 'build';
93
- if (value === 'eject') return 'eject';
94
- if (value === 'target') return 'target';
95
- if (value === 'deploy') return 'deploy';
96
- if (value === 'verify') return 'verify';
97
- if (value === 'rollback') return 'rollback';
98
- if (value === 'deployments') return 'deployments';
99
- if (value === 'interactive') return 'interactive';
100
-
101
- return null;
102
- }
103
-
104
- export function shouldRunInteractive(args: ParsedArgs): boolean {
105
- if (args.flags.has('--no-interactive')) {
106
- return false;
107
- }
108
- if (args.flags.has('--interactive')) {
109
- return true;
110
- }
111
- return process.stdout.isTTY === true;
112
- }
@@ -1,57 +0,0 @@
1
- import { deletePassword, getPassword, setPassword } from 'cross-keychain';
2
-
3
- const SYNCULAR_CLI_KEYCHAIN_SERVICE = 'syncular-cli';
4
-
5
- function sanitizeAccountSegment(value: string): string {
6
- const sanitized = value.replace(/[^A-Za-z0-9._@-]/g, '-');
7
- const collapsed = sanitized.replace(/-+/g, '-').replace(/^-+|-+$/g, '');
8
- return collapsed.length > 0 ? collapsed : 'default';
9
- }
10
-
11
- function controlPlaneAccount(controlPlaneBase: string): string {
12
- try {
13
- const url = new URL(controlPlaneBase);
14
- return `control-plane-${sanitizeAccountSegment(url.host)}`;
15
- } catch {
16
- return `control-plane-${sanitizeAccountSegment(controlPlaneBase)}`;
17
- }
18
- }
19
-
20
- export async function readStoredControlPlaneToken(
21
- controlPlaneBase: string
22
- ): Promise<string | null> {
23
- try {
24
- const token = await getPassword(
25
- SYNCULAR_CLI_KEYCHAIN_SERVICE,
26
- controlPlaneAccount(controlPlaneBase)
27
- );
28
- const normalized = token?.trim() ?? '';
29
- return normalized.length > 0 ? normalized : null;
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- export async function writeStoredControlPlaneToken(args: {
36
- controlPlaneBase: string;
37
- token: string;
38
- }): Promise<void> {
39
- await setPassword(
40
- SYNCULAR_CLI_KEYCHAIN_SERVICE,
41
- controlPlaneAccount(args.controlPlaneBase),
42
- args.token
43
- );
44
- }
45
-
46
- export async function clearStoredControlPlaneToken(
47
- controlPlaneBase: string
48
- ): Promise<void> {
49
- try {
50
- await deletePassword(
51
- SYNCULAR_CLI_KEYCHAIN_SERVICE,
52
- controlPlaneAccount(controlPlaneBase)
53
- );
54
- } catch {
55
- // ignore missing keychain entries
56
- }
57
- }
@@ -1,2 +0,0 @@
1
- export * from './registry';
2
- export * from './types';
@@ -1,47 +0,0 @@
1
- import type { ContractBuildpack } from '@syncular/cli-core';
2
- import { BUILDPACK_EXTENSION_MANIFEST } from '../extensions';
3
-
4
- type BuildpackManifest = typeof BUILDPACK_EXTENSION_MANIFEST;
5
-
6
- function buildBuildpackRegistry(manifest: BuildpackManifest) {
7
- const entries = Object.entries(manifest);
8
- const seenBuildpackIds = new Set<string>();
9
-
10
- for (const [registryKey, manifestEntry] of entries) {
11
- const buildpackId = manifestEntry.buildpack.id;
12
- if (buildpackId.length === 0) {
13
- throw new Error(`Buildpack "${registryKey}" has an empty id.`);
14
- }
15
-
16
- if (seenBuildpackIds.has(buildpackId)) {
17
- throw new Error(`Duplicate buildpack id in manifest: "${buildpackId}".`);
18
- }
19
- seenBuildpackIds.add(buildpackId);
20
- }
21
-
22
- return Object.freeze(
23
- Object.fromEntries(
24
- entries.map(([registryKey, manifestEntry]) => [
25
- registryKey,
26
- manifestEntry.buildpack,
27
- ])
28
- )
29
- ) as {
30
- [K in keyof BuildpackManifest]: BuildpackManifest[K]['buildpack'];
31
- };
32
- }
33
-
34
- const buildpackRegistry = buildBuildpackRegistry(BUILDPACK_EXTENSION_MANIFEST);
35
-
36
- const REGISTERED_BUILDPACKS = Object.freeze(Object.values(buildpackRegistry));
37
- const BUILDPACK_IDS = Object.freeze(
38
- REGISTERED_BUILDPACKS.map((buildpack) => buildpack.id)
39
- ) as readonly string[];
40
-
41
- export function listBuildpackIds(): readonly string[] {
42
- return BUILDPACK_IDS;
43
- }
44
-
45
- export function getBuildpackById(id: string): ContractBuildpack | null {
46
- return REGISTERED_BUILDPACKS.find((buildpack) => buildpack.id === id) ?? null;
47
- }
@@ -1 +0,0 @@
1
- export type { ContractBuildpack } from '@syncular/cli-core';