@elaraai/create-east 1.0.4 → 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.
- package/bin/create-east.mjs +10 -0
- package/dist/index.js +159 -20
- package/package.json +3 -2
- package/templates/east/eslint.config.js +11 -3
- package/templates/east/package.json +3 -0
- package/templates/east/template.json +19 -0
- package/templates/east/tsconfig.json +2 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
|
105
|
-
writeFileSync(destPath, substituteTokens(
|
|
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
|
-
|
|
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
|
|
159
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
"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": "./
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|