@pipelab/core-node 1.0.1-beta.23

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 (50) hide show
  1. package/.turbo/turbo-build.log +75 -0
  2. package/CHANGELOG.md +291 -0
  3. package/LICENSE +110 -0
  4. package/LICENSE.md +110 -0
  5. package/README.md +10 -0
  6. package/dist/index.d.mts +344 -0
  7. package/dist/index.d.mts.map +1 -0
  8. package/dist/index.mjs +51852 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/package.json +62 -0
  11. package/src/api.ts +115 -0
  12. package/src/config.ts +105 -0
  13. package/src/context.ts +53 -0
  14. package/src/handler-func.ts +234 -0
  15. package/src/handlers/agents.ts +32 -0
  16. package/src/handlers/auth.ts +95 -0
  17. package/src/handlers/build-history.ts +360 -0
  18. package/src/handlers/config.ts +109 -0
  19. package/src/handlers/engine.ts +229 -0
  20. package/src/handlers/fs.ts +97 -0
  21. package/src/handlers/history.ts +299 -0
  22. package/src/handlers/index.ts +41 -0
  23. package/src/handlers/shell.ts +57 -0
  24. package/src/handlers/system.ts +18 -0
  25. package/src/handlers.ts +2 -0
  26. package/src/heavy.ts +4 -0
  27. package/src/index.ts +16 -0
  28. package/src/ipc-core.ts +70 -0
  29. package/src/migrations.ts +72 -0
  30. package/src/paths.ts +1 -0
  31. package/src/plugins-registry.ts +62 -0
  32. package/src/presets/c3toSteam.ts +272 -0
  33. package/src/presets/demo.ts +123 -0
  34. package/src/presets/if.ts +69 -0
  35. package/src/presets/list.ts +30 -0
  36. package/src/presets/loop.ts +65 -0
  37. package/src/presets/moreToCome.ts +32 -0
  38. package/src/presets/newProject.ts +31 -0
  39. package/src/presets/preset.model.ts +0 -0
  40. package/src/presets/test-c3-offline.ts +78 -0
  41. package/src/presets/test-c3-unzip.ts +124 -0
  42. package/src/runner.ts +107 -0
  43. package/src/server.ts +80 -0
  44. package/src/types/runner.ts +101 -0
  45. package/src/utils/fs-extras.ts +182 -0
  46. package/src/utils/remote.ts +381 -0
  47. package/src/utils/storage.ts +99 -0
  48. package/src/utils.ts +258 -0
  49. package/src/websocket-server.ts +288 -0
  50. package/tsconfig.json +19 -0
