@elaraai/create-e3 1.0.30 → 1.0.31

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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // ../scaffold-core/dist/scaffold.js
4
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
5
- import { join, relative } from "node:path";
4
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
5
+ import { join as join2, relative as relative2 } from "node:path";
6
6
  import { spawnSync } from "node:child_process";
7
7
 
8
8
  // ../scaffold-core/dist/names.js
@@ -18,6 +18,203 @@ function deriveNames(rawName, cwd) {
18
18
  return { projectName, displayName, workspaceName };
19
19
  }
20
20
 
21
+ // ../scaffold-core/dist/packages.js
22
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
23
+ import { join, relative } from "node:path";
24
+ var RUNTIMES = {
25
+ // Python package/dir names must be import-safe (letters, digits, underscore).
26
+ python: {
27
+ runtime: "python",
28
+ destParent: "packages/python",
29
+ templateName: "python",
30
+ label: "Python",
31
+ valid: /^[a-z][a-z0-9_]*$/,
32
+ rule: "lowercase letters, digits and underscores, starting with a letter"
33
+ },
34
+ // Node member names become the last segment of an npm dir; keep them tame.
35
+ node: {
36
+ runtime: "node",
37
+ destParent: "packages/node",
38
+ templateName: "node",
39
+ label: "Node",
40
+ valid: /^[a-z][a-z0-9-]*$/,
41
+ rule: "lowercase letters, digits and hyphens, starting with a letter"
42
+ },
43
+ // C build dir names — letters, digits, underscore, hyphen.
44
+ c: {
45
+ runtime: "c",
46
+ destParent: "packages/native",
47
+ templateName: "c",
48
+ label: "C",
49
+ valid: /^[a-z][a-z0-9_-]*$/,
50
+ rule: "lowercase letters, digits, underscores and hyphens, starting with a letter"
51
+ }
52
+ };
53
+ var RUNTIME_ORDER = ["python", "node", "c"];
54
+ function toIdent(name) {
55
+ return name.replace(/[^a-zA-Z0-9]/g, "_");
56
+ }
57
+ function substitute(text, names, version, pkg) {
58
+ let out = text.replaceAll("__PROJECT_NAME__", names.projectName).replaceAll("__DISPLAY_NAME__", names.displayName).replaceAll("__WORKSPACE_NAME__", names.workspaceName).replaceAll("__VERSION__", version);
59
+ if (pkg)
60
+ out = out.replaceAll("__PACKAGE_NAME__", pkg.name).replaceAll("__PACKAGE_IDENT__", pkg.ident);
61
+ return out;
62
+ }
63
+ function walkFiles(dir) {
64
+ const out = [];
65
+ for (const entry of readdirSync(dir)) {
66
+ const full = join(dir, entry);
67
+ if (statSync(full).isDirectory())
68
+ out.push(...walkFiles(full));
69
+ else
70
+ out.push(full);
71
+ }
72
+ return out;
73
+ }
74
+ function stampMember(projectDir, templateRoot, cfg, names, version, pkg) {
75
+ const memberTemplate = join(templateRoot, cfg.templateName);
76
+ if (!existsSync(memberTemplate)) {
77
+ throw new Error(`internal: missing package template for ${cfg.label} at ${memberTemplate}`);
78
+ }
79
+ const memberDir = join(projectDir, cfg.destParent, pkg.name);
80
+ for (const srcPath of walkFiles(memberTemplate)) {
81
+ const rel = relative(memberTemplate, srcPath).replaceAll("\\", "/");
82
+ if (!rel.includes("/") && rel.startsWith("_")) {
83
+ if (rel === "_app.ts") {
84
+ const dest2 = join(projectDir, "src", "packages", `${pkg.name}.ts`);
85
+ mkdirSync(join(dest2, ".."), { recursive: true });
86
+ writeFileSync(dest2, substitute(readFileSync(srcPath, "utf8"), names, version, pkg));
87
+ }
88
+ continue;
89
+ }
90
+ const dest = join(memberDir, substitute(rel, names, version, pkg));
91
+ mkdirSync(join(dest, ".."), { recursive: true });
92
+ writeFileSync(dest, substitute(readFileSync(srcPath, "utf8"), names, version, pkg));
93
+ }
94
+ }
95
+ function writePythonWorkspaceRoot(projectDir, names, members) {
96
+ const sources = members.map((m) => `${m} = { workspace = true }`).join("\n");
97
+ const body = `[project]
98
+ name = "${names.projectName}"
99
+ version = "0.1.0"
100
+ description = "${names.displayName}"
101
+ requires-python = ">=3.11"
102
+ dependencies = [
103
+ "elaraai-east-py",
104
+ "elaraai-east-py-std",
105
+ "elaraai-east-py-io",
106
+ "elaraai-east-py-datascience",
107
+ "elaraai-east-py-cli",
108
+ ]
109
+
110
+ # This project is a uv workspace root: each package under packages/python/*
111
+ # is its own member with its own dependency closure (and its own captured
112
+ # execution environment). No [build-system] \u2014 the root is not itself a
113
+ # Python package, just the workspace + the shared runtime dependencies.
114
+ [tool.uv.workspace]
115
+ members = ["packages/python/*"]
116
+
117
+ [tool.uv.sources]
118
+ ${sources}
119
+ `;
120
+ writeFileSync(join(projectDir, "pyproject.toml"), body);
121
+ }
122
+ function patchNodeWorkspaceRoot(projectDir) {
123
+ const path = join(projectDir, "package.json");
124
+ const pkg = JSON.parse(readFileSync(path, "utf8"));
125
+ const globs = new Set(Array.isArray(pkg.workspaces) ? pkg.workspaces : []);
126
+ globs.add("packages/node/*");
127
+ pkg.workspaces = [...globs];
128
+ pkg.private = true;
129
+ const scripts = pkg.scripts ?? {};
130
+ if (typeof scripts.build === "string" && !scripts.build.includes("--workspaces")) {
131
+ scripts.build = `npm run build --workspaces --if-present && ${scripts.build}`;
132
+ pkg.scripts = scripts;
133
+ }
134
+ writeFileSync(path, `${JSON.stringify(pkg, null, 2)}
135
+ `);
136
+ }
137
+ function excludePackagesFromRootTsconfig(projectDir) {
138
+ const path = join(projectDir, "tsconfig.json");
139
+ if (!existsSync(path))
140
+ return;
141
+ const tsconfig = JSON.parse(readFileSync(path, "utf8"));
142
+ const exclude = new Set(tsconfig.exclude ?? []);
143
+ exclude.add("packages");
144
+ tsconfig.exclude = [...exclude];
145
+ writeFileSync(path, `${JSON.stringify(tsconfig, null, 2)}
146
+ `);
147
+ }
148
+ function writeAppWiring(projectDir, names, members) {
149
+ const imports = members.map((m) => `import { ${m.ident}_task } from "./${m.name}.js";`).join("\n");
150
+ const list = members.map((m) => ` ${m.ident}_task,`).join("\n");
151
+ const barrel = `// Generated by create-e3 \u2014 collects one example task per workspace package.
152
+ // Each task's \`environment\` points at its package, so editing one package
153
+ // re-runs only its task. Add packages with \`create-e3 --python-packages=\u2026\`.
154
+ ${imports}
155
+
156
+ export const packageTasks = [
157
+ ${list}
158
+ ];
159
+ `;
160
+ mkdirSync(join(projectDir, "src", "packages"), { recursive: true });
161
+ writeFileSync(join(projectDir, "src", "packages", "index.ts"), barrel);
162
+ const index = `import e3 from "@elaraai/e3";
163
+
164
+ import { packageTasks } from "./packages/index.js";
165
+
166
+ // This app wires one example task per generated workspace package. Every
167
+ // task's \`environment\` is captured independently, so editing one package
168
+ // re-runs only its task \u2014 the other packages stay cached. Add your own
169
+ // inputs, tasks and dataflows here.
170
+ export default e3.package("${names.projectName}", "1.0.0", ...packageTasks);
171
+ `;
172
+ writeFileSync(join(projectDir, "src", "index.ts"), index);
173
+ }
174
+ function generatePackages(options) {
175
+ const { projectDir, templateDir, names, version, spec, log } = options;
176
+ const templateRoot = join(templateDir, "_packages");
177
+ const members = [];
178
+ const seen = /* @__PURE__ */ new Map();
179
+ for (const runtime of RUNTIME_ORDER) {
180
+ const cfg = RUNTIMES[runtime];
181
+ for (const raw of spec[runtime] ?? []) {
182
+ const name = raw.trim();
183
+ if (!name)
184
+ throw new Error(`--${runtime}-packages contains an empty package name`);
185
+ if (!cfg.valid.test(name)) {
186
+ throw new Error(`${cfg.label} package name '${name}' is invalid \u2014 use ${cfg.rule}`);
187
+ }
188
+ const prior = seen.get(name);
189
+ if (prior) {
190
+ throw new Error(`package name '${name}' is declared twice (${prior} and ${runtime}) \u2014 names must be unique`);
191
+ }
192
+ seen.set(name, runtime);
193
+ members.push({ name, ident: toIdent(name), runtime });
194
+ }
195
+ }
196
+ if (members.length === 0)
197
+ return;
198
+ for (const runtime of RUNTIME_ORDER) {
199
+ if ((spec[runtime]?.length ?? 0) > 0 && !existsSync(join(templateRoot, RUNTIMES[runtime].templateName))) {
200
+ throw new Error(`${RUNTIMES[runtime].label} packages are not available in this scaffold yet \u2014 remove --${runtime}-packages for now`);
201
+ }
202
+ }
203
+ for (const pkg of members) {
204
+ stampMember(projectDir, templateRoot, RUNTIMES[pkg.runtime], names, version, pkg);
205
+ }
206
+ const py = members.filter((m) => m.runtime === "python").map((m) => m.name);
207
+ const node = members.filter((m) => m.runtime === "node").map((m) => m.name);
208
+ if (py.length > 0)
209
+ writePythonWorkspaceRoot(projectDir, names, py);
210
+ if (node.length > 0)
211
+ patchNodeWorkspaceRoot(projectDir);
212
+ excludePackagesFromRootTsconfig(projectDir);
213
+ writeAppWiring(projectDir, names, members);
214
+ const summary = RUNTIME_ORDER.map((r) => ({ r, n: members.filter((m) => m.runtime === r).length })).filter((x) => x.n > 0).map((x) => `${x.n} ${RUNTIMES[x.r].label}`).join(", ");
215
+ log(`Generated ${members.length} workspace package${members.length === 1 ? "" : "s"} (${summary}).`);
216
+ }
217
+
21
218
  // ../scaffold-core/dist/scaffold.js
