skein-js 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +393 -112
  2. package/package.json +8 -6
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, logger) {
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) logger.info(`Registering graph with id '${id}'`);
39
- if (authPath) logger.info(`Loading auth from ${authPath}`);
40
- logger.info(`Starting ${workerCount} worker${workerCount === 1 ? "" : "s"}`);
41
- logger.info(`Server running at ${base}`);
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,13 +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 {
141
- createServer,
142
- createServerModuleRunner,
143
- searchForWorkspaceRoot
144
- } from "vite";
145
159
  function findFreePort() {
146
160
  return new Promise((resolve, reject) => {
147
161
  const probe = createNetServer();
@@ -154,6 +168,7 @@ function findFreePort() {
154
168
  });
155
169
  }
156
170
  async function createViteGraphLoader(root, ignored = []) {
171
+ const { createServer, createServerModuleRunner, searchForWorkspaceRoot } = await import("vite");
157
172
  const workspaceRoot = searchForWorkspaceRoot(root);
158
173
  const server = await createServer({
159
174
  root,
@@ -195,16 +210,6 @@ var RELOAD_DEBOUNCE_MS = 120;
195
210
  var AUTOSAVE_MS = 2e3;
196
211
  var FORCE_EXIT_MS = 5e3;
197
212
  var devLogger = createDevLogger();
198
- function envPort(fallback) {
199
- const raw = process.env.PORT;
200
- if (raw === void 0 || raw.trim() === "") return fallback;
201
- const port = Number(raw);
202
- return Number.isInteger(port) && port >= 0 && port <= 65535 ? port : fallback;
203
- }
204
- function envHost(fallback) {
205
- const host = process.env.HOST;
206
- return host !== void 0 && host.trim() !== "" ? host : fallback;
207
- }
208
213
  async function runDev(options) {
209
214
  const configPath = path3.resolve(process.cwd(), options.config);
210
215
  const { config, configDir } = await loadConfig({ configPath });
@@ -263,10 +268,7 @@ async function runDev(options) {
263
268
  await server.listen(port, host);
264
269
  } catch (error) {
265
270
  await Promise.allSettled([loader.close(), runtime.dispose()]);
266
- const code = error.code;
267
- console.error(
268
- 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)}`
269
- );
271
+ console.error(`skein: ${describeBindError(error, port)}`);
270
272
  process.exitCode = 1;
271
273
  return;
272
274
  }
@@ -357,9 +359,206 @@ async function runDev(options) {
357
359
 
358
360
  // src/docker/commands.ts
359
361
  import { spawn, spawnSync } from "child_process";
360
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
362
+ import { writeFileSync as writeFileSync2 } from "fs";
363
+ import { createRequire as createRequire2 } from "module";
361
364
  import path5 from "path";
362
- import { loadConfig as loadConfig2 } from "@skein-js/config";
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
+ }
363
562
 
364
563
  // src/docker/compose.ts
365
564
  function portMapping(options) {
@@ -421,37 +620,10 @@ volumes:
421
620
  }
422
621
 
423
622
  // src/docker/dockerfile.ts
424
- import { existsSync as existsSync3 } from "fs";
425
- import path4 from "path";
426
623
  function baseImage(nodeVersion) {
427
624
  const major = nodeVersion?.trim().match(/^\d+/)?.[0] ?? "20";
428
625
  return `node:${major}-slim`;
429
626
  }
430
- function detectPackageManager(projectDir) {
431
- if (existsSync3(path4.join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
432
- if (existsSync3(path4.join(projectDir, "yarn.lock"))) return "yarn";
433
- return "npm";
434
- }
435
- function installSteps(manager) {
436
- switch (manager) {
437
- case "pnpm":
438
- return {
439
- copyLock: "COPY package.json pnpm-lock.yaml* ./",
440
- install: "RUN --mount=type=cache,target=/root/.local/share/pnpm/store corepack enable && pnpm install --frozen-lockfile"
441
- };
442
- case "yarn":
443
- return {
444
- copyLock: "COPY package.json yarn.lock* ./",
445
- install: "RUN --mount=type=cache,target=/usr/local/share/.cache/yarn corepack enable && yarn install --frozen-lockfile"
446
- };
447
- case "npm":
448
- return {
449
- copyLock: "COPY package.json package-lock.json* ./",
450
- // `npm ci` needs a lockfile; fall back to `npm install` when there isn't one.
451
- install: "RUN --mount=type=cache,target=/root/.npm npm ci || npm install"
452
- };
453
- }
454
- }
455
627
  function generateDockerignore() {
456
628
  return [
457
629
  "# Generated by skein \u2014 regenerated by `skein build` / `skein up`.",
@@ -459,7 +631,6 @@ function generateDockerignore() {
459
631
  "node_modules",
460
632
  ".env",
461
633
  ".env.*",
462
- ".skein",
463
634
  "Dockerfile",
464
635
  "compose.yaml",
465
636
  ".dockerignore",
@@ -468,65 +639,83 @@ function generateDockerignore() {
468
639
  ].join("\n");
469
640
  }
470
641
  function generateDockerfile(options) {
471
- const { nodeVersion, dockerfileLines = [], port, packageManager } = options;
472
- const { copyLock, install } = installSteps(packageManager);
642
+ const { nodeVersion, dockerfileLines = [], port } = options;
473
643
  const lines = [
474
- // The syntax directive must be the first line to be honored — it enables the RUN cache mounts.
644
+ // The syntax directive must be the first line to be honored — it enables the RUN cache mount.
475
645
  `# syntax=docker/dockerfile:1`,
476
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).`,
477
651
  `FROM ${baseImage(nodeVersion)}`,
478
- // Hand /app to the unprivileged 'node' user (uid 1000, shipped by the base image) so the server
479
- // never runs as root. corepack/npm need root to install, so install as root, then chown the
480
- // whole tree — including the root-created node_modules, where vite writes its cache at runtime.
481
652
  `WORKDIR /app`,
482
653
  ``,
483
- `# Install dependencies first for better layer caching (BuildKit cache mount speeds rebuilds).`,
484
- copyLock,
485
- install,
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`,
486
661
  ``,
487
- `# Copy the rest of the project (graphs, langgraph.json, source).`,
662
+ `# Copy the pre-built artifact (bundled JS graphs, production langgraph.json, baked schemas).`,
488
663
  `COPY . .`,
489
664
  ``,
490
- `# Give the runtime user ownership of the app + installed node_modules so vite can write its`,
491
- `# on-disk cache under node_modules/.vite without EACCES.`,
492
- `RUN chown -R node:node /app`,
493
- ``,
494
- // Set NODE_ENV only after install so devDependencies (the vite/tsx toolchain `skein dev` loads
495
- // graphs with) are installed; setting it before install would prune them and break graph loading.
496
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`,
497
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.
498
673
  `USER node`,
499
674
  ...dockerfileLines,
500
675
  ``,
501
- `# Serve the Agent Protocol against Postgres + Redis (POSTGRES_URI / REDIS_URI from the environment).`,
676
+ `# Serve the compiled artifact against Postgres + Redis (POSTGRES_URI / REDIS_URI from the env).`,
502
677
  `# No --port: the server binds $PORT when the platform injects one (Railway/Fly/Render), else its`,
503
678
  `# default. Exec form keeps node as PID 1 so SIGTERM reaches skein's graceful-shutdown handler.`,
504
- `CMD ["npx", "skein", "dev", "--no-reload", "--no-persist", "--store", "postgres", "--queue", "redis", "--host", "0.0.0.0"]`
679
+ `CMD ["npx", "skein", "start", "--store", "postgres", "--queue", "redis", "--host", "0.0.0.0"]`
505
680
  ];
506
681
  return `${lines.join("\n")}
507
682
  `;
508
683
  }
509
684
 
510
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");
511
690
  var CONTAINER_PORT = 8123;
512
691
  var DOCKERFILE_NAME = "Dockerfile";
513
692
  var COMPOSE_NAME = "compose.yaml";
514
693
  var DOCKERIGNORE_NAME = ".dockerignore";
515
- var GENERATED_MARKER = "# Generated by skein";
516
- function isSkeinGenerated(contents) {
517
- return contents.split("\n", 3).some((line2) => line2.startsWith(GENERATED_MARKER));
518
- }
519
694
  async function loadConfigContext(configOption) {
520
695
  const configPath = path5.resolve(process.cwd(), configOption);
521
- const { config, configDir } = await loadConfig2({ configPath });
522
- 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;
523
713
  }
524
- function renderDockerfile({ config, configDir }, port) {
714
+ function renderDockerfile({ config }, port) {
525
715
  return generateDockerfile({
526
716
  nodeVersion: config.node_version,
527
717
  dockerfileLines: config.dockerfile_lines,
528
- port,
529
- packageManager: detectPackageManager(configDir)
718
+ port
530
719
  });
531
720
  }
532
721
  function defaultImageTag(configDir) {
@@ -563,19 +752,12 @@ function wasInterrupted(result) {
563
752
  function describeExit(result) {
564
753
  return result.signal !== null ? `killed by ${result.signal}` : `exit ${result.code}`;
565
754
  }
566
- function writeGeneratedFile(file, contents) {
567
- const name = path5.basename(file);
568
- if (existsSync4(file) && !isSkeinGenerated(readFileSync3(file, "utf8"))) {
569
- console.log(`skein: ${name} exists and wasn't generated by skein \u2014 using it as-is.`);
570
- return "kept-user";
571
- }
572
- writeFileSync2(file, contents);
573
- console.log(`skein: wrote ${name}.`);
574
- return "wrote";
575
- }
576
755
  async function runDockerfile(options) {
577
756
  const context = await loadConfigContext(options.config);
578
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
+ );
579
761
  if (options.output !== void 0) {
580
762
  const target = path5.resolve(process.cwd(), options.output);
581
763
  writeFileSync2(target, contents);
@@ -587,15 +769,20 @@ async function runDockerfile(options) {
587
769
  async function runBuild(options) {
588
770
  const context = await loadConfigContext(options.config);
589
771
  if (!requireDocker()) return;
590
- const dockerfilePath = path5.join(context.configDir, DOCKERFILE_NAME);
591
- writeGeneratedFile(dockerfilePath, renderDockerfile(context, CONTAINER_PORT));
592
- writeGeneratedFile(path5.join(context.configDir, DOCKERIGNORE_NAME), generateDockerignore());
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
+ }
593
780
  const tag = options.tag ?? defaultImageTag(context.configDir);
594
781
  console.log(`skein: building image "${tag}"\u2026`);
595
782
  const result = await runToCompletion(
596
783
  "docker",
597
- ["build", "-t", tag, "-f", dockerfilePath, "."],
598
- context.configDir
784
+ ["build", "-t", tag, "-f", path5.join(artifactDir, DOCKERFILE_NAME), "."],
785
+ artifactDir
599
786
  );
600
787
  if (!succeeded(result)) {
601
788
  console.error(`skein: docker build failed (${describeExit(result)}).`);
@@ -607,22 +794,30 @@ async function runBuild(options) {
607
794
  async function runUp(options) {
608
795
  const context = await loadConfigContext(options.config);
609
796
  if (!requireDocker()) return;
610
- writeGeneratedFile(
611
- path5.join(context.configDir, DOCKERFILE_NAME),
612
- renderDockerfile(context, CONTAINER_PORT)
613
- );
614
- writeGeneratedFile(path5.join(context.configDir, DOCKERIGNORE_NAME), generateDockerignore());
615
- writeGeneratedFile(
616
- path5.join(context.configDir, COMPOSE_NAME),
617
- generateCompose({ hostPort: options.port, host: options.host, containerPort: CONTAINER_PORT })
618
- );
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
+ }
619
814
  console.log(
620
815
  `skein: bringing up the stack (app + Postgres + Redis) on http://${options.host}:${options.port}\u2026`
621
816
  );
622
817
  const result = await runToCompletion(
623
818
  "docker",
624
819
  ["compose", "-f", COMPOSE_NAME, "up", "--build"],
625
- context.configDir
820
+ artifactDir
626
821
  );
627
822
  if (!succeeded(result) && !wasInterrupted(result)) {
628
823
  console.error(`skein: docker compose up failed (${describeExit(result)}).`);
@@ -631,9 +826,9 @@ async function runUp(options) {
631
826
  }
632
827
 
633
828
  // src/import-command.ts
634
- import { existsSync as existsSync5 } from "fs";
829
+ import { existsSync as existsSync4 } from "fs";
635
830
  import path6 from "path";
636
- import { loadConfig as loadConfig3 } from "@skein-js/config";
831
+ import { loadConfig as loadConfig4 } from "@skein-js/config";
637
832
  import {
638
833
  describeSnapshot as describeSnapshot2,
639
834
  loadSnapshotIntoStore,
@@ -645,7 +840,7 @@ function summarize(counts) {
645
840
  }
646
841
  async function runImportLanggraph(options) {
647
842
  const configPath = path6.resolve(process.cwd(), options.config);
648
- const { config, configDir } = await loadConfig3({ configPath });
843
+ const { config, configDir } = await loadConfig4({ configPath });
649
844
  const sourceDir = options.from ? path6.resolve(process.cwd(), options.from) : path6.join(configDir, LANGGRAPH_DIR);
650
845
  const snapshot = await readLanggraphDevState2(sourceDir);
651
846
  if (snapshot === null) {
@@ -655,7 +850,7 @@ async function runImportLanggraph(options) {
655
850
  const counts = describeSnapshot2(snapshot);
656
851
  if (options.store === "memory") {
657
852
  const stateFile = devStateFile(configDir);
658
- if (existsSync5(stateFile) && !options.force) {
853
+ if (existsSync4(stateFile) && !options.force) {
659
854
  console.error(
660
855
  `skein: ${path6.relative(process.cwd(), stateFile)} already exists. Re-run with --force to overwrite it (or delete it first).`
661
856
  );
@@ -689,8 +884,87 @@ async function runImportLanggraph(options) {
689
884
  console.log(`skein: imported ${summarize(counts)} into Postgres.`);
690
885
  }
691
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
+
692
966
  // src/index.ts
693
- var require2 = createRequire(import.meta.url);
967
+ var require2 = createRequire3(import.meta.url);
694
968
  var { version } = require2("../package.json");
695
969
  function parsePort(value) {
696
970
  const port = Number(value);
@@ -719,6 +993,13 @@ program.command("dev").description("Run the in-process dev server with hot reloa
719
993
  hostExplicit: command.getOptionValueSource("host") === "cli"
720
994
  })
721
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
+ );
722
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));
723
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));
724
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skein-js",
3
- "version": "0.4.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
- "vite": "^8.1.0",
51
- "@skein-js/core": "0.4.0",
52
- "@skein-js/express": "0.4.0",
53
- "@skein-js/config": "0.4.0",
54
- "@skein-js/runtime": "0.4.0"
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"