@@ -0,0 +1,381 @@
1
+ import { dirname, delimiter, join } from "node:path";
2
+ import { tmpdir } from "node:os";
3
+ import { mkdir, readdir, readFile, writeFile, access, chmod, rm, cp } from "node:fs/promises";
4
+ import { existsSync, constants } from "node:fs";
5
+ import pacote from "pacote";
6
+ import semver from "semver";
7
+ import { isDev, projectRoot, PipelabContext } from "../context";
8
+ import { execa } from "execa";
9
+ import { downloadFile, extractZip, extractTarGz, generateTempFolder } from "./fs-extras";
10
+
11
+ export type FetchOptions = {
12
+ installDeps?: boolean;
13
+ signal?: AbortSignal;
14
+ context: PipelabContext;
15
+ };
16
+
17
+ /**
18
+ * Robust utility to fetch, cache, and resolve an NPM package.
19
+ * Centralized in core-node to avoid circular dependencies.
20
+ */
21
+ export async function fetchPackage(
22
+ packageName: string,
23
+ versionOrRange: string,
24
+ options: FetchOptions,
25
+ ): Promise<{
26
+ packageDir: string;
27
+ resolvedVersion: string;
28
+ isLocal?: boolean;
29
+ entryPoint?: string;
30
+ }> {
31
+ // 0. Check for local monorepo package in development
32
+ if (isDev && projectRoot && process.env.PIPELAB_FORCE_NPM !== "true") {
33
+ if (packageName.startsWith("@pipelab/")) {
34
+ const local = await tryResolveMonorepoPackage(packageName);
35
+ if (local) {
36
+ console.log(`[Fetcher] ${packageName}: Resolved to local source at ${local.packageDir}`);
37
+ return {
38
+ ...local,
39
+ resolvedVersion: "workspace",
40
+ };
41
+ }
42
+ }
43
+ }
44
+
45
+ const ctx = options.context;
46
+ const baseDir = ctx.getPackagesPath(packageName);
47
+ let resolvedVersion: string;
48
+
49
+ console.log(`[Fetcher] Resolving ${packageName}@${versionOrRange || "latest"}...`);
50
+
51
+ try {
52
+ // 1. Resolve version/range using npm
53
+ const packument = await pacote.packument(packageName);
54
+ const versions = Object.keys(packument.versions);
55
+ const range = versionOrRange || "latest";
56
+ const foundVersion = semver.maxSatisfying(versions, range) || packument["dist-tags"]?.[range];
57
+
58
+ if (!foundVersion) {
59
+ throw new Error(`Package ${packageName}@${range} not found on npm`);
60
+ }
61
+ resolvedVersion = foundVersion;
62
+ console.log(`[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm`);
63
+ } catch (error) {
64
+ console.warn(`[Fetcher] ${packageName}: remote resolution failed, trying local fallback...`);
65
+ const fallbackVersion = await tryLocalFallback(versionOrRange, error, baseDir, packageName);
66
+ if (fallbackVersion) {
67
+ resolvedVersion = fallbackVersion;
68
+ } else {
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ const packageDir = join(baseDir, resolvedVersion);
74
+ if (!existsSync(packageDir)) {
75
+ console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
76
+ await mkdir(packageDir, { recursive: true });
77
+ await pacote.extract(`${packageName}@${resolvedVersion}`, packageDir);
78
+ }
79
+
80
+ if (options?.installDeps) {
81
+ await installDependencies(packageDir, packageName, options);
82
+ }
83
+
84
+ return { packageDir, resolvedVersion };
85
+ }
86
+
87
+ /**
88
+ * Executes a pnpm install in a specific directory with a portable environment.
89
+ */
90
+ export async function runPnpm(
91
+ cwd: string,
92
+ options: {
93
+ args?: string[];
94
+ extraEnv?: Record<string, string>;
95
+ signal?: AbortSignal;
96
+ context: PipelabContext;
97
+ },
98
+ ) {
99
+ const {
100
+ args = ["install", "--prod", "--no-lockfile"],
101
+ extraEnv = {},
102
+ signal,
103
+ context: ctx,
104
+ } = options;
105
+
106
+ const nodePath = await ensureNodeJS("24.14.1", { context: ctx }).catch(() => process.execPath);
107
+ const pnpmPath = await ensurePNPM("10.12.0", { context: ctx }).catch(() => "pnpm");
108
+
109
+ const isScript = pnpmPath.endsWith(".cjs") || pnpmPath.endsWith(".js");
110
+ const command = isScript ? nodePath : pnpmPath;
111
+ const finalArgs = isScript ? [pnpmPath, ...args] : args;
112
+
113
+ return execa(command, finalArgs, {
114
+ cwd,
115
+ all: true,
116
+ cancelSignal: signal,
117
+ env: {
118
+ ...process.env,
119
+ NODE_ENV: "production",
120
+ PATH: nodePath ? `${dirname(nodePath)}${delimiter}${process.env.PATH}` : process.env.PATH,
121
+ PNPM_HOME: join(ctx.userDataPath, "pnpm"),
122
+ ...extraEnv,
123
+ },
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Installs a specific version of Node.js if not already present.
129
+ */
130
+ export async function ensureNodeJS(version: string, options: { context: PipelabContext }) {
131
+ const ctx = options.context;
132
+ const nodeDir = ctx.getThirdPartyPath("node", version);
133
+ const isWindows = process.platform === "win32";
134
+ const executableName = isWindows ? "node.exe" : "bin/node";
135
+ const finalNodePath = join(nodeDir, executableName);
136
+
137
+ try {
138
+ await access(finalNodePath, constants.X_OK);
139
+ return finalNodePath;
140
+ } catch (e) {}
141
+
142
+ const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x86";
143
+ const platform = isWindows ? "win" : process.platform === "darwin" ? "osx" : "linux";
144
+ const extension = isWindows ? "zip" : "tar.gz";
145
+ const downloadPlatform = platform === "osx" ? "darwin" : platform;
146
+
147
+ const fileName = `node-v${version}-${downloadPlatform}-${arch}.${extension}`;
148
+ const downloadUrl = `https://nodejs.org/dist/v${version}/${fileName}`;
149
+ const tempDir = await generateTempFolder(tmpdir());
150
+ const archivePath = join(tempDir, fileName);
151
+
152
+ console.log(`Downloading Node.js from ${downloadUrl}...`);
153
+ await downloadFile(downloadUrl, archivePath);
154
+
155
+ console.log(`Extracting Node.js to ${tempDir}...`);
156
+ const extractTempDir = join(tempDir, "extracted");
157
+ await mkdir(extractTempDir, { recursive: true });
158
+
159
+ if (extension === "zip") {
160
+ await extractZip(archivePath, extractTempDir);
161
+ } else {
162
+ await extractTarGz(archivePath, extractTempDir);
163
+ }
164
+
165
+ const extractedEntries = await readdir(extractTempDir);
166
+ const nodeSubDir = extractedEntries.find((entry) => entry.startsWith(`node-v${version}`));
167
+ if (!nodeSubDir) throw new Error(`Could not find extracted Node.js directory`);
168
+
169
+ const sourceDir = join(extractTempDir, nodeSubDir);
170
+ await mkdir(dirname(nodeDir), { recursive: true });
171
+ await rm(nodeDir, { recursive: true, force: true });
172
+ await cp(sourceDir, nodeDir, { recursive: true });
173
+ await rm(tempDir, { recursive: true, force: true });
174
+
175
+ if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => {});
176
+ return finalNodePath;
177
+ }
178
+
179
+ /**
180
+ * Installs the PNPM package from npm if not already present.
181
+ */
182
+ export async function ensurePNPM(version = "10.12.0", options: { context: PipelabContext }) {
183
+ const ctx = options.context;
184
+ const { packageDir } = await fetchPackage("pnpm", version, {
185
+ context: ctx,
186
+ });
187
+ return join(packageDir, "bin", "pnpm.cjs");
188
+ }
189
+
190
+ async function installDependencies(packageDir: string, packageName: string, options: FetchOptions) {
191
+ const nodeModulesPath = join(packageDir, "node_modules");
192
+
193
+ // Check if node_modules already exists and has content
194
+ if (existsSync(nodeModulesPath)) {
195
+ try {
196
+ const files = await readdir(nodeModulesPath);
197
+ if (files.length > 0) return;
198
+ console.warn(`[Fetcher] ${packageName}: node_modules exists but is empty. Re-installing...`);
199
+ } catch (e) {
200
+ // Continue to install if readdir fails
201
+ }
202
+ }
203
+
204
+ console.log(`[Fetcher] ${packageName}: Installing dependencies...`);
205
+ try {
206
+ const { all } = await runPnpm(packageDir, {
207
+ signal: options.signal,
208
+ context: options.context,
209
+ });
210
+
211
+ if (all) console.log(`[Fetcher] ${packageName}: Installation trace:\n${all}`);
212
+ console.log(`[Fetcher] ${packageName}: Dependencies installed successfully.`);
213
+ } catch (err: any) {
214
+ console.error(
215
+ `[Fetcher] ${packageName}: CRITICAL ERROR during dependency installation: ${err.message}`,
216
+ );
217
+ if (err.all) console.error(`[Fetcher] ${packageName}: Error details:\n${err.all}`);
218
+ throw new Error(`Failed to install dependencies for ${packageName}. See logs for details.`);
219
+ }
220
+ }
221
+
222
+ export async function fetchPipelabAsset(
223
+ packageName: string,
224
+ versionOrRange: string,
225
+ options: FetchOptions,
226
+ ): Promise<string> {
227
+ if (isDev && projectRoot) {
228
+ const assetId = packageName.replace("@pipelab/asset-", "");
229
+ const localPath = join(projectRoot, "assets", `asset-${assetId}`);
230
+ if (existsSync(localPath)) return localPath;
231
+ }
232
+ const { packageDir } = await fetchPackage(packageName, versionOrRange, options);
233
+ return packageDir;
234
+ }
235
+
236
+ export async function fetchPipelabPlugin(
237
+ pluginName: string,
238
+ versionOrRange: string,
239
+ options: FetchOptions,
240
+ ): Promise<{ packageDir: string; entryPoint: string; isLocal: boolean }> {
241
+ const { packageDir, isLocal, entryPoint } = await fetchPackage(pluginName, versionOrRange, {
242
+ installDeps: true,
243
+ ...options,
244
+ });
245
+
246
+ // Default entry point if not provided by fetchPackage
247
+ const finalEntryPoint = entryPoint || join(packageDir, "dist", "index.mjs");
248
+
249
+ return { packageDir, entryPoint: finalEntryPoint, isLocal: !!isLocal };
250
+ }
251
+
252
+ export async function fetchPipelabCli(
253
+ versionOrRange: string,
254
+ options: FetchOptions,
255
+ ): Promise<{ packageDir: string; entryPoint: string; isLocal: boolean }> {
256
+ const { packageDir, isLocal, entryPoint } = await fetchPackage(
257
+ "@pipelab/cli",
258
+ versionOrRange,
259
+ options,
260
+ );
261
+
262
+ // Default entry point for CLI if not provided
263
+ const finalEntryPoint = entryPoint || join(packageDir, "dist", "index.mjs");
264
+
265
+ return { packageDir, entryPoint: finalEntryPoint, isLocal: !!isLocal };
266
+ }
267
+
268
+ /**
269
+ * Cache for monorepo package locations to avoid repeated disk crawling.
270
+ */
271
+ let monorepoCache: Record<string, string> | null = null;
272
+
273
+ async function tryResolveMonorepoPackage(
274
+ packageName: string,
275
+ ): Promise<{ packageDir: string; isLocal: boolean; entryPoint: string } | null> {
276
+ if (!monorepoCache) {
277
+ monorepoCache = await crawlMonorepoPackages();
278
+ }
279
+
280
+ const packageDir = monorepoCache[packageName];
281
+ if (!packageDir) return null;
282
+
283
+ // Find best entry point from package.json
284
+ let entryPoint: string | undefined;
285
+ try {
286
+ const pkgPath = join(packageDir, "package.json");
287
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
288
+
289
+ // 1. Try to find the entry point from package.json
290
+ // User tip: main is usually source in dev, publishConfig.main is compiled for prod
291
+ const publishMain = pkg.publishConfig?.module || pkg.publishConfig?.main;
292
+ const devMain = pkg.module || pkg.main;
293
+
294
+ // Check bin field if it's a CLI
295
+ const binField =
296
+ typeof pkg.bin === "string"
297
+ ? pkg.bin
298
+ : pkg.bin?.[packageName.replace("@pipelab/", "")] ||
299
+ (pkg.bin ? pkg.bin[Object.keys(pkg.bin)[0]] : undefined);
300
+
301
+ // In dev, we prefer the "main" field if it points to TS, or a hardcoded src/index.ts
302
+ const tsSource = join(packageDir, "src", "index.ts");
303
+
304
+ if (devMain && devMain.endsWith(".ts")) {
305
+ entryPoint = join(packageDir, devMain);
306
+ } else if (existsSync(tsSource)) {
307
+ entryPoint = tsSource;
308
+ } else if (binField) {
309
+ entryPoint = join(packageDir, binField);
310
+ } else if (publishMain) {
311
+ entryPoint = join(packageDir, publishMain);
312
+ } else if (devMain) {
313
+ entryPoint = join(packageDir, devMain);
314
+ }
315
+ } catch (e) {
316
+ console.warn(`[Fetcher] Failed to parse package.json for ${packageName}:`, e);
317
+ }
318
+
319
+ return {
320
+ packageDir,
321
+ isLocal: true,
322
+ entryPoint: entryPoint || join(packageDir, "dist", "index.mjs"),
323
+ };
324
+ }
325
+
326
+ async function crawlMonorepoPackages(): Promise<Record<string, string>> {
327
+ const cache: Record<string, string> = {};
328
+ if (!projectRoot) return cache;
329
+
330
+ const searchDirs = ["plugins", "packages", "apps"];
331
+ for (const dir of searchDirs) {
332
+ const fullDir = join(projectRoot, dir);
333
+ if (!existsSync(fullDir)) continue;
334
+
335
+ try {
336
+ const entries = await readdir(fullDir, { withFileTypes: true });
337
+ for (const entry of entries) {
338
+ if (entry.isDirectory()) {
339
+ const pkgPath = join(fullDir, entry.name, "package.json");
340
+ if (existsSync(pkgPath)) {
341
+ try {
342
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
343
+ if (pkg.name) {
344
+ cache[pkg.name] = join(fullDir, entry.name);
345
+ }
346
+ } catch (e) {}
347
+ }
348
+ }
349
+ }
350
+ } catch (e) {}
351
+ }
352
+ return cache;
353
+ }
354
+
355
+ async function findLatestLocalVersion(baseDir: string): Promise<string | null> {
356
+ if (!existsSync(baseDir)) return null;
357
+ try {
358
+ const entries = await readdir(baseDir, { withFileTypes: true });
359
+ const versions = entries
360
+ .filter((e) => e.isDirectory() || e.isSymbolicLink())
361
+ .map((e) => e.name)
362
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
363
+ return versions[0] || null;
364
+ } catch {
365
+ return null;
366
+ }
367
+ }
368
+
369
+ async function tryLocalFallback(
370
+ _version: string | undefined,
371
+ _error: unknown,
372
+ baseDir: string,
373
+ logPrefix: string,
374
+ ): Promise<string | null> {
375
+ const latestLocal = await findLatestLocalVersion(baseDir);
376
+ if (latestLocal) {
377
+ console.info(`[Fetcher] ${logPrefix}: Using locally cached version: ${latestLocal}`);
378
+ return latestLocal;
379
+ }
380
+ return null;
381
+ }
@@ -0,0 +1,99 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { useLogger } from "@pipelab/shared";
4
+ import { PipelabContext } from "../context";
5
+
6
+ /**
7
+ * A generic file-based storage implementation for persisting JSON data.
8
+ * Useful for Supabase Auth and other CLI-specific persistent data.
9
+ */
10
+ export class JsonFileStorage {
11
+ private filePath: string;
12
+ private logger = useLogger().logger;
13
+
14
+ constructor(fileName: string, context: PipelabContext) {
15
+ this.filePath = path.join(context.userDataPath, fileName);
16
+ const dir = path.dirname(this.filePath);
17
+ if (!fs.existsSync(dir)) {
18
+ try {
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ } catch (e) {
21
+ this.logger().error(`[Storage] Failed to create directory ${dir}:`, e);
22
+ throw e;
23
+ }
24
+ }
25
+ }
26
+
27
+ getItem(key: string): string | null {
28
+ try {
29
+ if (!fs.existsSync(this.filePath)) return null;
30
+ const content = fs.readFileSync(this.filePath, "utf8");
31
+ if (!content) return null;
32
+ const data = JSON.parse(content);
33
+ return data[key] || null;
34
+ } catch (e) {
35
+ this.logger().error(`[Storage] Failed to read or parse ${this.filePath}:`, e);
36
+ // We throw here because the user explicitly asked NOT to ignore errors
37
+ throw e;
38
+ }
39
+ }
40
+
41
+ setItem(key: string, value: string): void {
42
+ try {
43
+ let data: any = {};
44
+ if (fs.existsSync(this.filePath)) {
45
+ const content = fs.readFileSync(this.filePath, "utf8");
46
+ if (content) {
47
+ data = JSON.parse(content);
48
+ }
49
+ }
50
+ data[key] = value;
51
+ fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), "utf8");
52
+ } catch (e) {
53
+ this.logger().error(`[Storage] Failed to write to ${this.filePath}:`, e);
54
+ throw e;
55
+ }
56
+ }
57
+
58
+ removeItem(key: string): void {
59
+ try {
60
+ if (!fs.existsSync(this.filePath)) return;
61
+ const content = fs.readFileSync(this.filePath, "utf8");
62
+ if (!content) return;
63
+ const data = JSON.parse(content);
64
+ delete data[key];
65
+ fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2), "utf8");
66
+ } catch (e) {
67
+ this.logger().error(`[Storage] Failed to remove key from ${this.filePath}:`, e);
68
+ throw e;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Helper to get the entire data object
74
+ */
75
+ getAll(): Record<string, any> {
76
+ try {
77
+ if (!fs.existsSync(this.filePath)) return {};
78
+ const content = fs.readFileSync(this.filePath, "utf8");
79
+ return content ? JSON.parse(content) : {};
80
+ } catch (e) {
81
+ this.logger().error(`[Storage] Failed to read all data from ${this.filePath}:`, e);
82
+ throw e;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Helper to clear the entire storage file
88
+ */
89
+ clear(): void {
90
+ try {
91
+ if (fs.existsSync(this.filePath)) {
92
+ fs.unlinkSync(this.filePath);
93
+ }
94
+ } catch (e) {
95
+ this.logger().error(`[Storage] Failed to clear ${this.filePath}:`, e);
96
+ throw e;
97
+ }
98
+ }
99
+ }