22
219
  var TEXT_REPLACE_EXT = /* @__PURE__ */ new Set([
23
220
  ".ts",
@@ -102,9 +299,9 @@ function transformPackageJson(raw, names, version, manifest, enabled) {
102
299
  }
103
300
  function walk(dir) {
104
301
  const out = [];
105
- for (const entry of readdirSync(dir)) {
106
- const full = join(dir, entry);
107
- if (statSync(full).isDirectory())
302
+ for (const entry of readdirSync2(dir)) {
303
+ const full = join2(dir, entry);
304
+ if (statSync2(full).isDirectory())
108
305
  out.push(...walk(full));
109
306
  else
110
307
  out.push(full);
@@ -117,16 +314,16 @@ function extOf(path) {
117
314
  return dot <= 0 ? "" : base.slice(dot);
118
315
  }
119
316
  function loadManifest(templateDir) {
120
- const path = join(templateDir, MANIFEST_FILE);
121
- if (!existsSync(path))
317
+ const path = join2(templateDir, MANIFEST_FILE);
318
+ if (!existsSync2(path))
122
319
  return null;
123
- return JSON.parse(readFileSync(path, "utf8"));
320
+ return JSON.parse(readFileSync2(path, "utf8"));
124
321
  }
125
322
  function scaffold(options) {
126
323
  const { kind, name, templateDir, version } = options;
127
324
  const cwd = options.cwd ?? process.cwd();
128
325
  const log = options.log ?? ((m) => console.log(m));
129
- if (!existsSync(templateDir)) {
326
+ if (!existsSync2(templateDir)) {
130
327
  throw new Error(`Template directory not found: ${templateDir}`);
131
328
  }
132
329
  const manifest = loadManifest(templateDir);
@@ -164,34 +361,39 @@ function scaffold(options) {
164
361
  }
165
362
  const names = deriveNames(name, cwd);
166
363
  const inPlace = name === ".";
167
- const projectDir = inPlace ? cwd : join(cwd, names.projectName);
168
- if (!inPlace && existsSync(projectDir)) {
364
+ const projectDir = inPlace ? cwd : join2(cwd, names.projectName);
365
+ if (!inPlace && existsSync2(projectDir)) {
169
366
  throw new Error(`Directory already exists: ${projectDir}`);
170
367
  }
171
- mkdirSync(projectDir, { recursive: true });
368
+ mkdirSync2(projectDir, { recursive: true });
172
369
  for (const srcPath of walk(templateDir)) {
173
- const rel = relative(templateDir, srcPath).replaceAll("\\", "/");
370
+ const rel = relative2(templateDir, srcPath).replaceAll("\\", "/");
174
371
  if (rel === MANIFEST_FILE)
175
372
  continue;
373
+ if (rel.split("/")[0].startsWith("_"))
374
+ continue;
176
375
  if (skip.has(rel))
177
376
  continue;
178
377
  const destRel = renames[rel] ?? rel;
179
378
  const segments = destRel.split("/");
180
379
  const last = segments[segments.length - 1];
181
380
  segments[segments.length - 1] = DOTFILE_RENAMES[last] ?? last;
182
- const destPath = join(projectDir, segments.join("/"));
183
- mkdirSync(join(destPath, ".."), { recursive: true });
381
+ const destPath = join2(projectDir, segments.join("/"));
382
+ mkdirSync2(join2(destPath, ".."), { recursive: true });
184
383
  const baseName = segments[segments.length - 1];
185
384
  if (baseName === "package.json") {
186
- const transformed = transformPackageJson(readFileSync(srcPath, "utf8"), names, version, manifest, enabled);
187
- writeFileSync(destPath, substituteTokens(transformed, names));
385
+ const transformed = transformPackageJson(readFileSync2(srcPath, "utf8"), names, version, manifest, enabled);
386
+ writeFileSync2(destPath, substituteTokens(transformed, names));
188
387
  } else if (TEXT_REPLACE_EXT.has(extOf(srcPath)) || baseName.startsWith(".")) {
189
- writeFileSync(destPath, substituteTokens(readFileSync(srcPath, "utf8"), names));
388
+ writeFileSync2(destPath, substituteTokens(readFileSync2(srcPath, "utf8"), names));
190
389
  } else {
191
- writeFileSync(destPath, readFileSync(srcPath));
390
+ writeFileSync2(destPath, readFileSync2(srcPath));
192
391
  }
193
392
  }
194
393
  log(`Created ${names.projectName} (${kind}) at ${projectDir}`);
394
+ if (options.packages) {
395
+ generatePackages({ projectDir, templateDir, names, version, spec: options.packages, log });
396
+ }
195
397
  if (options.install) {
196
398
  runInstall(kind, projectDir, enabled, log);
197
399
  }
@@ -221,35 +423,36 @@ function runInstall(kind, projectDir, enabled, log) {
221
423
  }
222
424
 
223
425
  // ../scaffold-core/dist/cli.js
224
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
225
- import { dirname, join as join2 } from "node:path";
426
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
427
+ import { dirname, join as join3 } from "node:path";
226
428
  import { createInterface } from "node:readline/promises";
227
429
  import { fileURLToPath } from "node:url";
228
430
  async function runCreateCli(kind, moduleUrl) {
229
- const pkgRoot = join2(dirname(fileURLToPath(moduleUrl)), "..");
431
+ const pkgRoot = join3(dirname(fileURLToPath(moduleUrl)), "..");
230
432
  const args = process.argv.slice(2);
231
433
  if (args.includes("--help") || args.includes("-h")) {
232
434
  printHelp(kind);
233
435
  return;
234
436
  }
235
- const version = JSON.parse(readFileSync2(join2(pkgRoot, "package.json"), "utf8")).version;
236
- const templateDir = join2(pkgRoot, "templates", kind);
437
+ const version = JSON.parse(readFileSync3(join3(pkgRoot, "package.json"), "utf8")).version;
438
+ const templateDir = join3(pkgRoot, "templates", kind);
237
439
  const name = args.find((a) => !a.startsWith("-")) ?? ".";
238
440
  const install = args.includes("--install") ? true : args.includes("--no-install") ? false : Boolean(process.stdout.isTTY);
239
441
  try {
240
- const features = await resolveFeatures(templateDir, args);
241
- const result = scaffold({ kind, name, templateDir, version, install, features });
442
+ const packages = kind === "e3" ? parsePackages(args) : void 0;
443
+ const features = await resolveFeatures(templateDir, args, packages);
444
+ const result = scaffold({ kind, name, templateDir, version, install, features, packages });
242
445
  printNextSteps(kind, result.projectName, result.inPlace, install, features);
243
446
  } catch (err) {
244
447
  console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
245
448
  process.exit(1);
246
449
  }
247
450
  }
248
- async function resolveFeatures(templateDir, args) {
249
- const manifestPath = join2(templateDir, "template.json");
250
- if (!existsSync2(manifestPath))
451
+ async function resolveFeatures(templateDir, args, packages) {
452
+ const manifestPath = join3(templateDir, "template.json");
453
+ if (!existsSync3(manifestPath))
251
454
  return {};
252
- const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
455
+ const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
253
456
  const features = {};
254
457
  for (const [key, spec] of Object.entries(manifest.features)) {
255
458
  if (spec.allOf)
@@ -259,7 +462,9 @@ async function resolveFeatures(templateDir, args) {
259
462
  const runnerKeys = Object.keys(manifest.features).filter((k) => k.startsWith("runner:"));
260
463
  const runnerName = (key) => key.slice("runner:".length);
261
464
  const runnersFlag = args.find((a) => a.startsWith("--runners="));
262
- const selectionFlags = ["--tests", "--no-tests", "--ui", "--no-ui", "--platform", "--no-platform", "--eslint", "--no-eslint"].some((f) => args.includes(f)) || Boolean(runnersFlag);
465
+ const selectionFlags = ["--tests", "--no-tests", "--ui", "--no-ui", "--platform", "--no-platform", "--eslint", "--no-eslint"].some((f) => args.includes(f)) || Boolean(runnersFlag) || // `--python-packages=…` / `--node-packages=…` / `--c-packages=…` are an
466
+ // explicit non-interactive selection: skip the prompt.
467
+ Boolean(packages);
263
468
  if (args.includes("--tests"))
264
469
  features["tests"] = true;
265
470
  if (args.includes("--no-tests"))
@@ -285,6 +490,15 @@ async function resolveFeatures(templateDir, args) {
285
490
  for (const key of runnerKeys)
286
491
  features[key] = chosen.has(runnerName(key));
287
492
  }
493
+ if (packages) {
494
+ if (packages.python?.length && "runner:east-py" in manifest.features)
495
+ features["runner:east-py"] = true;
496
+ if (packages.node?.length && "runner:east-node" in manifest.features)
497
+ features["runner:east-node"] = true;
498
+ if (packages.c?.length && "runner:east-c" in manifest.features)
499
+ features["runner:east-c"] = true;
500
+ features["platform"] = false;
501
+ }
288
502
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !selectionFlags;
289
503
  if (!interactive)
290
504
  return features;
@@ -317,6 +531,28 @@ async function resolveFeatures(templateDir, args) {
317
531
  }
318
532
  return features;
319
533
  }
534
+ function parsePackages(args) {
535
+ const grab = (flag) => {
536
+ const arg = args.find((a) => a === flag || a.startsWith(`${flag}=`));
537
+ if (arg === void 0)
538
+ return void 0;
539
+ const value = arg.startsWith(`${flag}=`) ? arg.slice(flag.length + 1) : "";
540
+ return value.split(",").map((s) => s.trim()).filter(Boolean);
541
+ };
542
+ const python = grab("--python-packages");
543
+ const node = grab("--node-packages");
544
+ const c = grab("--c-packages");
545
+ if (!python && !node && !c)
546
+ return void 0;
547
+ const spec = {};
548
+ if (python)
549
+ spec.python = python;
550
+ if (node)
551
+ spec.node = node;
552
+ if (c)
553
+ spec.c = c;
554
+ return spec;
555
+ }
320
556
  async function askYesNo(rl, question, def) {
321
557
  const answer = (await rl.question(`${question} [${def ? "Y/n" : "y/N"}] `)).trim().toLowerCase();
322
558
  if (answer === "")
@@ -336,6 +572,12 @@ function printHelp(kind) {
336
572
  console.log(" --ui | --no-ui include east-ui + e3-ui UI components (default: no)");
337
573
  console.log(" --platform | --no-platform include a project-owned platform module (TS-East; +Python when east-py is on) (default: no)");
338
574
  console.log(" --runners=east-node,east-c,east-py East runtimes to include (default: all)");
575
+ console.log("");
576
+ console.log(" Split platform code into separate workspace packages, each with its own");
577
+ console.log(" execution environment (editing one re-runs only its tasks):");
578
+ console.log(" --python-packages=pricing,common uv workspace members (packages/python/*)");
579
+ console.log(" --node-packages=api npm workspace members (packages/node/*)");
580
+ console.log(" --c-packages=solver native binaries wired via a tools env (packages/native/*)");
339
581
  }
340
582
  console.log("");
341
583
  console.log("Run interactively (a TTY with no feature flags) to be prompted for these.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elaraai/create-e3",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "description": "Scaffold a new e3 project (BSL-1.1, Node + Python, durable execution): npm create @elaraai/e3",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,16 @@
1
+ # Builds the __PACKAGE_NAME__ tool -> build/__PACKAGE_NAME__.
2
+ #
3
+ # e3 captures this binary into the exported bundle via the task's
4
+ # `environment: { tools: { files: [...] } }`, so rebuilding it changes the
5
+ # environment hash and re-runs only the tasks that use it. Build before export:
6
+ # make -C packages/native/__PACKAGE_NAME__
7
+ CC ?= cc
8
+ CFLAGS ?= -O2 -Wall -Wextra
9
+
10
+ build/__PACKAGE_NAME__: src/__PACKAGE_NAME__.c
11
+ mkdir -p build
12
+ $(CC) $(CFLAGS) -o $@ $<
13
+
14
+ .PHONY: clean
15
+ clean:
16
+ rm -rf build
@@ -0,0 +1,22 @@
1
+ import e3 from "@elaraai/e3";
2
+ import { East, ArrayType, FloatType } from "@elaraai/east";
3
+
4
+ // A C tool captured via `environment: { tools }`. Unlike python/node packages,
5
+ // e3 does NOT auto-derive a tools environment — you attach the prebuilt binary
6
+ // explicitly, and rebuilding it changes the env hash so only this task re-runs.
7
+ // Build it first: `make -C packages/native/__PACKAGE_NAME__`.
8
+ //
9
+ // This example passes a value through the tool unchanged (the scaffold binary is
10
+ // a passthrough). Replace src/__PACKAGE_NAME__.c with real native logic — embed
11
+ // east-c to decode / compute / encode the BEAST2 dataset files.
12
+ const __PACKAGE_IDENT___values = e3.input("__PACKAGE_IDENT___values", ArrayType(FloatType), [1.0, 2.0, 3.0]);
13
+
14
+ export const __PACKAGE_IDENT___task = e3.customTask(
15
+ "__PACKAGE_IDENT___tool",
16
+ [__PACKAGE_IDENT___values],
17
+ ArrayType(FloatType),
18
+ (_$, inputs, output) => East.str`__PACKAGE_NAME__ ${inputs.get(0n)} ${output}`,
19
+ {
20
+ environment: { tools: { files: ["packages/native/__PACKAGE_NAME__/build/__PACKAGE_NAME__"] } },
21
+ },
22
+ );
@@ -0,0 +1,36 @@
1
+ /*
2
+ * The __PACKAGE_NAME__ tool for __DISPLAY_NAME__ — a prebuilt native binary
3
+ * captured into the e3 bundle via `environment: { tools: { files: [...] } }`.
4
+ *
5
+ * A custom-runner tool is invoked as `__PACKAGE_NAME__ <input> <output>`: it
6
+ * reads the input dataset file and writes the output dataset file, both
7
+ * BEAST2-encoded East values. This example is a PASSTHROUGH (it copies input to
8
+ * output — valid when the task's input and output types match) so the scaffold
9
+ * stays dependency-free and builds with a plain C compiler.
10
+ *
11
+ * For real native computation, embed the east-c runtime to decode the input,
12
+ * compute, and encode the output BEAST2 value (see the docs on custom C
13
+ * runners). A rebuild changes the captured binary's hash, so e3 re-runs only
14
+ * the tasks wired to this tool.
15
+ */
16
+ #include <stdio.h>
17
+
18
+ int main(int argc, char **argv) {
19
+ if (argc < 3) {
20
+ fprintf(stderr, "usage: %s <input> <output>\n", argv[0]);
21
+ return 2;
22
+ }
23
+ FILE *in = fopen(argv[1], "rb");
24
+ if (!in) { perror("open input"); return 1; }
25
+ FILE *out = fopen(argv[2], "wb");
26
+ if (!out) { perror("open output"); fclose(in); return 1; }
27
+
28
+ unsigned char buf[65536];
29
+ size_t n;
30
+ while ((n = fread(buf, 1, sizeof buf, in)) > 0) {
31
+ if (fwrite(buf, 1, n, out) != n) { perror("write output"); fclose(in); fclose(out); return 1; }
32
+ }
33
+ fclose(in);
34
+ fclose(out);
35
+ return 0;
36
+ }
@@ -0,0 +1,25 @@
1
+ import e3 from "@elaraai/e3";
2
+ import { East, IntegerType, FloatType } from "@elaraai/east";
3
+
4
+ // TS-East mirror of @__PROJECT_NAME__/__PACKAGE_NAME__'s platform function —
5
+ // same dotted "__PACKAGE_NAME__.example" name + signature. Keep this in lockstep
6
+ // with packages/node/__PACKAGE_NAME__/src/platform.ts.
7
+ const example = East.platform("__PACKAGE_NAME__.example", [IntegerType, FloatType], IntegerType);
8
+
9
+ const value = e3.input("__PACKAGE_IDENT___value", IntegerType, 21n);
10
+ const factor = e3.input("__PACKAGE_IDENT___factor", FloatType, 2.0);
11
+
12
+ // No `environment` — e3 derives it from the `{ custom: "@__PROJECT_NAME__/__PACKAGE_NAME__" }`
13
+ // platform reference below, capturing packages/node/__PACKAGE_NAME__'s npm
14
+ // workspace closure into the bundle. Editing that package re-runs only this
15
+ // task; sibling packages stay cached.
16
+ export const __PACKAGE_IDENT___task = e3.task(
17
+ "__PACKAGE_IDENT___example",
18
+ [value, factor],
19
+ East.function([IntegerType, FloatType], IntegerType, ($, v, f) => {
20
+ $.return(example(v, f));
21
+ }),
22
+ {
23
+ runner: { runtime: "east-node", platforms: [{ custom: "@__PROJECT_NAME__/__PACKAGE_NAME__" }] },
24
+ },
25
+ );
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@__PROJECT_NAME__/__PACKAGE_NAME__",
3
+ "version": "1.0.0",
4
+ "description": "The __PACKAGE_NAME__ platform package for __DISPLAY_NAME__",
5
+ "type": "module",
6
+ "private": true,
7
+ "exports": {
8
+ "./platform": "./dist/platform.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc"
16
+ },
17
+ "dependencies": {
18
+ "@elaraai/east": "^__VERSION__",
19
+ "@elaraai/east-node-std": "^__VERSION__"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.7.2"
23
+ }
24
+ }
@@ -0,0 +1,17 @@
1
+ import { East, IntegerType, FloatType } from "@elaraai/east";
2
+
3
+ // The east-node platform function owned by @__PROJECT_NAME__/__PACKAGE_NAME__.
4
+ // Platform functions let East call native TS/Node code East can't express. Its
5
+ // dotted "__PACKAGE_NAME__.example" name + signature mirror the TS-East
6
+ // declaration the app calls (src/packages/__PACKAGE_NAME__.ts) — keep the two
7
+ // in lockstep. Add native dependencies to this package's package.json and use
8
+ // them in the implementation below.
9
+ const example = East.platform("__PACKAGE_NAME__.example", [IntegerType, FloatType], IntegerType);
10
+
11
+ const exampleImpl = example.implement(
12
+ (value: bigint, factor: number): bigint => BigInt(Math.ceil(Number(value) * factor)),
13
+ );
14
+
15
+ // The PlatformFunction[] the east-node runner loads via the package's
16
+ // `@__PROJECT_NAME__/__PACKAGE_NAME__/platform` export.
17
+ export default [exampleImpl];
@@ -0,0 +1,23 @@
1
+ {
2
+ "exclude": [
3
+ "dist"
4
+ ],
5
+ "compilerOptions": {
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "module": "nodenext",
9
+ "target": "esnext",
10
+ "lib": [
11
+ "esnext"
12
+ ],
13
+ "declaration": false,
14
+ "sourceMap": true,
15
+ "strict": true,
16
+ "noUncheckedIndexedAccess": true,
17
+ "exactOptionalPropertyTypes": true,
18
+ "verbatimModuleSyntax": true,
19
+ "isolatedModules": true,
20
+ "moduleDetection": "force",
21
+ "skipLibCheck": true
22
+ }
23
+ }
@@ -0,0 +1,26 @@
1
+ import e3 from "@elaraai/e3";
2
+ import { East, ArrayType, FloatType } from "@elaraai/east";
3
+
4
+ // TS-East mirror of `packages/python/__PACKAGE_NAME__`'s @platform_function —
5
+ // same dotted "__PACKAGE_NAME__.example" name, same signature. Keep this in
6
+ // lockstep with packages/python/__PACKAGE_NAME__/src/__PACKAGE_NAME__/example.py.
7
+ const example = East.platform("__PACKAGE_NAME__.example", [ArrayType(FloatType)], FloatType);
8
+
9
+ const values = e3.input("__PACKAGE_IDENT___values", ArrayType(FloatType), [1.0, 2.0, 3.0]);
10
+
11
+ // No `environment` needed: e3 derives it from the `{ custom: "__PACKAGE_NAME__" }`
12
+ // platform reference below — at export it captures packages/python/__PACKAGE_NAME__'s
13
+ // dependency closure into the bundle. Editing anything under that package re-runs
14
+ // only this task; sibling packages stay cached. (To override — e.g. pin a
15
+ // container image, or add prebuilt `tools` binaries — add an explicit
16
+ // `environment:` here.)
17
+ export const __PACKAGE_IDENT___task = e3.task(
18
+ "__PACKAGE_IDENT___example",
19
+ [values],
20
+ East.function([ArrayType(FloatType)], FloatType, ($, v) => {
21
+ $.return(example(v));
22
+ }),
23
+ {
24
+ runner: { runtime: "east-py", platforms: [{ custom: "__PACKAGE_NAME__" }, "east-py-std"] },
25
+ },
26
+ );
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "__PACKAGE_NAME__"
3
+ version = "0.1.0"
4
+ description = "The __PACKAGE_NAME__ platform package for __DISPLAY_NAME__"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "elaraai-east-py",
8
+ ]
9
+
10
+ # Hatchling gives byte-reproducible sdists, so this package's captured
11
+ # environment hash is stable across exports. (Legacy setuptools embeds file
12
+ # mtimes into the sdist, which would re-invalidate the environment — and re-run
13
+ # every task wired to it — on every export.) Keep hatchling here.
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"
@@ -0,0 +1,18 @@
1
+ """The ``__PACKAGE_NAME__`` platform package — its own execution environment.
2
+
3
+ Everything under ``packages/python/__PACKAGE_NAME__`` is captured as ONE
4
+ environment when a task wired to it is exported. Editing this package changes
5
+ only its environment hash, so only the tasks that declare
6
+ ``environment: { python: { project: "packages/python/__PACKAGE_NAME__" } }``
7
+ re-run — sibling packages are left untouched. That package boundary IS the
8
+ change-detection granularity.
9
+
10
+ ``east-py run -p __PACKAGE_NAME__`` imports this package and reads the top-level
11
+ ``platform`` list below. To add a function: create a module beside this file
12
+ (e.g. ``pricing.py``) ending with ``pricing_impl = platform_functions(__name__)``,
13
+ then import and spread it into ``platform``.
14
+ """
15
+
16
+ from .example import example_impl
17
+
18
+ platform = [*example_impl]
@@ -0,0 +1,27 @@
1
+ """An example Python platform function owned by the ``__PACKAGE_NAME__`` package.
2
+
3
+ Platform functions let East call NATIVE Python (numpy, pandas, scikit-learn, …)
4
+ that East itself can't express. The ``@platform_function`` is bound to East by
5
+ its dotted ``"__PACKAGE_NAME__.<fn>"`` name, which the TypeScript declaration in
6
+ ``src/packages/__PACKAGE_NAME__.ts`` mirrors exactly — keep the two in lockstep.
7
+ Add native dependencies to this package's ``pyproject.toml`` and import them
8
+ inside the body.
9
+ """
10
+
11
+ from east.runtime.platform import platform_function, platform_functions
12
+ from east.types.types import ArrayType, FloatType
13
+
14
+
15
+ @platform_function(
16
+ inputs=[ArrayType(FloatType)],
17
+ output=FloatType,
18
+ name="__PACKAGE_NAME__.example",
19
+ )
20
+ def example(values):
21
+ """Example: the mean of a list of floats. Replace with your own logic."""
22
+ values = list(values)
23
+ return sum(values) / len(values) if values else 0.0
24
+
25
+
26
+ # The platform functions defined in THIS module, in definition order.
27
+ example_impl = platform_functions(__name__)