skein-js 0.3.0 → 0.5.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.
- package/README.md +2 -2
- package/dist/index.js +405 -112
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -67,8 +67,8 @@ All commands take `-c, --config <path>` (default `langgraph.json`).
|
|
|
67
67
|
| ------------------ | -------------------- | ----------- | -------------------------------------------------------------- |
|
|
68
68
|
| `-p, --port` | number | `2024` | Port to listen on. |
|
|
69
69
|
| `--host` | host | `127.0.0.1` | Interface to bind. |
|
|
70
|
-
| `--store <driver>` | `memory`, `postgres` | `memory` | `postgres` reads `
|
|
71
|
-
| `--queue <driver>` | `memory`, `redis` | `memory` | `redis` reads `
|
|
70
|
+
| `--store <driver>` | `memory`, `postgres` | `memory` | `postgres` reads `POSTGRES_URI`; also selects `PostgresSaver`. |
|
|
71
|
+
| `--queue <driver>` | `memory`, `redis` | `memory` | `redis` reads `REDIS_URI` (BullMQ queue + Redis Streams bus). |
|
|
72
72
|
| `--no-persist` | — | persists | Don't snapshot dev state to `.skein/` across restarts. |
|
|
73
73
|
| `--no-reload` | — | reloads | Disable hot reload on source change. |
|
|
74
74
|
| `-v, --verbose` | — | off | Log per-run activity (tool calls, interrupts, timing). |
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { createRequire } from "module";
|
|
4
|
+
import { createRequire as createRequire3 } from "module";
|
|
5
5
|
import { Command, InvalidArgumentError } from "@commander-js/extra-typings";
|
|
6
6
|
|
|
7
7
|
// src/dev-command.ts
|
|
@@ -26,7 +26,7 @@ var bold = paint(1);
|
|
|
26
26
|
var dim = paint(2);
|
|
27
27
|
|
|
28
28
|
// src/banner.ts
|
|
29
|
-
function printBanner(info,
|
|
29
|
+
function printBanner(info, logger2) {
|
|
30
30
|
const { host, port, graphIds, authPath, workerCount } = info;
|
|
31
31
|
const base = `http://${host}:${port}`;
|
|
32
32
|
console.log();
|
|
@@ -35,10 +35,10 @@ function printBanner(info, logger) {
|
|
|
35
35
|
console.log(`${dim("API ")} ${cyan(base)}`);
|
|
36
36
|
console.log(`${dim("Docs")} ${cyan(`${base}/docs`)}`);
|
|
37
37
|
console.log();
|
|
38
|
-
for (const id of graphIds)
|
|
39
|
-
if (authPath)
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
for (const id of graphIds) logger2.info(`Registering graph with id '${id}'`);
|
|
39
|
+
if (authPath) logger2.info(`Loading auth from ${authPath}`);
|
|
40
|
+
logger2.info(`Starting ${workerCount} worker${workerCount === 1 ? "" : "s"}`);
|
|
41
|
+
logger2.info(`Server running at ${base}`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/dev-logger.ts
|
|
@@ -135,9 +135,27 @@ async function applyProjectEnv(config, configDir) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// src/serve-env.ts
|
|
139
|
+
function envPort(fallback) {
|
|
140
|
+
const raw = process.env.PORT;
|
|
141
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
142
|
+
const port = Number(raw);
|
|
143
|
+
return Number.isInteger(port) && port >= 0 && port <= 65535 ? port : fallback;
|
|
144
|
+
}
|
|
145
|
+
function envHost(fallback) {
|
|
146
|
+
const host = process.env.HOST;
|
|
147
|
+
return host !== void 0 && host.trim() !== "" ? host : fallback;
|
|
148
|
+
}
|
|
149
|
+
function describeBindError(error, port) {
|
|
150
|
+
const code = error.code;
|
|
151
|
+
if (code === "EADDRINUSE") {
|
|
152
|
+
return `port ${port} is already in use. Stop the other process or pass --port.`;
|
|
153
|
+
}
|
|
154
|
+
return `failed to start server: ${error instanceof Error ? error.message : String(error)}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
138
157
|
// src/vite-graph-loader.ts
|
|
139
158
|
import { createServer as createNetServer } from "net";
|
|
140
|
-
import { createServer, createServerModuleRunner } from "vite";
|
|
141
159
|
function findFreePort() {
|
|
142
160
|
return new Promise((resolve, reject) => {
|
|
143
161
|
const probe = createNetServer();
|
|
@@ -150,9 +168,15 @@ function findFreePort() {
|
|
|
150
168
|
});
|
|
151
169
|
}
|
|
152
170
|
async function createViteGraphLoader(root, ignored = []) {
|
|
171
|
+
const { createServer, createServerModuleRunner, searchForWorkspaceRoot } = await import("vite");
|
|
172
|
+
const workspaceRoot = searchForWorkspaceRoot(root);
|
|
153
173
|
const server = await createServer({
|
|
154
174
|
root,
|
|
155
175
|
configFile: false,
|
|
176
|
+
// Resolve tsconfig `paths` natively (vite 8+): vite reads the project's tsconfig, follows
|
|
177
|
+
// `extends` to a base tsconfig (e.g. `tsconfig.base.json`), and honors `baseUrl` + wildcard
|
|
178
|
+
// aliases — so graphs that import workspace packages through path aliases resolve.
|
|
179
|
+
resolve: { tsconfigPaths: true },
|
|
156
180
|
appType: "custom",
|
|
157
181
|
logLevel: "warn",
|
|
158
182
|
server: {
|
|
@@ -162,7 +186,10 @@ async function createViteGraphLoader(root, ignored = []) {
|
|
|
162
186
|
// "port 24678 already in use" error — when two `skein dev`s run at once. Give it an ephemeral
|
|
163
187
|
// port so instances never clash.
|
|
164
188
|
hmr: { port: await findFreePort() },
|
|
165
|
-
watch: { ignored: ["**/node_modules/**", "**/.git/**", "**/dist/**", ...ignored] }
|
|
189
|
+
watch: { ignored: ["**/node_modules/**", "**/.git/**", "**/dist/**", ...ignored] },
|
|
190
|
+
// Aliased lib sources resolve outside the app `root` (e.g. `../../libs/**`). Widen vite's
|
|
191
|
+
// file-serving allowlist to the workspace root so the module runner can transform them.
|
|
192
|
+
fs: { allow: [workspaceRoot] }
|
|
166
193
|
},
|
|
167
194
|
optimizeDeps: { noDiscovery: true }
|
|
168
195
|
});
|
|
@@ -183,16 +210,6 @@ var RELOAD_DEBOUNCE_MS = 120;
|
|
|
183
210
|
var AUTOSAVE_MS = 2e3;
|
|
184
211
|
var FORCE_EXIT_MS = 5e3;
|
|
185
212
|
var devLogger = createDevLogger();
|
|
186
|
-
function envPort(fallback) {
|
|
187
|
-
const raw = process.env.PORT;
|
|
188
|
-
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
189
|
-
const port = Number(raw);
|
|
190
|
-
return Number.isInteger(port) && port >= 0 && port <= 65535 ? port : fallback;
|
|
191
|
-
}
|
|
192
|
-
function envHost(fallback) {
|
|
193
|
-
const host = process.env.HOST;
|
|
194
|
-
return host !== void 0 && host.trim() !== "" ? host : fallback;
|
|
195
|
-
}
|
|
196
213
|
async function runDev(options) {
|
|
197
214
|
const configPath = path3.resolve(process.cwd(), options.config);
|
|
198
215
|
const { config, configDir } = await loadConfig({ configPath });
|
|
@@ -251,10 +268,7 @@ async function runDev(options) {
|
|
|
251
268
|
await server.listen(port, host);
|
|
252
269
|
} catch (error) {
|
|
253
270
|
await Promise.allSettled([loader.close(), runtime.dispose()]);
|
|
254
|
-
|
|
255
|
-
console.error(
|
|
256
|
-
code === "EADDRINUSE" ? `skein: port ${port} is already in use. Stop the other process or pass --port.` : `skein: failed to start dev server: ${error instanceof Error ? error.message : String(error)}`
|
|
257
|
-
);
|
|
271
|
+
console.error(`skein: ${describeBindError(error, port)}`);
|
|
258
272
|
process.exitCode = 1;
|
|
259
273
|
return;
|
|
260
274
|
}
|
|
@@ -345,9 +359,206 @@ async function runDev(options) {
|
|
|
345
359
|
|
|
346
360
|
// src/docker/commands.ts
|
|
347
361
|
import { spawn, spawnSync } from "child_process";
|
|
348
|
-
import {
|
|
362
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
363
|
+
import { createRequire as createRequire2 } from "module";
|
|
349
364
|
import path5 from "path";
|
|
350
|
-
import { loadConfig as
|
|
365
|
+
import { loadConfig as loadConfig3 } from "@skein-js/config";
|
|
366
|
+
|
|
367
|
+
// src/bundle/bundle-project.ts
|
|
368
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
369
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
370
|
+
import { createRequire, isBuiltin } from "module";
|
|
371
|
+
import path4 from "path";
|
|
372
|
+
import { loadConfig as loadConfig2, parseGraphSpec } from "@skein-js/config";
|
|
373
|
+
import { embedRuntimePackage, isCustomFunctionPath } from "@skein-js/runtime";
|
|
374
|
+
|
|
375
|
+
// src/bundle/precompute-schemas.ts
|
|
376
|
+
async function precomputeSchemas(graphs) {
|
|
377
|
+
const entries = await Promise.all(
|
|
378
|
+
graphs.ids.map(async (id) => [id, await graphs.schemas(id)])
|
|
379
|
+
);
|
|
380
|
+
return Object.fromEntries(entries);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/bundle/write-manifest.ts
|
|
384
|
+
function buildProductionConfig(source, rewrites) {
|
|
385
|
+
const config = structuredClone(source);
|
|
386
|
+
config.graphs = { ...rewrites.graphs };
|
|
387
|
+
if (rewrites.auth && config.auth) {
|
|
388
|
+
config.auth = { ...config.auth, path: rewrites.auth };
|
|
389
|
+
}
|
|
390
|
+
if (rewrites.embed && config.store?.index) {
|
|
391
|
+
config.store = {
|
|
392
|
+
...config.store,
|
|
393
|
+
index: { ...config.store.index, embed: rewrites.embed }
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (typeof config.env === "string") delete config.env;
|
|
397
|
+
return config;
|
|
398
|
+
}
|
|
399
|
+
function buildArtifactPackageJson(appName, dependencies) {
|
|
400
|
+
const sorted = Object.fromEntries(
|
|
401
|
+
Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
|
|
402
|
+
);
|
|
403
|
+
const pkg = {
|
|
404
|
+
name: `${appName}-skein-artifact`,
|
|
405
|
+
version: "0.0.0",
|
|
406
|
+
private: true,
|
|
407
|
+
type: "module",
|
|
408
|
+
dependencies: sorted
|
|
409
|
+
};
|
|
410
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/bundle/bundle-project.ts
|
|
415
|
+
var SKEIN_RUNTIME_PEERS = ["@langchain/langgraph", "@langchain/langgraph-checkpoint-postgres"];
|
|
416
|
+
function packageNameOf(specifier) {
|
|
417
|
+
const parts = specifier.split("/");
|
|
418
|
+
return specifier.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0] ?? specifier;
|
|
419
|
+
}
|
|
420
|
+
function safeName(graphId) {
|
|
421
|
+
return graphId.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
422
|
+
}
|
|
423
|
+
function uniqueSafeName(graphId, used) {
|
|
424
|
+
const base = safeName(graphId);
|
|
425
|
+
let candidate = base;
|
|
426
|
+
let suffix = 2;
|
|
427
|
+
while (used.has(candidate)) candidate = `${base}-${suffix++}`;
|
|
428
|
+
used.add(candidate);
|
|
429
|
+
return candidate;
|
|
430
|
+
}
|
|
431
|
+
function nodeMajor(nodeVersion) {
|
|
432
|
+
return nodeVersion?.trim().match(/\d+/)?.[0] ?? "20";
|
|
433
|
+
}
|
|
434
|
+
function readVersion(pkgJsonPath) {
|
|
435
|
+
try {
|
|
436
|
+
const parsed = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
|
|
437
|
+
return parsed.version;
|
|
438
|
+
} catch {
|
|
439
|
+
return void 0;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function resolveInstalledVersion(pkg, fromDir) {
|
|
443
|
+
const require3 = createRequire(path4.join(fromDir, "__skein_resolver__.js"));
|
|
444
|
+
try {
|
|
445
|
+
const version2 = readVersion(require3.resolve(`${pkg}/package.json`));
|
|
446
|
+
if (version2) return version2;
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
let dir = path4.dirname(require3.resolve(pkg));
|
|
451
|
+
while (dir !== path4.dirname(dir)) {
|
|
452
|
+
const pkgJson = path4.join(dir, "package.json");
|
|
453
|
+
if (existsSync3(pkgJson)) {
|
|
454
|
+
const parsed = JSON.parse(readFileSync3(pkgJson, "utf8"));
|
|
455
|
+
if (parsed.name === pkg && parsed.version) return parsed.version;
|
|
456
|
+
}
|
|
457
|
+
dir = path4.dirname(dir);
|
|
458
|
+
}
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
throw new Error(
|
|
462
|
+
`skein build: could not resolve an installed version of "${pkg}". Install it in your project (it is imported by a graph/auth/embed module and must ship in the production image).`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
function externalizeNodeModules(externals) {
|
|
466
|
+
return {
|
|
467
|
+
name: "skein-externalize-node-modules",
|
|
468
|
+
enforce: "post",
|
|
469
|
+
async resolveId(source, importer, options) {
|
|
470
|
+
if (source.startsWith("\0") || source.startsWith("/") || source.startsWith(".")) return null;
|
|
471
|
+
if (isBuiltin(source) || source.startsWith("node:")) return { id: source, external: true };
|
|
472
|
+
const resolved = await this.resolve(source, importer, { ...options, skipSelf: true });
|
|
473
|
+
if (!resolved || resolved.id.includes("node_modules")) {
|
|
474
|
+
externals.add(packageNameOf(source));
|
|
475
|
+
return { id: source, external: true };
|
|
476
|
+
}
|
|
477
|
+
return resolved;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async function bundleProject(options) {
|
|
482
|
+
const { configPath, outDir, nodeVersion, skeinVersion } = options;
|
|
483
|
+
const { build, searchForWorkspaceRoot } = await import("vite");
|
|
484
|
+
const { config, configDir, graphs } = await loadConfig2({ configPath });
|
|
485
|
+
const workspaceRoot = searchForWorkspaceRoot(configDir);
|
|
486
|
+
const input = {};
|
|
487
|
+
const rewrites = { graphs: {} };
|
|
488
|
+
const usedGraphNames = /* @__PURE__ */ new Set();
|
|
489
|
+
for (const graphId of graphs.ids) {
|
|
490
|
+
const spec = graphs.spec(graphId);
|
|
491
|
+
const entryKey = `graphs/${uniqueSafeName(graphId, usedGraphNames)}`;
|
|
492
|
+
input[entryKey] = spec.sourceFile;
|
|
493
|
+
rewrites.graphs[graphId] = `./${entryKey}.js:${spec.exportSymbol}`;
|
|
494
|
+
}
|
|
495
|
+
if (config.auth?.path) {
|
|
496
|
+
const spec = parseGraphSpec(config.auth.path, configDir);
|
|
497
|
+
input["auth"] = spec.sourceFile;
|
|
498
|
+
rewrites.auth = `./auth.js:${spec.exportSymbol}`;
|
|
499
|
+
}
|
|
500
|
+
if (config.store?.index?.embed && isCustomFunctionPath(config.store.index.embed)) {
|
|
501
|
+
const spec = parseGraphSpec(config.store.index.embed, configDir);
|
|
502
|
+
input["embed"] = spec.sourceFile;
|
|
503
|
+
rewrites.embed = `./embed.js:${spec.exportSymbol}`;
|
|
504
|
+
}
|
|
505
|
+
const externals = /* @__PURE__ */ new Set();
|
|
506
|
+
const [, schemas] = await Promise.all([
|
|
507
|
+
build({
|
|
508
|
+
root: workspaceRoot,
|
|
509
|
+
configFile: false,
|
|
510
|
+
logLevel: "warn",
|
|
511
|
+
resolve: { tsconfigPaths: true },
|
|
512
|
+
plugins: [externalizeNodeModules(externals)],
|
|
513
|
+
build: {
|
|
514
|
+
ssr: true,
|
|
515
|
+
outDir,
|
|
516
|
+
emptyOutDir: true,
|
|
517
|
+
target: `node${nodeMajor(nodeVersion)}`,
|
|
518
|
+
minify: true,
|
|
519
|
+
sourcemap: true,
|
|
520
|
+
rollupOptions: {
|
|
521
|
+
input,
|
|
522
|
+
output: {
|
|
523
|
+
format: "es",
|
|
524
|
+
entryFileNames: "[name].js",
|
|
525
|
+
chunkFileNames: "chunks/[name]-[hash].js"
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}),
|
|
530
|
+
precomputeSchemas(graphs)
|
|
531
|
+
]);
|
|
532
|
+
const dependencies = {};
|
|
533
|
+
for (const pkg of externals) dependencies[pkg] = resolveInstalledVersion(pkg, workspaceRoot);
|
|
534
|
+
dependencies["skein-js"] = skeinVersion;
|
|
535
|
+
for (const peer of SKEIN_RUNTIME_PEERS) {
|
|
536
|
+
dependencies[peer] = resolveInstalledVersion(peer, workspaceRoot);
|
|
537
|
+
}
|
|
538
|
+
const embedPkg = config.store?.index?.embed && embedRuntimePackage(config.store.index.embed);
|
|
539
|
+
if (embedPkg) dependencies[embedPkg] = resolveInstalledVersion(embedPkg, workspaceRoot);
|
|
540
|
+
for (const dep of config.dependencies ?? []) {
|
|
541
|
+
if (dep.startsWith(".") || dep.startsWith("/")) continue;
|
|
542
|
+
const pkg = packageNameOf(dep);
|
|
543
|
+
dependencies[pkg] = resolveInstalledVersion(pkg, workspaceRoot);
|
|
544
|
+
}
|
|
545
|
+
await mkdir(outDir, { recursive: true });
|
|
546
|
+
const productionConfig = buildProductionConfig(config, rewrites);
|
|
547
|
+
await Promise.all([
|
|
548
|
+
writeFile(
|
|
549
|
+
path4.join(outDir, "langgraph.json"),
|
|
550
|
+
`${JSON.stringify(productionConfig, null, 2)}
|
|
551
|
+
`
|
|
552
|
+
),
|
|
553
|
+
writeFile(path4.join(outDir, "schemas.json"), `${JSON.stringify(schemas, null, 2)}
|
|
554
|
+
`),
|
|
555
|
+
writeFile(
|
|
556
|
+
path4.join(outDir, "package.json"),
|
|
557
|
+
buildArtifactPackageJson(path4.basename(configDir), dependencies)
|
|
558
|
+
)
|
|
559
|
+
]);
|
|
560
|
+
return { outDir, graphIds: graphs.ids, externals: dependencies };
|
|
561
|
+
}
|
|
351
562
|
|
|
352
563
|
// src/docker/compose.ts
|
|
353
564
|
function portMapping(options) {
|
|
@@ -370,8 +581,8 @@ services:
|
|
|
370
581
|
# The container CMD passes no --port; it binds $PORT (as a hosting platform would inject).
|
|
371
582
|
# Set it here so the app listens on the port the mapping above publishes.
|
|
372
583
|
PORT: "${options.containerPort}"
|
|
373
|
-
|
|
374
|
-
|
|
584
|
+
POSTGRES_URI: postgresql://postgres:postgres@postgres:5432/skein
|
|
585
|
+
REDIS_URI: redis://redis:6379
|
|
375
586
|
depends_on:
|
|
376
587
|
postgres:
|
|
377
588
|
condition: service_healthy
|
|
@@ -409,37 +620,10 @@ volumes:
|
|
|
409
620
|
}
|
|
410
621
|
|
|
411
622
|
// src/docker/dockerfile.ts
|
|
412
|
-
import { existsSync as existsSync3 } from "fs";
|
|
413
|
-
import path4 from "path";
|
|
414
623
|
function baseImage(nodeVersion) {
|
|
415
624
|
const major = nodeVersion?.trim().match(/^\d+/)?.[0] ?? "20";
|
|
416
625
|
return `node:${major}-slim`;
|
|
417
626
|
}
|
|
418
|
-
function detectPackageManager(projectDir) {
|
|
419
|
-
if (existsSync3(path4.join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
|
|
420
|
-
if (existsSync3(path4.join(projectDir, "yarn.lock"))) return "yarn";
|
|
421
|
-
return "npm";
|
|
422
|
-
}
|
|
423
|
-
function installSteps(manager) {
|
|
424
|
-
switch (manager) {
|
|
425
|
-
case "pnpm":
|
|
426
|
-
return {
|
|
427
|
-
copyLock: "COPY package.json pnpm-lock.yaml* ./",
|
|
428
|
-
install: "RUN --mount=type=cache,target=/root/.local/share/pnpm/store corepack enable && pnpm install --frozen-lockfile"
|
|
429
|
-
};
|
|
430
|
-
case "yarn":
|
|
431
|
-
return {
|
|
432
|
-
copyLock: "COPY package.json yarn.lock* ./",
|
|
433
|
-
install: "RUN --mount=type=cache,target=/usr/local/share/.cache/yarn corepack enable && yarn install --frozen-lockfile"
|
|
434
|
-
};
|
|
435
|
-
case "npm":
|
|
436
|
-
return {
|
|
437
|
-
copyLock: "COPY package.json package-lock.json* ./",
|
|
438
|
-
// `npm ci` needs a lockfile; fall back to `npm install` when there isn't one.
|
|
439
|
-
install: "RUN --mount=type=cache,target=/root/.npm npm ci || npm install"
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
627
|
function generateDockerignore() {
|
|
444
628
|
return [
|
|
445
629
|
"# Generated by skein \u2014 regenerated by `skein build` / `skein up`.",
|
|
@@ -447,7 +631,6 @@ function generateDockerignore() {
|
|
|
447
631
|
"node_modules",
|
|
448
632
|
".env",
|
|
449
633
|
".env.*",
|
|
450
|
-
".skein",
|
|
451
634
|
"Dockerfile",
|
|
452
635
|
"compose.yaml",
|
|
453
636
|
".dockerignore",
|
|
@@ -456,65 +639,83 @@ function generateDockerignore() {
|
|
|
456
639
|
].join("\n");
|
|
457
640
|
}
|
|
458
641
|
function generateDockerfile(options) {
|
|
459
|
-
const { nodeVersion, dockerfileLines = [], port
|
|
460
|
-
const { copyLock, install } = installSteps(packageManager);
|
|
642
|
+
const { nodeVersion, dockerfileLines = [], port } = options;
|
|
461
643
|
const lines = [
|
|
462
|
-
// The syntax directive must be the first line to be honored — it enables the RUN cache
|
|
644
|
+
// The syntax directive must be the first line to be honored — it enables the RUN cache mount.
|
|
463
645
|
`# syntax=docker/dockerfile:1`,
|
|
464
646
|
`# Generated by skein \u2014 do not edit by hand (regenerated by \`skein build\` / \`skein up\`).`,
|
|
647
|
+
// The build context must be a skein artifact dir (bundled JS + pinned package.json + schemas.json),
|
|
648
|
+
// which `skein build`/`up` create under `.skein/build`. Building this Dockerfile against a raw
|
|
649
|
+
// project dir will fail — run `skein build` instead of `docker build` by hand.
|
|
650
|
+
`# Build context: a \`.skein/build\` artifact produced by \`skein build\` (not the project root).`,
|
|
465
651
|
`FROM ${baseImage(nodeVersion)}`,
|
|
466
|
-
// Hand /app to the unprivileged 'node' user (uid 1000, shipped by the base image) so the server
|
|
467
|
-
// never runs as root. corepack/npm need root to install, so install as root, then chown the
|
|
468
|
-
// whole tree — including the root-created node_modules, where vite writes its cache at runtime.
|
|
469
652
|
`WORKDIR /app`,
|
|
470
653
|
``,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
654
|
+
// Install prod deps first for layer caching: the artifact's pinned package.json changes rarely,
|
|
655
|
+
// so this layer is reused across rebuilds while only the bundle layer below re-runs. The BuildKit
|
|
656
|
+
// cache mount keeps the npm cache out of the committed layer.
|
|
657
|
+
`# Install the artifact's pinned production dependencies (exact versions \u2014 deterministic).`,
|
|
658
|
+
`COPY package.json ./`,
|
|
659
|
+
`RUN --mount=type=cache,target=/root/.npm \\`,
|
|
660
|
+
` npm install --omit=dev --omit=optional --no-audit --no-fund`,
|
|
474
661
|
``,
|
|
475
|
-
`# Copy the
|
|
662
|
+
`# Copy the pre-built artifact (bundled JS graphs, production langgraph.json, baked schemas).`,
|
|
476
663
|
`COPY . .`,
|
|
477
664
|
``,
|
|
478
|
-
`# Give the runtime user ownership of the app + installed node_modules so vite can write its`,
|
|
479
|
-
`# on-disk cache under node_modules/.vite without EACCES.`,
|
|
480
|
-
`RUN chown -R node:node /app`,
|
|
481
|
-
``,
|
|
482
|
-
// Set NODE_ENV only after install so devDependencies (the vite/tsx toolchain `skein dev` loads
|
|
483
|
-
// graphs with) are installed; setting it before install would prune them and break graph loading.
|
|
484
665
|
`ENV NODE_ENV=production`,
|
|
666
|
+
// The bundle ships sourcemaps; enable them so stack traces map back to the original TypeScript.
|
|
667
|
+
`ENV NODE_OPTIONS=--enable-source-maps`,
|
|
485
668
|
`EXPOSE ${port}`,
|
|
669
|
+
// Liveness probe against skein's `/ok` endpoint, targeting the same port the server binds.
|
|
670
|
+
`HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \\`,
|
|
671
|
+
` CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||${port})+'/ok').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"`,
|
|
672
|
+
// Run unprivileged. The bundle writes nothing to disk, and /app is world-readable, so no chown.
|
|
486
673
|
`USER node`,
|
|
487
674
|
...dockerfileLines,
|
|
488
675
|
``,
|
|
489
|
-
`# Serve the
|
|
676
|
+
`# Serve the compiled artifact against Postgres + Redis (POSTGRES_URI / REDIS_URI from the env).`,
|
|
490
677
|
`# No --port: the server binds $PORT when the platform injects one (Railway/Fly/Render), else its`,
|
|
491
678
|
`# default. Exec form keeps node as PID 1 so SIGTERM reaches skein's graceful-shutdown handler.`,
|
|
492
|
-
`CMD ["npx", "skein", "
|
|
679
|
+
`CMD ["npx", "skein", "start", "--store", "postgres", "--queue", "redis", "--host", "0.0.0.0"]`
|
|
493
680
|
];
|
|
494
681
|
return `${lines.join("\n")}
|
|
495
682
|
`;
|
|
496
683
|
}
|
|
497
684
|
|
|
498
685
|
// src/docker/commands.ts
|
|
686
|
+
function skeinCliVersion() {
|
|
687
|
+
return createRequire2(import.meta.url)("../package.json").version;
|
|
688
|
+
}
|
|
689
|
+
var ARTIFACT_SUBDIR = path5.join(".skein", "build");
|
|
499
690
|
var CONTAINER_PORT = 8123;
|
|
500
691
|
var DOCKERFILE_NAME = "Dockerfile";
|
|
501
692
|
var COMPOSE_NAME = "compose.yaml";
|
|
502
693
|
var DOCKERIGNORE_NAME = ".dockerignore";
|
|
503
|
-
var GENERATED_MARKER = "# Generated by skein";
|
|
504
|
-
function isSkeinGenerated(contents) {
|
|
505
|
-
return contents.split("\n", 3).some((line2) => line2.startsWith(GENERATED_MARKER));
|
|
506
|
-
}
|
|
507
694
|
async function loadConfigContext(configOption) {
|
|
508
695
|
const configPath = path5.resolve(process.cwd(), configOption);
|
|
509
|
-
const { config, configDir } = await
|
|
510
|
-
return { config, configDir };
|
|
696
|
+
const { config, configDir } = await loadConfig3({ configPath });
|
|
697
|
+
return { config, configDir, configPath };
|
|
698
|
+
}
|
|
699
|
+
async function prepareArtifact(context, extraAssets = []) {
|
|
700
|
+
const outDir = path5.join(context.configDir, ARTIFACT_SUBDIR);
|
|
701
|
+
console.log("skein: bundling graphs into a production artifact\u2026");
|
|
702
|
+
await bundleProject({
|
|
703
|
+
configPath: context.configPath,
|
|
704
|
+
outDir,
|
|
705
|
+
nodeVersion: context.config.node_version,
|
|
706
|
+
skeinVersion: skeinCliVersion()
|
|
707
|
+
});
|
|
708
|
+
writeFileSync2(path5.join(outDir, DOCKERFILE_NAME), renderDockerfile(context, CONTAINER_PORT));
|
|
709
|
+
writeFileSync2(path5.join(outDir, DOCKERIGNORE_NAME), generateDockerignore());
|
|
710
|
+
for (const asset of extraAssets) writeFileSync2(path5.join(outDir, asset.name), asset.contents);
|
|
711
|
+
console.log(`skein: wrote artifact to ${path5.relative(process.cwd(), outDir) || "."}.`);
|
|
712
|
+
return outDir;
|
|
511
713
|
}
|
|
512
|
-
function renderDockerfile({ config
|
|
714
|
+
function renderDockerfile({ config }, port) {
|
|
513
715
|
return generateDockerfile({
|
|
514
716
|
nodeVersion: config.node_version,
|
|
515
717
|
dockerfileLines: config.dockerfile_lines,
|
|
516
|
-
port
|
|
517
|
-
packageManager: detectPackageManager(configDir)
|
|
718
|
+
port
|
|
518
719
|
});
|
|
519
720
|
}
|
|
520
721
|
function defaultImageTag(configDir) {
|
|
@@ -551,19 +752,12 @@ function wasInterrupted(result) {
|
|
|
551
752
|
function describeExit(result) {
|
|
552
753
|
return result.signal !== null ? `killed by ${result.signal}` : `exit ${result.code}`;
|
|
553
754
|
}
|
|
554
|
-
function writeGeneratedFile(file, contents) {
|
|
555
|
-
const name = path5.basename(file);
|
|
556
|
-
if (existsSync4(file) && !isSkeinGenerated(readFileSync3(file, "utf8"))) {
|
|
557
|
-
console.log(`skein: ${name} exists and wasn't generated by skein \u2014 using it as-is.`);
|
|
558
|
-
return "kept-user";
|
|
559
|
-
}
|
|
560
|
-
writeFileSync2(file, contents);
|
|
561
|
-
console.log(`skein: wrote ${name}.`);
|
|
562
|
-
return "wrote";
|
|
563
|
-
}
|
|
564
755
|
async function runDockerfile(options) {
|
|
565
756
|
const context = await loadConfigContext(options.config);
|
|
566
757
|
const contents = renderDockerfile(context, CONTAINER_PORT);
|
|
758
|
+
console.error(
|
|
759
|
+
"skein: this Dockerfile builds against a `.skein/build` artifact (run `skein build`), not the project root."
|
|
760
|
+
);
|
|
567
761
|
if (options.output !== void 0) {
|
|
568
762
|
const target = path5.resolve(process.cwd(), options.output);
|
|
569
763
|
writeFileSync2(target, contents);
|
|
@@ -575,15 +769,20 @@ async function runDockerfile(options) {
|
|
|
575
769
|
async function runBuild(options) {
|
|
576
770
|
const context = await loadConfigContext(options.config);
|
|
577
771
|
if (!requireDocker()) return;
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
772
|
+
let artifactDir;
|
|
773
|
+
try {
|
|
774
|
+
artifactDir = await prepareArtifact(context);
|
|
775
|
+
} catch (error) {
|
|
776
|
+
console.error(`skein: ${error instanceof Error ? error.message : String(error)}`);
|
|
777
|
+
process.exitCode = 1;
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
581
780
|
const tag = options.tag ?? defaultImageTag(context.configDir);
|
|
582
781
|
console.log(`skein: building image "${tag}"\u2026`);
|
|
583
782
|
const result = await runToCompletion(
|
|
584
783
|
"docker",
|
|
585
|
-
["build", "-t", tag, "-f",
|
|
586
|
-
|
|
784
|
+
["build", "-t", tag, "-f", path5.join(artifactDir, DOCKERFILE_NAME), "."],
|
|
785
|
+
artifactDir
|
|
587
786
|
);
|
|
588
787
|
if (!succeeded(result)) {
|
|
589
788
|
console.error(`skein: docker build failed (${describeExit(result)}).`);
|
|
@@ -595,22 +794,30 @@ async function runBuild(options) {
|
|
|
595
794
|
async function runUp(options) {
|
|
596
795
|
const context = await loadConfigContext(options.config);
|
|
597
796
|
if (!requireDocker()) return;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
797
|
+
let artifactDir;
|
|
798
|
+
try {
|
|
799
|
+
artifactDir = await prepareArtifact(context, [
|
|
800
|
+
{
|
|
801
|
+
name: COMPOSE_NAME,
|
|
802
|
+
contents: generateCompose({
|
|
803
|
+
hostPort: options.port,
|
|
804
|
+
host: options.host,
|
|
805
|
+
containerPort: CONTAINER_PORT
|
|
806
|
+
})
|
|
807
|
+
}
|
|
808
|
+
]);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
console.error(`skein: ${error instanceof Error ? error.message : String(error)}`);
|
|
811
|
+
process.exitCode = 1;
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
607
814
|
console.log(
|
|
608
815
|
`skein: bringing up the stack (app + Postgres + Redis) on http://${options.host}:${options.port}\u2026`
|
|
609
816
|
);
|
|
610
817
|
const result = await runToCompletion(
|
|
611
818
|
"docker",
|
|
612
819
|
["compose", "-f", COMPOSE_NAME, "up", "--build"],
|
|
613
|
-
|
|
820
|
+
artifactDir
|
|
614
821
|
);
|
|
615
822
|
if (!succeeded(result) && !wasInterrupted(result)) {
|
|
616
823
|
console.error(`skein: docker compose up failed (${describeExit(result)}).`);
|
|
@@ -619,9 +826,9 @@ async function runUp(options) {
|
|
|
619
826
|
}
|
|
620
827
|
|
|
621
828
|
// src/import-command.ts
|
|
622
|
-
import { existsSync as
|
|
829
|
+
import { existsSync as existsSync4 } from "fs";
|
|
623
830
|
import path6 from "path";
|
|
624
|
-
import { loadConfig as
|
|
831
|
+
import { loadConfig as loadConfig4 } from "@skein-js/config";
|
|
625
832
|
import {
|
|
626
833
|
describeSnapshot as describeSnapshot2,
|
|
627
834
|
loadSnapshotIntoStore,
|
|
@@ -633,7 +840,7 @@ function summarize(counts) {
|
|
|
633
840
|
}
|
|
634
841
|
async function runImportLanggraph(options) {
|
|
635
842
|
const configPath = path6.resolve(process.cwd(), options.config);
|
|
636
|
-
const { config, configDir } = await
|
|
843
|
+
const { config, configDir } = await loadConfig4({ configPath });
|
|
637
844
|
const sourceDir = options.from ? path6.resolve(process.cwd(), options.from) : path6.join(configDir, LANGGRAPH_DIR);
|
|
638
845
|
const snapshot = await readLanggraphDevState2(sourceDir);
|
|
639
846
|
if (snapshot === null) {
|
|
@@ -643,7 +850,7 @@ async function runImportLanggraph(options) {
|
|
|
643
850
|
const counts = describeSnapshot2(snapshot);
|
|
644
851
|
if (options.store === "memory") {
|
|
645
852
|
const stateFile = devStateFile(configDir);
|
|
646
|
-
if (
|
|
853
|
+
if (existsSync4(stateFile) && !options.force) {
|
|
647
854
|
console.error(
|
|
648
855
|
`skein: ${path6.relative(process.cwd(), stateFile)} already exists. Re-run with --force to overwrite it (or delete it first).`
|
|
649
856
|
);
|
|
@@ -677,8 +884,87 @@ async function runImportLanggraph(options) {
|
|
|
677
884
|
console.log(`skein: imported ${summarize(counts)} into Postgres.`);
|
|
678
885
|
}
|
|
679
886
|
|
|
887
|
+
// src/start-command.ts
|
|
888
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
889
|
+
import path7 from "path";
|
|
890
|
+
import { loadConfig as loadConfig5 } from "@skein-js/config";
|
|
891
|
+
import { createExpressServer as createExpressServer2 } from "@skein-js/express";
|
|
892
|
+
import {
|
|
893
|
+
buildRuntime as buildRuntime3
|
|
894
|
+
} from "@skein-js/runtime";
|
|
895
|
+
var FORCE_EXIT_MS2 = 5e3;
|
|
896
|
+
var logger = createDevLogger();
|
|
897
|
+
function readBakedSchemas(configDir) {
|
|
898
|
+
const schemasFile = path7.join(configDir, "schemas.json");
|
|
899
|
+
if (!existsSync5(schemasFile)) {
|
|
900
|
+
throw new Error(
|
|
901
|
+
`no schemas.json next to the config \u2014 \`skein start\` expects a built artifact. Run \`skein build\` (or \`skein up\`), or use \`skein dev\` for a source project.`
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
return JSON.parse(readFileSync4(schemasFile, "utf8"));
|
|
905
|
+
}
|
|
906
|
+
async function runStart(options) {
|
|
907
|
+
const configPath = path7.resolve(process.cwd(), options.config);
|
|
908
|
+
let schemas;
|
|
909
|
+
let configDir;
|
|
910
|
+
let authPath;
|
|
911
|
+
try {
|
|
912
|
+
const loaded = await loadConfig5({ configPath });
|
|
913
|
+
configDir = loaded.configDir;
|
|
914
|
+
authPath = loaded.config.auth?.path;
|
|
915
|
+
await applyProjectEnv(loaded.config, configDir);
|
|
916
|
+
schemas = readBakedSchemas(configDir);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
console.error(`skein: ${error instanceof Error ? error.message : String(error)}`);
|
|
919
|
+
process.exitCode = 1;
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
let runtime;
|
|
923
|
+
try {
|
|
924
|
+
runtime = await buildRuntime3({
|
|
925
|
+
configPath,
|
|
926
|
+
store: options.store,
|
|
927
|
+
queue: options.queue,
|
|
928
|
+
schemas
|
|
929
|
+
});
|
|
930
|
+
} catch (error) {
|
|
931
|
+
console.error(`skein: ${error instanceof Error ? error.message : String(error)}`);
|
|
932
|
+
process.exitCode = 1;
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (options.verbose) runtime.deps.logRunActivity = true;
|
|
936
|
+
const port = options.portExplicit ? options.port : envPort(options.port);
|
|
937
|
+
const host = options.hostExplicit ? options.host : envHost(options.host);
|
|
938
|
+
let server;
|
|
939
|
+
try {
|
|
940
|
+
server = await createExpressServer2({
|
|
941
|
+
deps: runtime.deps,
|
|
942
|
+
cors: runtime.cors,
|
|
943
|
+
warm: true,
|
|
944
|
+
logger
|
|
945
|
+
});
|
|
946
|
+
await server.listen(port, host);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
await runtime.dispose();
|
|
949
|
+
console.error(`skein: ${describeBindError(error, port)}`);
|
|
950
|
+
process.exitCode = 1;
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
printBanner({ host, port, graphIds: runtime.deps.graphs.ids, authPath, workerCount: 1 }, logger);
|
|
954
|
+
let shuttingDown = false;
|
|
955
|
+
const shutdown = () => {
|
|
956
|
+
if (shuttingDown) return;
|
|
957
|
+
shuttingDown = true;
|
|
958
|
+
const forceExit = setTimeout(() => process.exit(0), FORCE_EXIT_MS2);
|
|
959
|
+
forceExit.unref();
|
|
960
|
+
void Promise.allSettled([server.close(), runtime.dispose()]).then(() => process.exit(0));
|
|
961
|
+
};
|
|
962
|
+
process.on("SIGINT", shutdown);
|
|
963
|
+
process.on("SIGTERM", shutdown);
|
|
964
|
+
}
|
|
965
|
+
|
|
680
966
|
// src/index.ts
|
|
681
|
-
var require2 =
|
|
967
|
+
var require2 = createRequire3(import.meta.url);
|
|
682
968
|
var { version } = require2("../package.json");
|
|
683
969
|
function parsePort(value) {
|
|
684
970
|
const port = Number(value);
|
|
@@ -707,12 +993,19 @@ program.command("dev").description("Run the in-process dev server with hot reloa
|
|
|
707
993
|
hostExplicit: command.getOptionValueSource("host") === "cli"
|
|
708
994
|
})
|
|
709
995
|
);
|
|
996
|
+
program.command("start").description("Serve a pre-built .skein/build artifact (the production image entrypoint).").option("-c, --config <path>", "Path to the artifact's langgraph.json", "langgraph.json").option("-p, --port <port>", "Port to bind", parsePort, 2024).option("--host <host>", "Host to bind", "127.0.0.1").option("--store <driver>", "Store driver: memory | postgres", parseStore, "memory").option("--queue <driver>", "Queue driver: memory | redis", parseQueue, "memory").option("-v, --verbose", "Log per-run activity: start/finish, tool calls, and interrupts").action(
|
|
997
|
+
(options, command) => runStart({
|
|
998
|
+
...options,
|
|
999
|
+
portExplicit: command.getOptionValueSource("port") === "cli",
|
|
1000
|
+
hostExplicit: command.getOptionValueSource("host") === "cli"
|
|
1001
|
+
})
|
|
1002
|
+
);
|
|
710
1003
|
program.command("up").description("Bring up the production stack (Docker Compose: app + Postgres + Redis).").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option("-p, --port <port>", "Port to expose", parsePort, 8123).option("--host <host>", "Host to bind", "0.0.0.0").action((options) => runUp(options));
|
|
711
1004
|
program.command("build").description("Build a deployable Docker image from the config.").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option("-t, --tag <tag>", "Image tag (defaults to the project directory name)").action((options) => runBuild(options));
|
|
712
1005
|
program.command("dockerfile").description("Emit a standalone Dockerfile from the config.").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option("-o, --output <path>", "Write the Dockerfile here instead of stdout").action((options) => runDockerfile(options));
|
|
713
1006
|
program.command("import-langgraph").description("Import an existing LangGraph in-memory dev state (.langgraph_api/) into skein.").option("-c, --config <path>", "Path to langgraph.json", "langgraph.json").option(
|
|
714
1007
|
"--store <driver>",
|
|
715
|
-
"Import target: memory (.skein/dev-state.json) | postgres (
|
|
1008
|
+
"Import target: memory (.skein/dev-state.json) | postgres (POSTGRES_URI)",
|
|
716
1009
|
parseStore,
|
|
717
1010
|
"memory"
|
|
718
1011
|
).option("--from <dir>", "Source .langgraph_api directory (defaults to alongside langgraph.json)").option("--force", "Overwrite an existing .skein/dev-state.json (memory target)", false).action((options) => runImportLanggraph(options));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skein-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The skein-js CLI — a drop-in replacement for the LangGraph CLI (dev/up/build/dockerfile).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Maina Wycliffe <wmmaina7@gmail.com>",
|
|
@@ -47,11 +47,13 @@
|
|
|
47
47
|
"@commander-js/extra-typings": "~13.1.0",
|
|
48
48
|
"@langchain/langgraph-checkpoint-postgres": "^1.0.4",
|
|
49
49
|
"commander": "~13.1.0",
|
|
50
|
-
"
|
|
51
|
-
"@skein-js/
|
|
52
|
-
"@skein-js/config": "0.
|
|
53
|
-
"@skein-js/
|
|
54
|
-
|
|
50
|
+
"@skein-js/core": "0.5.0",
|
|
51
|
+
"@skein-js/express": "0.5.0",
|
|
52
|
+
"@skein-js/config": "0.5.0",
|
|
53
|
+
"@skein-js/runtime": "0.5.0"
|
|
54
|
+
},
|
|
55
|
+
"optionalDependencies": {
|
|
56
|
+
"vite": "^8.1.0"
|
|
55
57
|
},
|
|
56
58
|
"peerDependencies": {
|
|
57
59
|
"@langchain/langgraph": "^1.4.0"
|