@elaraai/create-east 1.0.3 → 1.0.5

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.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copyright (c) 2025 Elara AI Pty Ltd
4
+ * Licensed under AGPL-3.0-or-later. See LICENSE for details.
5
+ *
6
+ * Static bin shim — exists in fresh checkout so pnpm install can create
7
+ * the bin symlink BEFORE the package is built. Dynamic-imports the
8
+ * built CLI entrypoint; argv flows through via process.argv.
9
+ */
10
+ await import('../dist/index.js');
package/dist/index.js CHANGED
@@ -39,15 +39,38 @@ var DOTFILE_RENAMES = {
39
39
  gitignore: ".gitignore",
40
40
  npmrc: ".npmrc"
41
41
  };
42
+ var MANIFEST_FILE = "template.json";
42
43
  function substituteTokens(content, names) {
43
44
  return content.replaceAll("__PROJECT_NAME__", names.projectName).replaceAll("__DISPLAY_NAME__", names.displayName).replaceAll("__WORKSPACE_NAME__", names.workspaceName);
44
45
  }
45
- function transformPackageJson(raw, names, version) {
46
+ function transformPackageJson(raw, names, version, manifest, enabled) {
46
47
  const pkg = JSON.parse(raw);
47
48
  pkg.name = `@elaraai/${names.projectName}`;
48
49
  pkg.description = names.displayName;
49
50
  pkg.version = "0.0.1";
50
51
  delete pkg.private;
52
+ if (manifest) {
53
+ const deps = pkg.dependencies;
54
+ const devDeps = pkg.devDependencies;
55
+ const scripts = pkg.scripts;
56
+ for (const [feature, spec] of Object.entries(manifest.features)) {
57
+ if (enabled(feature))
58
+ continue;
59
+ for (const d of spec.dependencies ?? [])
60
+ delete deps?.[d];
61
+ for (const d of spec.devDependencies ?? [])
62
+ delete devDeps?.[d];
63
+ for (const s of spec.scripts ?? [])
64
+ delete scripts?.[s];
65
+ }
66
+ for (const [name, variants] of Object.entries(manifest.scriptVariants ?? {})) {
67
+ const match = variants.find((v) => v.when.every(enabled));
68
+ if (match)
69
+ scripts[name] = match.value;
70
+ else
71
+ delete scripts?.[name];
72
+ }
73
+ }
51
74
  const pin = `^${version}`;
52
75
  for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
53
76
  const deps = pkg[field];
@@ -77,6 +100,12 @@ function extOf(path) {
77
100
  const dot = base.lastIndexOf(".");
78
101
  return dot <= 0 ? "" : base.slice(dot);
79
102
  }
103
+ function loadManifest(templateDir) {
104
+ const path = join(templateDir, MANIFEST_FILE);
105
+ if (!existsSync(path))
106
+ return null;
107
+ return JSON.parse(readFileSync(path, "utf8"));
108
+ }
80
109
  function scaffold(options) {
81
110
  const { kind, name, templateDir, version } = options;
82
111
  const cwd = options.cwd ?? process.cwd();
@@ -84,6 +113,23 @@ function scaffold(options) {
84
113
  if (!existsSync(templateDir)) {
85
114
  throw new Error(`Template directory not found: ${templateDir}`);
86
115
  }
116
+ const manifest = loadManifest(templateDir);
117
+ const enabled = (feature) => options.features?.[feature] ?? manifest?.features[feature]?.default ?? true;
118
+ const skip = /* @__PURE__ */ new Set();
119
+ const renames = {};
120
+ if (manifest) {
121
+ for (const [feature, spec] of Object.entries(manifest.features)) {
122
+ if (enabled(feature)) {
123
+ for (const f of spec.disable ?? [])
124
+ skip.add(f);
125
+ for (const [src, dest] of Object.entries(spec.rename ?? {}))
126
+ renames[src] = dest;
127
+ } else {
128
+ for (const f of spec.files ?? [])
129
+ skip.add(f);
130
+ }
131
+ }
132
+ }
87
133
  const names = deriveNames(name, cwd);
88
134
  const inPlace = name === ".";
89
135
  const projectDir = inPlace ? cwd : join(cwd, names.projectName);
@@ -92,17 +138,21 @@ function scaffold(options) {
92
138
  }
93
139
  mkdirSync(projectDir, { recursive: true });
94
140
  for (const srcPath of walk(templateDir)) {
95
- const rel = relative(templateDir, srcPath);
96
- const segments = rel.split(/[/\\]/);
141
+ const rel = relative(templateDir, srcPath).replaceAll("\\", "/");
142
+ if (rel === MANIFEST_FILE)
143
+ continue;
144
+ if (skip.has(rel))
145
+ continue;
146
+ const destRel = renames[rel] ?? rel;
147
+ const segments = destRel.split("/");
97
148
  const last = segments[segments.length - 1];
98
149
  segments[segments.length - 1] = DOTFILE_RENAMES[last] ?? last;
99
- const destRel = segments.join("/");
100
- const destPath = join(projectDir, destRel);
150
+ const destPath = join(projectDir, segments.join("/"));
101
151
  mkdirSync(join(destPath, ".."), { recursive: true });
102
152
  const baseName = segments[segments.length - 1];
103
153
  if (baseName === "package.json") {
104
- const manifest = transformPackageJson(readFileSync(srcPath, "utf8"), names, version);
105
- writeFileSync(destPath, substituteTokens(manifest, names));
154
+ const transformed = transformPackageJson(readFileSync(srcPath, "utf8"), names, version, manifest, enabled);
155
+ writeFileSync(destPath, substituteTokens(transformed, names));
106
156
  } else if (TEXT_REPLACE_EXT.has(extOf(srcPath)) || baseName.startsWith(".")) {
107
157
  writeFileSync(destPath, substituteTokens(readFileSync(srcPath, "utf8"), names));
108
158
  } else {
@@ -111,7 +161,7 @@ function scaffold(options) {
111
161
  }
112
162
  log(`Created ${names.projectName} (${kind}) at ${projectDir}`);
113
163
  if (options.install) {
114
- runInstall(kind, projectDir, log);
164
+ runInstall(kind, projectDir, enabled, log);
115
165
  }
116
166
  return { ...names, projectDir, inPlace };
117
167
  }
@@ -119,14 +169,14 @@ function hasCommand(cmd) {
119
169
  const probe = process.platform === "win32" ? "where" : "which";
120
170
  return spawnSync(probe, [cmd], { stdio: "ignore" }).status === 0;
121
171
  }
122
- function runInstall(kind, projectDir, log) {
172
+ function runInstall(kind, projectDir, enabled, log) {
123
173
  log("Installing Node dependencies (npm install)...");
124
174
  const npm = spawnSync("npm", ["install"], { cwd: projectDir, stdio: "inherit", shell: process.platform === "win32" });
125
175
  if (npm.status !== 0) {
126
176
  log("npm install failed \u2014 fix the issue and re-run `npm install`.");
127
177
  return;
128
178
  }
129
- if (kind === "e3") {
179
+ if (kind === "e3" && enabled("runner:east-py")) {
130
180
  if (hasCommand("uv")) {
131
181
  log("Installing Python dependencies (uv sync)...");
132
182
  const uv = spawnSync("uv", ["sync"], { cwd: projectDir, stdio: "inherit", shell: process.platform === "win32" });
@@ -139,15 +189,15 @@ function runInstall(kind, projectDir, log) {
139
189
  }
140
190
 
141
191
  // ../scaffold-core/dist/cli.js
142
- import { readFileSync as readFileSync2 } from "node:fs";
192
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
143
193
  import { dirname, join as join2 } from "node:path";
194
+ import { createInterface } from "node:readline/promises";
144
195
  import { fileURLToPath } from "node:url";
145
- function runCreateCli(kind, moduleUrl) {
196
+ async function runCreateCli(kind, moduleUrl) {
146
197
  const pkgRoot = join2(dirname(fileURLToPath(moduleUrl)), "..");
147
198
  const args = process.argv.slice(2);
148
199
  if (args.includes("--help") || args.includes("-h")) {
149
- console.log(`Usage: npm create @elaraai/${kind} <project-name> [-- --install|--no-install]`);
150
- console.log(` create-${kind} <project-name|.> [--install|--no-install]`);
200
+ printHelp(kind);
151
201
  return;
152
202
  }
153
203
  const version = JSON.parse(readFileSync2(join2(pkgRoot, "package.json"), "utf8")).version;
@@ -155,22 +205,111 @@ function runCreateCli(kind, moduleUrl) {
155
205
  const name = args.find((a) => !a.startsWith("-")) ?? ".";
156
206
  const install = args.includes("--install") ? true : args.includes("--no-install") ? false : Boolean(process.stdout.isTTY);
157
207
  try {
158
- const result = scaffold({ kind, name, templateDir, version, install });
159
- printNextSteps(kind, result.projectName, result.inPlace, install);
208
+ const features = await resolveFeatures(templateDir, args);
209
+ const result = scaffold({ kind, name, templateDir, version, install, features });
210
+ printNextSteps(kind, result.projectName, result.inPlace, install, features);
160
211
  } catch (err) {
161
212
  console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
162
213
  process.exit(1);
163
214
  }
164
215
  }
165
- function printNextSteps(kind, projectName, inPlace, installed) {
216
+ async function resolveFeatures(templateDir, args) {
217
+ const manifestPath = join2(templateDir, "template.json");
218
+ if (!existsSync2(manifestPath))
219
+ return {};
220
+ const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
221
+ const features = {};
222
+ for (const [key, spec] of Object.entries(manifest.features))
223
+ features[key] = spec.default ?? true;
224
+ const runnerKeys = Object.keys(manifest.features).filter((k) => k.startsWith("runner:"));
225
+ const runnerName = (key) => key.slice("runner:".length);
226
+ const runnersFlag = args.find((a) => a.startsWith("--runners="));
227
+ const selectionFlags = ["--tests", "--no-tests", "--ui", "--no-ui", "--eslint", "--no-eslint"].some((f) => args.includes(f)) || Boolean(runnersFlag);
228
+ if (args.includes("--tests"))
229
+ features["tests"] = true;
230
+ if (args.includes("--no-tests"))
231
+ features["tests"] = false;
232
+ if (args.includes("--ui"))
233
+ features["ui"] = true;
234
+ if (args.includes("--no-ui"))
235
+ features["ui"] = false;
236
+ if (args.includes("--eslint"))
237
+ features["eslint"] = true;
238
+ if (args.includes("--no-eslint"))
239
+ features["eslint"] = false;
240
+ if (args.includes("--editor-diagnostics"))
241
+ features["editor-diagnostics"] = true;
242
+ if (args.includes("--no-editor-diagnostics"))
243
+ features["editor-diagnostics"] = false;
244
+ if (runnersFlag) {
245
+ const chosen = new Set(runnersFlag.slice("--runners=".length).split(",").map((s) => s.trim()).filter(Boolean));
246
+ for (const key of runnerKeys)
247
+ features[key] = chosen.has(runnerName(key));
248
+ }
249
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !selectionFlags;
250
+ if (!interactive)
251
+ return features;
252
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
253
+ try {
254
+ if ("tests" in manifest.features) {
255
+ features["tests"] = await askYesNo(rl, "Include tests?", features["tests"]);
256
+ }
257
+ if ("ui" in manifest.features) {
258
+ features["ui"] = await askYesNo(rl, "Include UI components (east-ui + e3-ui)?", features["ui"]);
259
+ }
260
+ if (runnerKeys.length > 0) {
261
+ const defaults = runnerKeys.filter((k) => features[k]).map(runnerName);
262
+ const answer = (await rl.question(`Runners to include (comma-separated) [${defaults.join(",")}]: `)).trim();
263
+ const picks = new Set(answer ? answer.split(",").map((s) => s.trim()).filter(Boolean) : defaults);
264
+ for (const key of runnerKeys)
265
+ features[key] = picks.has(runnerName(key));
266
+ }
267
+ if ("eslint" in manifest.features) {
268
+ features["eslint"] = await askYesNo(rl, "Include ESLint with the East lint rules?", features["eslint"]);
269
+ }
270
+ if ("editor-diagnostics" in manifest.features) {
271
+ features["editor-diagnostics"] = await askYesNo(rl, "Include East editor diagnostics (TypeScript language service plugin)?", features["editor-diagnostics"]);
272
+ }
273
+ } finally {
274
+ rl.close();
275
+ }
276
+ return features;
277
+ }
278
+ async function askYesNo(rl, question, def) {
279
+ const answer = (await rl.question(`${question} [${def ? "Y/n" : "y/N"}] `)).trim().toLowerCase();
280
+ if (answer === "")
281
+ return def;
282
+ return answer.startsWith("y");
283
+ }
284
+ function printHelp(kind) {
285
+ console.log(`Usage: npm create @elaraai/${kind} <project-name> [-- <options>]`);
286
+ console.log(` create-${kind} <project-name|.> [options]`);
287
+ console.log("");
288
+ console.log("Options:");
289
+ console.log(" --install | --no-install install dependencies after scaffolding (default: TTY)");
290
+ console.log(" --eslint | --no-eslint include ESLint with the East lint rules (default: yes)");
291
+ console.log(" --editor-diagnostics | --no-editor-diagnostics include the East tsserver plugin for editor squiggles (default: yes)");
292
+ if (kind === "e3") {
293
+ console.log(" --tests | --no-tests include test files (default: yes)");
294
+ console.log(" --ui | --no-ui include east-ui + e3-ui UI components (default: no)");
295
+ console.log(" --runners=east-node,east-c,east-py East runtimes to include (default: all)");
296
+ }
297
+ console.log("");
298
+ console.log("Run interactively (a TTY with no feature flags) to be prompted for these.");
299
+ }
300
+ function printNextSteps(kind, projectName, inPlace, installed, features) {
166
301
  console.log("");
167
302
  console.log("Next steps:");
168
303
  if (!inPlace)
169
304
  console.log(` cd ${projectName}`);
170
- if (!installed)
171
- console.log(kind === "e3" ? " npm run setup" : " npm install");
305
+ if (!installed) {
306
+ if (kind === "e3")
307
+ console.log(features["runner:east-py"] === false ? " npm install" : " npm run setup");
308
+ else
309
+ console.log(" npm install");
310
+ }
172
311
  console.log(kind === "e3" ? " npm run start" : " npm run test");
173
312
  }
174
313
 
175
314
  // src/index.ts
176
- runCreateCli("east", import.meta.url);
315
+ await runCreateCli("east", import.meta.url);
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@elaraai/create-east",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Scaffold a new East project (AGPL-3.0, Node-only): npm create @elaraai/east",
5
5
  "type": "module",
6
6
  "bin": {
7
- "create-east": "./dist/index.js"
7
+ "create-east": "./bin/create-east.mjs"
8
8
  },
9
9
  "files": [
10
+ "bin",
10
11
  "dist",
11
12
  "templates"
12
13
  ],
@@ -1,5 +1,6 @@
1
1
  import tseslint from "@typescript-eslint/eslint-plugin";
2
2
  import tsparser from "@typescript-eslint/parser";
3
+ import east from "@elaraai/eslint-plugin-east";
3
4
 
4
5
  export default [
5
6
  { ignores: ["dist/", "node_modules/"] },
@@ -7,9 +8,16 @@ export default [
7
8
  files: ["src/**/*.ts"],
8
9
  languageOptions: {
9
10
  parser: tsparser,
10
- parserOptions: { ecmaVersion: "latest", sourceType: "module" },
11
+ // Type-aware linting: the East rules read the TypeScript program, so the
12
+ // parser must build it (`projectService` discovers the nearest tsconfig).
13
+ parserOptions: {
14
+ projectService: true,
15
+ tsconfigRootDir: import.meta.dirname,
16
+ },
11
17
  },
12
- plugins: { "@typescript-eslint": tseslint },
13
- rules: {},
18
+ plugins: { "@typescript-eslint": tseslint, east },
19
+ // One rule runs the whole East idiom diagnostic set (prefer some()/none, no
20
+ // hand-rolled variants, no $.let in an expression, etc.).
21
+ rules: { "east/east-rules": "warn" },
14
22
  },
15
23
  ];
@@ -17,11 +17,14 @@
17
17
  "@elaraai/east-node-io": "workspace:*"
18
18
  },
19
19
  "devDependencies": {
20
+ "@elaraai/east-node-cli": "workspace:*",
20
21
  "@types/node": "^22",
21
22
  "typescript": "^5",
22
23
  "eslint": "^9",
23
24
  "@typescript-eslint/eslint-plugin": "^8",
24
25
  "@typescript-eslint/parser": "^8",
26
+ "@elaraai/eslint-plugin-east": "workspace:*",
27
+ "@elaraai/tsserver-plugin-east": "workspace:*",
25
28
  "rimraf": "^6"
26
29
  },
27
30
  "engines": { "node": ">=22" }
@@ -0,0 +1,19 @@
1
+ {
2
+ "features": {
3
+ "eslint": {
4
+ "default": true,
5
+ "files": ["eslint.config.js"],
6
+ "scripts": ["lint"],
7
+ "devDependencies": [
8
+ "eslint",
9
+ "@typescript-eslint/eslint-plugin",
10
+ "@typescript-eslint/parser",
11
+ "@elaraai/eslint-plugin-east"
12
+ ]
13
+ },
14
+ "editor-diagnostics": {
15
+ "default": true,
16
+ "devDependencies": ["@elaraai/tsserver-plugin-east"]
17
+ }
18
+ }
19
+ }
@@ -19,6 +19,7 @@
19
19
  "moduleDetection": "force",
20
20
  "skipLibCheck": true,
21
21
  "noErrorTruncation": true,
22
- "incremental": true
22
+ "incremental": true,
23
+ "plugins": [{ "name": "@elaraai/tsserver-plugin-east" }]
23
24
  }
24
25
  }