@elaraai/create-east 1.0.29 → 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.
Files changed (2) hide show
  1. package/dist/index.js +273 -31
  2. package/package.json +1 -1
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-east",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "description": "Scaffold a new East project (AGPL-3.0, Node-only): npm create @elaraai/east",
5
5
  "type": "module",
6
6
  "bin": {