freestyle-sync 0.1.8 → 0.1.10
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.
- package/README.md +33 -3
- package/dist/src/main.js +324 -70
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# Freestyle Sync
|
|
2
2
|
|
|
3
|
-
Sync your current directory, it's dependencies, and your agent context into a
|
|
3
|
+
Sync your current directory, it's dependencies, and your agent context into a remote runtime. Freestyle VMs support snapshots so only changes are uploaded on later runs.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npx freestyle-sync
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
Freestyle VMs are the default provider. You can sync into a local Apple container on macOS with the Apple `container` CLI installed:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx freestyle-sync --provider apple-container --apple-container-image ubuntu:24.04
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Use `--vm-id <container-name-or-id>` to reuse an existing Apple container. Snapshot caching is only available with the Freestyle provider.
|
|
16
|
+
|
|
9
17
|
Use the SDK when embedding sync inside another CLI/app (no required `freestyle-sync.config.ts` or cache file):
|
|
10
18
|
|
|
11
19
|
```ts
|
|
@@ -29,7 +37,7 @@ const result = await sync({
|
|
|
29
37
|
},
|
|
30
38
|
});
|
|
31
39
|
|
|
32
|
-
console.log("synced
|
|
40
|
+
console.log("synced runtime", result.vmId);
|
|
33
41
|
```
|
|
34
42
|
|
|
35
43
|
freestyle-sync creates `freestyle-sync.config.mjs` on first run and installs the config dependencies into your project as dev dependencies. Remove the generated config or dependencies if you want to reset it later.
|
|
@@ -71,4 +79,26 @@ export default defineConfig({
|
|
|
71
79
|
shellHistoryPlugin(),
|
|
72
80
|
],
|
|
73
81
|
});
|
|
74
|
-
```
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Project sync can exclude folders and include additional local paths. Include paths may point outside the project directory; they are copied into the remote project at the configured relative target.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
export default defineConfig({
|
|
88
|
+
sync: {
|
|
89
|
+
exclude: ["dist", ".turbo"],
|
|
90
|
+
include: [
|
|
91
|
+
{ source: "../shared-config", target: "shared-config" },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
plugins: [
|
|
95
|
+
nodeNpmPlugin(),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`nodeNpmPlugin()` excludes `node_modules` from upload by default and installs dependencies on the remote runtime using the project lockfile: `pnpm install --frozen-lockfile`, Yarn `--frozen-lockfile`/`--immutable`, or `npm ci`. To keep the previous behavior of syncing `node_modules` and only repairing native/workspace packages remotely, use:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
nodeNpmPlugin({ syncNodeModules: true })
|
|
104
|
+
```
|
package/dist/src/main.js
CHANGED
|
@@ -10,7 +10,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
10
10
|
import "dotenv/config";
|
|
11
11
|
import { createHash } from "node:crypto";
|
|
12
12
|
import { createReadStream, realpathSync } from "node:fs";
|
|
13
|
-
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
13
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, readlink, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
14
14
|
import { homedir, tmpdir } from "node:os";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -22,8 +22,9 @@ const execFileAsync = promisify(execFile);
|
|
|
22
22
|
const CLI_NAME = "freestyle-sync";
|
|
23
23
|
const CACHE_VERSION = 1;
|
|
24
24
|
const PLUGIN_PREFERENCES_VERSION = 1;
|
|
25
|
-
const ARCHIVE_CHUNK_BYTES =
|
|
25
|
+
const ARCHIVE_CHUNK_BYTES = 512 * 1024;
|
|
26
26
|
const MS_PER_SECOND = 1000;
|
|
27
|
+
const DEFAULT_APPLE_CONTAINER_IMAGE = "ubuntu:24.04";
|
|
27
28
|
const DEFAULT_FREESTYLE_API_URL = "https://api.freestyle.sh";
|
|
28
29
|
const DEFAULT_STACK_API_URL = "https://api.stack-auth.com";
|
|
29
30
|
const DEFAULT_STACK_PROJECT_ID = "0edf478c-f123-46fb-818f-34c0024a9f35";
|
|
@@ -138,9 +139,10 @@ export async function sync(sdkOptions) {
|
|
|
138
139
|
progress.step("Reading sync cache");
|
|
139
140
|
const cache = currentCache;
|
|
140
141
|
if (!options.vmId && !cache.snapshotId) {
|
|
141
|
-
console.warn(`${CLI_NAME} --skip-sync: no cached snapshot found. A new empty
|
|
142
|
+
console.warn(`${CLI_NAME} --skip-sync: no cached snapshot found. A new empty runtime will be created. Run without --skip-sync first to create a snapshot.`);
|
|
142
143
|
}
|
|
143
|
-
const
|
|
144
|
+
const provider = resolveVmProvider(options.provider);
|
|
145
|
+
const source = options.vmId ? `existing ${provider.runtimeLabel} ${options.vmId}` : cache.snapshotId ? `snapshot ${cache.snapshotId}` : `a new ${provider.runtimeLabel}`;
|
|
144
146
|
console.log(`Skipping sync: creating VM from ${source}`);
|
|
145
147
|
console.log(`Remote project: ${options.remoteProjectDir}`);
|
|
146
148
|
if (options.dryRun) {
|
|
@@ -149,26 +151,26 @@ export async function sync(sdkOptions) {
|
|
|
149
151
|
snapshotId: currentCache.snapshotId,
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
|
-
progress.step(
|
|
153
|
-
const { vm, vmId } = await getOrCreateVm(options, cache.snapshotId);
|
|
154
|
+
progress.step(`Preparing ${provider.displayName}`);
|
|
155
|
+
const { vm, vmId, provider: runtimeProvider } = await getOrCreateVm(options, cache.snapshotId);
|
|
154
156
|
resultVmId = vmId;
|
|
155
|
-
console.log(`Using
|
|
157
|
+
console.log(`Using ${runtimeProvider.runtimeLabel}: ${vmId}`);
|
|
156
158
|
progress.step(`Running post-sync plugins for ${vmId}`);
|
|
157
159
|
const contextCandidates = await discoverPluginContextCandidates(options);
|
|
158
160
|
const postSyncMessages = await runAfterSyncPlugins(vm, vmId, options, contextCandidates);
|
|
159
161
|
console.log("");
|
|
160
|
-
console.log(
|
|
162
|
+
console.log(`${capitalize(runtimeProvider.runtimeLabel)} ready: ${vmId}`);
|
|
161
163
|
console.log(`Project: ${options.remoteProjectDir}`);
|
|
162
164
|
if (cache.snapshotId) {
|
|
163
165
|
console.log(`Snapshot cache: ${cache.snapshotId}`);
|
|
164
166
|
}
|
|
165
167
|
for (const message of postSyncMessages)
|
|
166
168
|
console.log(message);
|
|
167
|
-
console.log(
|
|
169
|
+
console.log(`${capitalize(runtimeProvider.runtimeLabel)} access: ${runtimeProvider.connectionHint(vmId)}`);
|
|
168
170
|
if (options.autoSsh) {
|
|
169
171
|
const connected = await runConnectPlugins(vm, vmId, options, contextCandidates);
|
|
170
172
|
if (!connected)
|
|
171
|
-
await
|
|
173
|
+
await connectToVm(runtimeProvider, vmId);
|
|
172
174
|
}
|
|
173
175
|
return {
|
|
174
176
|
vmId,
|
|
@@ -180,7 +182,8 @@ export async function sync(sdkOptions) {
|
|
|
180
182
|
progress.step("Scanning project files");
|
|
181
183
|
const cache = currentCache;
|
|
182
184
|
const base = cacheBaseForSync(options, cache);
|
|
183
|
-
const
|
|
185
|
+
const projectSyncConfig = await resolveProjectSyncConfig(options);
|
|
186
|
+
const projectEntries = await scanProject(options.projectRoot, options.includeGitDir, projectSyncConfig);
|
|
184
187
|
const projectCurrent = digestMap(projectEntries);
|
|
185
188
|
const projectChanges = diffEntries(projectEntries, base.projectFiles);
|
|
186
189
|
progress.step("Detecting auth and agent context");
|
|
@@ -198,10 +201,11 @@ export async function sync(sdkOptions) {
|
|
|
198
201
|
snapshotId: currentCache.snapshotId,
|
|
199
202
|
};
|
|
200
203
|
}
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
const provider = resolveVmProvider(options.provider);
|
|
205
|
+
progress.step(`Preparing ${provider.displayName}`);
|
|
206
|
+
const { vm, vmId, provider: runtimeProvider } = await getOrCreateVm(options, cache.snapshotId);
|
|
203
207
|
resultVmId = vmId;
|
|
204
|
-
console.log(`Uploading to
|
|
208
|
+
console.log(`Uploading to ${runtimeProvider.runtimeLabel}: ${vmId}`);
|
|
205
209
|
progress.step(`Uploading project changes to ${vmId}`);
|
|
206
210
|
await ensureRemoteBase(vm, options.remoteProjectDir);
|
|
207
211
|
const projectResult = await syncProject(vm, vmId, options, projectChanges);
|
|
@@ -223,6 +227,7 @@ export async function sync(sdkOptions) {
|
|
|
223
227
|
version: CACHE_VERSION,
|
|
224
228
|
projectRoot: options.projectRoot,
|
|
225
229
|
remoteProjectDir: options.remoteProjectDir,
|
|
230
|
+
provider: options.provider,
|
|
226
231
|
vmId,
|
|
227
232
|
snapshotId: snapshotOverride ? snapshotOverride.snapshotId : cache.snapshotId,
|
|
228
233
|
projectFiles: projectCurrent,
|
|
@@ -236,12 +241,13 @@ export async function sync(sdkOptions) {
|
|
|
236
241
|
progress.step("Saving local sync cache");
|
|
237
242
|
await saveCache(buildSyncCache());
|
|
238
243
|
let snapshotPromise = null;
|
|
239
|
-
|
|
244
|
+
const createSnapshot = vm.snapshot;
|
|
245
|
+
if (options.snapshot && createSnapshot) {
|
|
240
246
|
progress.step(`Creating snapshot cache for ${vmId} in background`);
|
|
241
247
|
snapshotPromise = (async () => {
|
|
242
248
|
await runBeforeSnapshotPlugins(vm, vmId, options);
|
|
243
249
|
try {
|
|
244
|
-
const snapshot = await
|
|
250
|
+
const snapshot = await createSnapshot({ name: `${CLI_NAME}-${path.basename(options.projectRoot)}-${Date.now()}` });
|
|
245
251
|
return snapshot.snapshotId;
|
|
246
252
|
}
|
|
247
253
|
catch (error) {
|
|
@@ -250,6 +256,9 @@ export async function sync(sdkOptions) {
|
|
|
250
256
|
}
|
|
251
257
|
})();
|
|
252
258
|
}
|
|
259
|
+
else if (options.snapshot) {
|
|
260
|
+
progress.step(`Skipping snapshot cache for ${runtimeProvider.displayName}`);
|
|
261
|
+
}
|
|
253
262
|
else {
|
|
254
263
|
progress.step("Skipping snapshot cache");
|
|
255
264
|
}
|
|
@@ -257,7 +266,7 @@ export async function sync(sdkOptions) {
|
|
|
257
266
|
const postSyncMessages = await runAfterSyncPlugins(vm, vmId, options, contextCandidates);
|
|
258
267
|
progress.finish();
|
|
259
268
|
console.log("");
|
|
260
|
-
console.log(`${success(symbol("✓", "*"))} ${bold(
|
|
269
|
+
console.log(`${success(symbol("✓", "*"))} ${bold(`${capitalize(runtimeProvider.runtimeLabel)} ready: ${vmId}`)}`);
|
|
261
270
|
console.log(`${dim("Remote project:")} ${options.remoteProjectDir}`);
|
|
262
271
|
console.log(`${dim("Project files:")} ${projectResult.uploaded} uploaded, ${projectResult.removed} removed, ${projectResult.unchanged} unchanged`);
|
|
263
272
|
console.log(`${dim("Context files:")} ${contextResult.uploaded} uploaded, ${contextResult.unchanged} unchanged`);
|
|
@@ -269,11 +278,11 @@ export async function sync(sdkOptions) {
|
|
|
269
278
|
}
|
|
270
279
|
for (const message of postSyncMessages)
|
|
271
280
|
console.log(message);
|
|
272
|
-
console.log(`${accent(symbol("➜", ">"))} ${bold(
|
|
281
|
+
console.log(`${accent(symbol("➜", ">"))} ${bold(`${capitalize(runtimeProvider.runtimeLabel)} access: ${runtimeProvider.connectionHint(vmId)}`)}`);
|
|
273
282
|
if (options.autoSsh) {
|
|
274
283
|
const connected = await runConnectPlugins(vm, vmId, options, contextCandidates);
|
|
275
284
|
if (!connected)
|
|
276
|
-
await
|
|
285
|
+
await connectToVm(runtimeProvider, vmId);
|
|
277
286
|
}
|
|
278
287
|
if (snapshotPromise) {
|
|
279
288
|
const newSnapshotId = await snapshotPromise;
|
|
@@ -516,9 +525,15 @@ async function parseArgs(args) {
|
|
|
516
525
|
else if (arg === "--no-ssh") {
|
|
517
526
|
options.autoSsh = false;
|
|
518
527
|
}
|
|
528
|
+
else if (arg === "--provider" || arg === "--vm-provider") {
|
|
529
|
+
options.provider = parseVmProvider(readOptionValue(args, ++index, arg));
|
|
530
|
+
}
|
|
519
531
|
else if (arg === "--vm-id") {
|
|
520
532
|
options.vmId = readOptionValue(args, ++index, arg);
|
|
521
533
|
}
|
|
534
|
+
else if (arg === "--apple-container-image" || arg === "--container-image") {
|
|
535
|
+
options.appleContainerImage = readOptionValue(args, ++index, arg);
|
|
536
|
+
}
|
|
522
537
|
else if (arg === "--name") {
|
|
523
538
|
options.name = readOptionValue(args, ++index, arg);
|
|
524
539
|
}
|
|
@@ -565,7 +580,9 @@ function defaultCliOptions() {
|
|
|
565
580
|
projectRoot: process.cwd(),
|
|
566
581
|
cachePath: "",
|
|
567
582
|
remoteProjectDir: "",
|
|
583
|
+
provider: "freestyle",
|
|
568
584
|
name: "",
|
|
585
|
+
appleContainerImage: DEFAULT_APPLE_CONTAINER_IMAGE,
|
|
569
586
|
yes: false,
|
|
570
587
|
dryRun: false,
|
|
571
588
|
disablePlugins: [],
|
|
@@ -599,14 +616,17 @@ async function finalizeCliOptions(options) {
|
|
|
599
616
|
};
|
|
600
617
|
}
|
|
601
618
|
function printHelp() {
|
|
602
|
-
console.log(`${CLI_NAME} uploads the current project into a
|
|
619
|
+
console.log(`${CLI_NAME} uploads the current project into a remote runtime.
|
|
603
620
|
|
|
604
621
|
Usage:
|
|
605
622
|
${CLI_NAME} [project-dir] [options]
|
|
606
623
|
|
|
607
624
|
Options:
|
|
608
|
-
|
|
609
|
-
|
|
625
|
+
--provider <name> Runtime provider: freestyle or apple-container. Defaults to freestyle.
|
|
626
|
+
--vm-id <id> Sync into an existing provider runtime.
|
|
627
|
+
--apple-container-image <image>
|
|
628
|
+
Apple Containers image. Defaults to ${DEFAULT_APPLE_CONTAINER_IMAGE}.
|
|
629
|
+
--name <name> Name for a newly created runtime. Defaults to ${CLI_NAME}-<project>.
|
|
610
630
|
--remote-dir <path> Remote project directory. Defaults to the local absolute path.
|
|
611
631
|
--cache <path> Snapshot/hash cache path. Defaults to .freestyle-sync/cache.json.
|
|
612
632
|
--include-env <name> Always copy an environment variable. Repeatable.
|
|
@@ -616,16 +636,16 @@ Options:
|
|
|
616
636
|
--list-plugins Show configured plugins and whether they are enabled.
|
|
617
637
|
--install Run detected dependency install command after sync.
|
|
618
638
|
--no-ssh Do not automatically open VS Code/Cursor or SSH after sync.
|
|
619
|
-
|
|
639
|
+
--idle-timeout <seconds> Set runtime idle timeout when supported.
|
|
620
640
|
--no-auth Disable auth plugins for this run.
|
|
621
641
|
--no-agent-context Disable agent context plugins for this run.
|
|
622
642
|
--no-git-dir Exclude the local .git directory from project sync.
|
|
623
643
|
--all-copilot-workspaces Include Copilot chat state for every VS Code workspace.
|
|
624
644
|
--no-snapshot, --skip-snapshot
|
|
625
|
-
Do not snapshot the
|
|
626
|
-
--skip-sync Create a
|
|
627
|
-
files. Requires a
|
|
628
|
-
--dry-run Show what would sync without creating or changing a
|
|
645
|
+
Do not snapshot the runtime after sync.
|
|
646
|
+
--skip-sync Create a runtime from the last cached snapshot without syncing current
|
|
647
|
+
files. Requires a provider that supports snapshots, or --vm-id.
|
|
648
|
+
--dry-run Show what would sync without creating or changing a runtime.
|
|
629
649
|
-y, --yes Deprecated; accepted for compatibility.
|
|
630
650
|
-h, --help Show this help.
|
|
631
651
|
`);
|
|
@@ -640,9 +660,11 @@ function readOptionValue(args, index, option) {
|
|
|
640
660
|
async function readCache(cachePath, options) {
|
|
641
661
|
try {
|
|
642
662
|
const parsed = JSON.parse(await readFile(cachePath, "utf8"));
|
|
643
|
-
|
|
663
|
+
const provider = parsed.provider ?? "freestyle";
|
|
664
|
+
if (parsed.version === CACHE_VERSION && parsed.projectRoot === options.projectRoot && parsed.remoteProjectDir === options.remoteProjectDir && provider === options.provider) {
|
|
644
665
|
return {
|
|
645
666
|
...parsed,
|
|
667
|
+
provider,
|
|
646
668
|
projectFiles: parsed.projectFiles ?? {},
|
|
647
669
|
contextFiles: parsed.contextFiles ?? {},
|
|
648
670
|
snapshotProjectFiles: parsed.snapshotProjectFiles,
|
|
@@ -659,6 +681,7 @@ async function readCache(cachePath, options) {
|
|
|
659
681
|
version: CACHE_VERSION,
|
|
660
682
|
projectRoot: options.projectRoot,
|
|
661
683
|
remoteProjectDir: options.remoteProjectDir,
|
|
684
|
+
provider: options.provider,
|
|
662
685
|
projectFiles: {},
|
|
663
686
|
contextFiles: {},
|
|
664
687
|
};
|
|
@@ -669,6 +692,7 @@ function normalizeCache(cache, options) {
|
|
|
669
692
|
version: CACHE_VERSION,
|
|
670
693
|
projectRoot: options.projectRoot,
|
|
671
694
|
remoteProjectDir: options.remoteProjectDir,
|
|
695
|
+
provider: options.provider,
|
|
672
696
|
projectFiles: cache.projectFiles ?? {},
|
|
673
697
|
contextFiles: cache.contextFiles ?? {},
|
|
674
698
|
snapshotProjectFiles: cache.snapshotProjectFiles ?? {},
|
|
@@ -696,23 +720,92 @@ async function writeCache(cachePath, cache) {
|
|
|
696
720
|
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
697
721
|
await writeFile(cachePath, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
|
|
698
722
|
}
|
|
699
|
-
async function
|
|
700
|
-
const
|
|
701
|
-
|
|
723
|
+
async function resolveProjectSyncConfig(options) {
|
|
724
|
+
const configs = [];
|
|
725
|
+
if (config.sync)
|
|
726
|
+
configs.push(config.sync);
|
|
727
|
+
for (const plugin of plugins) {
|
|
728
|
+
const pluginConfig = await plugin.configureProjectSync?.({ options, utils: pluginUtils });
|
|
729
|
+
if (pluginConfig)
|
|
730
|
+
configs.push(pluginConfig);
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
exclude: configs.flatMap((projectConfig) => projectConfig.exclude ?? []).map(normalizeProjectPattern),
|
|
734
|
+
include: configs.flatMap((projectConfig) => normalizeProjectIncludes(options.projectRoot, projectConfig.include ?? [])),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function normalizeProjectIncludes(projectRoot, includes) {
|
|
738
|
+
return includes.map((include) => {
|
|
739
|
+
const source = typeof include === "string" ? include : include.source;
|
|
740
|
+
const target = typeof include === "string" ? undefined : include.target;
|
|
741
|
+
const resolvedSource = path.resolve(projectRoot, source);
|
|
742
|
+
return {
|
|
743
|
+
source: resolvedSource,
|
|
744
|
+
target: normalizeProjectPattern(target ?? path.basename(resolvedSource)),
|
|
745
|
+
};
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function normalizeProjectPattern(value) {
|
|
749
|
+
const normalized = path.posix.normalize(value.replace(/\\/g, "/")).replace(/^\/+/, "").replace(/\/+$/, "");
|
|
750
|
+
if (!normalized || normalized === ".") {
|
|
751
|
+
throw new Error("project sync paths must not be empty");
|
|
752
|
+
}
|
|
753
|
+
if (normalized === ".." || normalized.startsWith("../")) {
|
|
754
|
+
throw new Error(`project sync paths must stay inside the remote project: ${value}`);
|
|
755
|
+
}
|
|
756
|
+
return normalized;
|
|
757
|
+
}
|
|
758
|
+
async function scanProject(projectRoot, includeGitDir, syncConfig) {
|
|
759
|
+
const entriesByPath = new Map();
|
|
760
|
+
const rootEntries = [];
|
|
761
|
+
await walk(projectRoot, "", rootEntries, {
|
|
702
762
|
skipDirectory(relativePath, name) {
|
|
703
763
|
if (!includeGitDir && relativePath === ".git")
|
|
704
764
|
return true;
|
|
705
|
-
return
|
|
765
|
+
return shouldSkipProjectDirectory(relativePath, name, syncConfig.exclude);
|
|
706
766
|
},
|
|
707
767
|
});
|
|
708
|
-
|
|
768
|
+
upsertProjectEntries(entriesByPath, rootEntries);
|
|
769
|
+
for (const include of syncConfig.include) {
|
|
770
|
+
upsertProjectEntries(entriesByPath, await scanProjectInclude(include, syncConfig.exclude));
|
|
771
|
+
}
|
|
772
|
+
return Array.from(entriesByPath.values()).sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
773
|
+
}
|
|
774
|
+
async function scanProjectInclude(include, exclude) {
|
|
775
|
+
const stats = await lstat(include.source).catch((error) => {
|
|
776
|
+
throw new Error(`project sync include does not exist: ${include.source} (${error instanceof Error ? error.message : String(error)})`);
|
|
777
|
+
});
|
|
778
|
+
if (stats.isFile() || stats.isSymbolicLink()) {
|
|
779
|
+
return [await digestEntry(include.source, include.target)];
|
|
780
|
+
}
|
|
781
|
+
if (!stats.isDirectory()) {
|
|
782
|
+
return [];
|
|
783
|
+
}
|
|
784
|
+
const entries = [];
|
|
785
|
+
await walk(include.source, "", entries, {
|
|
786
|
+
mapRelativePath(relativePath) {
|
|
787
|
+
return `${include.target}/${relativePath}`;
|
|
788
|
+
},
|
|
789
|
+
skipDirectory(relativePath, name) {
|
|
790
|
+
return shouldSkipProjectDirectory(relativePath, name, exclude);
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
return entries;
|
|
794
|
+
}
|
|
795
|
+
function upsertProjectEntries(entriesByPath, entries) {
|
|
796
|
+
for (const entry of entries)
|
|
797
|
+
entriesByPath.set(entry.relativePath, entry);
|
|
798
|
+
}
|
|
799
|
+
function shouldSkipProjectDirectory(relativePath, name, exclude) {
|
|
800
|
+
return exclude.some((excludedPath) => relativePath === excludedPath || name === excludedPath);
|
|
709
801
|
}
|
|
710
802
|
async function walk(root, relativePath, entries, options) {
|
|
711
803
|
const absolutePath = path.join(root, relativePath);
|
|
712
804
|
const dir = await import("node:fs/promises").then((fs) => fs.readdir(absolutePath, { withFileTypes: true }));
|
|
713
805
|
for (const dirent of dir) {
|
|
714
806
|
const childRelativePath = relativePath ? path.join(relativePath, dirent.name) : dirent.name;
|
|
715
|
-
const
|
|
807
|
+
const localRelativePath = toPosix(childRelativePath);
|
|
808
|
+
const normalizedRelativePath = options.mapRelativePath?.(localRelativePath) ?? localRelativePath;
|
|
716
809
|
const childAbsolutePath = path.join(root, childRelativePath);
|
|
717
810
|
if (dirent.isDirectory()) {
|
|
718
811
|
if (options.skipDirectory(normalizedRelativePath, dirent.name)) {
|
|
@@ -962,9 +1055,11 @@ async function writePluginPreferences(preferencesPath, preferences) {
|
|
|
962
1055
|
await writeFile(preferencesPath, `${JSON.stringify({ ...preferences, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
963
1056
|
}
|
|
964
1057
|
function printPlan(options, projectChanges, contextChanges, envExports, cache) {
|
|
965
|
-
const
|
|
1058
|
+
const provider = resolveVmProvider(options.provider);
|
|
1059
|
+
const source = options.vmId ? `existing ${provider.runtimeLabel} ${options.vmId}` : cache.snapshotId ? `snapshot ${cache.snapshotId}` : `a new ${provider.runtimeLabel}`;
|
|
966
1060
|
console.log("");
|
|
967
1061
|
console.log(`${bold("Sync plan")}`);
|
|
1062
|
+
console.log(`${dim(" Provider:")} ${provider.displayName}`);
|
|
968
1063
|
console.log(`${dim(" Source:")} ${source}`);
|
|
969
1064
|
console.log(`${dim(" Local:")} ${options.projectRoot}`);
|
|
970
1065
|
console.log(`${dim(" Remote:")} ${options.remoteProjectDir}`);
|
|
@@ -1200,20 +1295,161 @@ async function runFreestyleLogin(extraArgs = []) {
|
|
|
1200
1295
|
});
|
|
1201
1296
|
});
|
|
1202
1297
|
}
|
|
1298
|
+
function parseVmProvider(value) {
|
|
1299
|
+
if (value === "freestyle" || value === "apple-container")
|
|
1300
|
+
return value;
|
|
1301
|
+
throw new Error(`unknown provider: ${value}. Expected freestyle or apple-container.`);
|
|
1302
|
+
}
|
|
1303
|
+
function resolveVmProvider(provider) {
|
|
1304
|
+
if (provider === "apple-container")
|
|
1305
|
+
return appleContainerProvider;
|
|
1306
|
+
return freestyleVmProvider;
|
|
1307
|
+
}
|
|
1308
|
+
const freestyleVmProvider = {
|
|
1309
|
+
name: "freestyle",
|
|
1310
|
+
displayName: "Freestyle VM",
|
|
1311
|
+
runtimeLabel: "VM",
|
|
1312
|
+
async prepare(options, snapshotId) {
|
|
1313
|
+
const freestyle = await getFreestyleClient();
|
|
1314
|
+
if (options.vmId) {
|
|
1315
|
+
const { vm } = await freestyle.vms.get({ vmId: options.vmId });
|
|
1316
|
+
await vm.start(options.idleTimeoutSeconds ? { idleTimeoutSeconds: options.idleTimeoutSeconds } : undefined);
|
|
1317
|
+
return { vm: vm, vmId: options.vmId };
|
|
1318
|
+
}
|
|
1319
|
+
console.log(snapshotId ? `Creating VM from snapshot ${snapshotId}...` : "Creating Freestyle VM...");
|
|
1320
|
+
const result = await freestyle.vms.create({
|
|
1321
|
+
name: options.name,
|
|
1322
|
+
snapshotId: snapshotId ?? undefined,
|
|
1323
|
+
idleTimeoutSeconds: options.idleTimeoutSeconds,
|
|
1324
|
+
});
|
|
1325
|
+
return { vm: result.vm, vmId: result.vmId };
|
|
1326
|
+
},
|
|
1327
|
+
connectionHint(vmId) {
|
|
1328
|
+
return `npx freestyle vm ssh ${vmId}`;
|
|
1329
|
+
},
|
|
1330
|
+
async connect(vmId) {
|
|
1331
|
+
console.log(`Connecting to VM ${vmId}...`);
|
|
1332
|
+
await spawnInherited("npx", ["freestyle", "vm", "ssh", vmId], "ssh exited with status");
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1335
|
+
const appleContainerProvider = {
|
|
1336
|
+
name: "apple-container",
|
|
1337
|
+
displayName: "Apple container",
|
|
1338
|
+
runtimeLabel: "container",
|
|
1339
|
+
async prepare(options) {
|
|
1340
|
+
if (options.vmId) {
|
|
1341
|
+
await startAppleContainer(options.vmId);
|
|
1342
|
+
return { vm: new AppleContainerVm(options.vmId), vmId: options.vmId };
|
|
1343
|
+
}
|
|
1344
|
+
console.log(`Creating Apple container ${options.name} from ${options.appleContainerImage}...`);
|
|
1345
|
+
const result = await execFileAsync("container", ["run", "--detach", "--name", options.name, options.appleContainerImage, "sleep", "infinity"]);
|
|
1346
|
+
const vmId = result.stdout.trim() || options.name;
|
|
1347
|
+
return { vm: new AppleContainerVm(vmId), vmId };
|
|
1348
|
+
},
|
|
1349
|
+
connectionHint(vmId) {
|
|
1350
|
+
return `container exec --interactive --tty ${vmId} sh -l`;
|
|
1351
|
+
},
|
|
1352
|
+
async connect(vmId) {
|
|
1353
|
+
console.log(`Connecting to container ${vmId}...`);
|
|
1354
|
+
await spawnInherited("container", ["exec", "--interactive", "--tty", vmId, "sh", "-l"], "container shell exited with status");
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1203
1357
|
async function getOrCreateVm(options, snapshotId) {
|
|
1204
|
-
const
|
|
1205
|
-
if (
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1358
|
+
const provider = resolveVmProvider(options.provider);
|
|
1359
|
+
if (snapshotId && provider.name !== "freestyle") {
|
|
1360
|
+
console.warn(`${provider.displayName} does not support Freestyle snapshots. Creating a fresh ${provider.runtimeLabel}.`);
|
|
1361
|
+
}
|
|
1362
|
+
const runtime = await provider.prepare(options, provider.name === "freestyle" ? snapshotId : undefined);
|
|
1363
|
+
return { ...runtime, provider };
|
|
1364
|
+
}
|
|
1365
|
+
async function startAppleContainer(containerId) {
|
|
1366
|
+
const result = await execFileResult("container", ["start", containerId]);
|
|
1367
|
+
if (result.statusCode !== 0 && !/already running/i.test(result.stderr || result.stdout || "")) {
|
|
1368
|
+
throw new Error(`failed to start Apple container ${containerId}: ${result.stderr || result.stdout || `exit ${result.statusCode}`}`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
class AppleContainerVm {
|
|
1372
|
+
containerId;
|
|
1373
|
+
fs = {
|
|
1374
|
+
writeFile: (remotePath, content) => this.writeFile(remotePath, content),
|
|
1375
|
+
writeTextFile: (remotePath, content) => this.writeFile(remotePath, Buffer.from(content)),
|
|
1376
|
+
};
|
|
1377
|
+
constructor(containerId) {
|
|
1378
|
+
this.containerId = containerId;
|
|
1379
|
+
}
|
|
1380
|
+
exec(args) {
|
|
1381
|
+
return execFileResult("container", ["exec", this.containerId, "sh", "-lc", args.command], { timeoutMs: args.timeoutMs });
|
|
1382
|
+
}
|
|
1383
|
+
async writeFile(remotePath, content) {
|
|
1384
|
+
const directory = path.posix.dirname(remotePath);
|
|
1385
|
+
await checkedContainerExec(this.containerId, `mkdir -p ${shellQuote(directory)} && cat > ${shellQuote(remotePath)}`, content);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
async function checkedContainerExec(containerId, command, input) {
|
|
1389
|
+
const result = await execFileResult("container", ["exec", "--interactive", containerId, "sh", "-lc", command], { input });
|
|
1390
|
+
if (result.statusCode && result.statusCode !== 0) {
|
|
1391
|
+
throw new Error(`container command failed (${result.statusCode}): ${command}\n${result.stderr || result.stdout || ""}`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
async function execFileResult(file, args, options = {}) {
|
|
1395
|
+
return new Promise((resolve, reject) => {
|
|
1396
|
+
const child = spawn(file, args, { stdio: [options.input ? "pipe" : "ignore", "pipe", "pipe"] });
|
|
1397
|
+
const stdout = [];
|
|
1398
|
+
const stderr = [];
|
|
1399
|
+
let settled = false;
|
|
1400
|
+
let timedOut = false;
|
|
1401
|
+
const timer = options.timeoutMs
|
|
1402
|
+
? setTimeout(() => {
|
|
1403
|
+
timedOut = true;
|
|
1404
|
+
child.kill("SIGTERM");
|
|
1405
|
+
}, options.timeoutMs)
|
|
1406
|
+
: undefined;
|
|
1407
|
+
child.stdout?.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
1408
|
+
child.stderr?.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
1409
|
+
child.on("error", (error) => {
|
|
1410
|
+
if (settled)
|
|
1411
|
+
return;
|
|
1412
|
+
settled = true;
|
|
1413
|
+
if (timer)
|
|
1414
|
+
clearTimeout(timer);
|
|
1415
|
+
reject(error);
|
|
1416
|
+
});
|
|
1417
|
+
child.on("exit", (code) => {
|
|
1418
|
+
if (settled)
|
|
1419
|
+
return;
|
|
1420
|
+
settled = true;
|
|
1421
|
+
if (timer)
|
|
1422
|
+
clearTimeout(timer);
|
|
1423
|
+
resolve({
|
|
1424
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
1425
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
1426
|
+
statusCode: timedOut ? 124 : code ?? 1,
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1429
|
+
if (options.input) {
|
|
1430
|
+
child.stdin?.end(options.input);
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
async function spawnInherited(file, args, errorPrefix) {
|
|
1435
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
1436
|
+
const child = spawn(file, args, { stdio: "inherit" });
|
|
1437
|
+
child.on("error", reject);
|
|
1438
|
+
child.on("exit", (code) => resolve(code));
|
|
1215
1439
|
});
|
|
1216
|
-
|
|
1440
|
+
if (exitCode && exitCode !== 0) {
|
|
1441
|
+
throw new Error(`${errorPrefix} ${exitCode}`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
async function connectToVm(provider, vmId) {
|
|
1445
|
+
if (!provider.connect) {
|
|
1446
|
+
console.log(`Connect with: ${provider.connectionHint(vmId)}`);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
await provider.connect(vmId);
|
|
1450
|
+
}
|
|
1451
|
+
function capitalize(value) {
|
|
1452
|
+
return value.length === 0 ? value : `${value[0].toUpperCase()}${value.slice(1)}`;
|
|
1217
1453
|
}
|
|
1218
1454
|
async function ensureRemoteBase(vm, remoteProjectDir) {
|
|
1219
1455
|
await checkedExec(vm, `mkdir -p ${shellQuote(remoteProjectDir)} /root/.freestyle-sync`);
|
|
@@ -1221,7 +1457,7 @@ async function ensureRemoteBase(vm, remoteProjectDir) {
|
|
|
1221
1457
|
async function syncProject(vm, vmId, options, changes) {
|
|
1222
1458
|
if (changes.changed.length > 0) {
|
|
1223
1459
|
console.log(`VM ${vmId}: uploading ${changes.changed.length} changed project files...`);
|
|
1224
|
-
const archive = await createProjectArchive(
|
|
1460
|
+
const archive = await createProjectArchive(changes.changed);
|
|
1225
1461
|
try {
|
|
1226
1462
|
await uploadArchiveInChunks(vm, vmId, archive, "/tmp/freestyle-sync-project.tgz", "project");
|
|
1227
1463
|
await checkedExec(vm, `mkdir -p ${shellQuote(options.remoteProjectDir)} && tar --no-same-owner --no-same-permissions -xzf /tmp/freestyle-sync-project.tgz -C ${shellQuote(options.remoteProjectDir)} && rm -f /tmp/freestyle-sync-project.tgz`);
|
|
@@ -1346,11 +1582,11 @@ async function runInstall(vm, projectRoot, remoteProjectDir) {
|
|
|
1346
1582
|
}
|
|
1347
1583
|
async function detectInstallCommand(projectRoot) {
|
|
1348
1584
|
if (await exists(path.join(projectRoot, "pnpm-lock.yaml")))
|
|
1349
|
-
return "corepack enable && pnpm install";
|
|
1585
|
+
return "corepack enable && pnpm install --frozen-lockfile";
|
|
1350
1586
|
if (await exists(path.join(projectRoot, "yarn.lock")))
|
|
1351
|
-
return "corepack enable && yarn install";
|
|
1352
|
-
if (await exists(path.join(projectRoot, "package-lock.json")))
|
|
1353
|
-
return "npm
|
|
1587
|
+
return "corepack enable && yarn_version=$(yarn --version) && case \"$yarn_version\" in 1.*) yarn install --frozen-lockfile ;; *) yarn install --immutable ;; esac";
|
|
1588
|
+
if (await exists(path.join(projectRoot, "package-lock.json")) || await exists(path.join(projectRoot, "npm-shrinkwrap.json")))
|
|
1589
|
+
return "npm ci";
|
|
1354
1590
|
if (await exists(path.join(projectRoot, "requirements.txt")))
|
|
1355
1591
|
return "python3 -m pip install -r requirements.txt";
|
|
1356
1592
|
if (await exists(path.join(projectRoot, "pyproject.toml")))
|
|
@@ -1361,12 +1597,23 @@ async function detectInstallCommand(projectRoot) {
|
|
|
1361
1597
|
return "go mod download";
|
|
1362
1598
|
return undefined;
|
|
1363
1599
|
}
|
|
1364
|
-
async function createProjectArchive(
|
|
1600
|
+
async function createProjectArchive(entries) {
|
|
1365
1601
|
const tempDir = await mkdtemp(path.join(tmpdir(), "freestyle-sync-"));
|
|
1366
|
-
const
|
|
1602
|
+
const stagingDir = path.join(tempDir, "staging");
|
|
1367
1603
|
const archivePath = path.join(tempDir, "project.tgz");
|
|
1368
|
-
await
|
|
1369
|
-
|
|
1604
|
+
await mkdir(stagingDir, { recursive: true });
|
|
1605
|
+
for (const entry of entries) {
|
|
1606
|
+
const destination = path.join(stagingDir, ...entry.relativePath.split("/"));
|
|
1607
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
1608
|
+
if (entry.kind === "symlink") {
|
|
1609
|
+
await symlink(await readlink(entry.absolutePath), destination);
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
await copyFile(entry.absolutePath, destination);
|
|
1613
|
+
await chmod(destination, entry.mode);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
await createTar(["--no-xattrs", "-czf", archivePath, "-C", stagingDir, "."]);
|
|
1370
1617
|
return archivePath;
|
|
1371
1618
|
}
|
|
1372
1619
|
async function createContextArchive(entries) {
|
|
@@ -1393,6 +1640,7 @@ async function createTar(args) {
|
|
|
1393
1640
|
}
|
|
1394
1641
|
async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, label) {
|
|
1395
1642
|
const archiveSize = (await stat(archivePath)).size;
|
|
1643
|
+
const archiveHash = await hashFile(archivePath);
|
|
1396
1644
|
const chunkCount = Math.max(1, Math.ceil(archiveSize / ARCHIVE_CHUNK_BYTES));
|
|
1397
1645
|
const chunkDir = `/tmp/freestyle-sync-${label}-${Date.now()}.chunks`;
|
|
1398
1646
|
console.log(`VM ${vmId}: streaming ${formatBytes(archiveSize)} ${label} archive in ${chunkCount} chunk${chunkCount === 1 ? "" : "s"}...`);
|
|
@@ -1402,8 +1650,9 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
|
|
|
1402
1650
|
const logEvery = Math.max(1, Math.ceil(chunkCount / 4));
|
|
1403
1651
|
let index = 0;
|
|
1404
1652
|
for await (const chunk of createReadStream(archivePath, { highWaterMark: ARCHIVE_CHUNK_BYTES })) {
|
|
1405
|
-
const chunkName = `${String(index).padStart(width, "0")}.chunk`;
|
|
1406
|
-
|
|
1653
|
+
const chunkName = `${String(index).padStart(width, "0")}.chunk.b64`;
|
|
1654
|
+
const content = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1655
|
+
await vm.fs.writeTextFile(`${chunkDir}/${chunkName}`, content.toString("base64"));
|
|
1407
1656
|
const uploadedChunks = index + 1;
|
|
1408
1657
|
if (chunkCount > 1) {
|
|
1409
1658
|
const progressMessage = `VM ${vmId}: uploaded ${uploadedChunks}/${chunkCount} ${label} archive chunks`;
|
|
@@ -1421,7 +1670,23 @@ async function uploadArchiveInChunks(vm, vmId, archivePath, remoteArchivePath, l
|
|
|
1421
1670
|
}
|
|
1422
1671
|
await checkedExec(vm, archiveSize === 0
|
|
1423
1672
|
? `: > ${shellQuote(remoteArchivePath)} && rm -rf ${shellQuote(chunkDir)}`
|
|
1424
|
-
:
|
|
1673
|
+
: [
|
|
1674
|
+
"set -e",
|
|
1675
|
+
`rm -f ${shellQuote(remoteArchivePath)}`,
|
|
1676
|
+
`for chunk in ${shellQuote(chunkDir)}/*.chunk.b64; do`,
|
|
1677
|
+
` base64 -d "$chunk" >> ${shellQuote(remoteArchivePath)}`,
|
|
1678
|
+
"done",
|
|
1679
|
+
`actual_size=$(wc -c < ${shellQuote(remoteArchivePath)} | tr -d '[:space:]')`,
|
|
1680
|
+
`actual_hash=$(sha256sum ${shellQuote(remoteArchivePath)} | awk '{print $1}')`,
|
|
1681
|
+
`if [ "$actual_size" != ${shellQuote(String(archiveSize))} ] || [ "$actual_hash" != ${shellQuote(archiveHash)} ]; then`,
|
|
1682
|
+
` rm -f ${shellQuote(remoteArchivePath)}`,
|
|
1683
|
+
` rm -rf ${shellQuote(chunkDir)}`,
|
|
1684
|
+
` echo ${shellQuote(`archive integrity check failed for ${label}: expected ${archiveSize} bytes/${archiveHash}`)} >&2`,
|
|
1685
|
+
` echo "got $actual_size bytes/$actual_hash" >&2`,
|
|
1686
|
+
" exit 1",
|
|
1687
|
+
"fi",
|
|
1688
|
+
`rm -rf ${shellQuote(chunkDir)}`,
|
|
1689
|
+
].join("\n"));
|
|
1425
1690
|
}
|
|
1426
1691
|
async function mkdirRemote(vm, directories) {
|
|
1427
1692
|
for (const chunk of chunkArray(directories, 50)) {
|
|
@@ -1488,17 +1753,6 @@ async function runConnectPlugins(vm, vmId, options, contextCandidates) {
|
|
|
1488
1753
|
}
|
|
1489
1754
|
return false;
|
|
1490
1755
|
}
|
|
1491
|
-
async function sshIntoVm(vmId) {
|
|
1492
|
-
console.log(`Connecting to VM ${vmId}...`);
|
|
1493
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
1494
|
-
const child = spawn("npx", ["freestyle", "vm", "ssh", vmId], { stdio: "inherit" });
|
|
1495
|
-
child.on("error", reject);
|
|
1496
|
-
child.on("exit", (code) => resolve(code));
|
|
1497
|
-
});
|
|
1498
|
-
if (exitCode && exitCode !== 0) {
|
|
1499
|
-
throw new Error(`ssh exited with status ${exitCode}`);
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
1756
|
async function hashFile(filePath) {
|
|
1503
1757
|
const hash = createHash("sha256");
|
|
1504
1758
|
await new Promise((resolve, reject) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freestyle-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/src/main.js",
|
|
6
6
|
"exports": {
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"build": "rm -rf dist plugins/*/dist && tsc -p tsconfig.json && node scripts/prepare-plugin-packages.mjs",
|
|
23
23
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
24
24
|
"prepack": "npm run build",
|
|
25
|
-
"publish": "npm run check && npm run build && npm run publish:workspaces && npm publish",
|
|
25
|
+
"publish": "npm run check && npm run build && npm run publish:workspaces && npm run publish:root",
|
|
26
|
+
"publish:root": "node scripts/publish-root.mjs",
|
|
26
27
|
"publish:workspaces": "node scripts/publish-workspaces.mjs",
|
|
27
28
|
"start": "node src/main.ts"
|
|
28
29
|
},
|