+9
-1
packages/cli/src/cli.ts
+9
-1
packages/cli/src/cli.ts
···
2
2
import yargs from "yargs";
3
3
import { hideBin } from "yargs/helpers";
4
4
import { compileCommand } from "./commands/compile.js";
5
+
import { initCommand } from "./commands/init.js";
5
6
6
7
async function main() {
7
8
await yargs(hideBin(process.argv))
8
9
.scriptName("typelex")
9
-
.usage("$0 compile <namespace>")
10
+
.command(
11
+
"init",
12
+
"Initialize a new typelex project",
13
+
() => {},
14
+
async () => {
15
+
await initCommand();
16
+
}
17
+
)
10
18
.command(
11
19
"compile <namespace>",
12
20
"Compile TypeSpec files to Lexicon JSON",
+152
packages/cli/src/commands/init.ts
+152
packages/cli/src/commands/init.ts
···
1
+
import { resolve } from "path";
2
+
import { mkdir, writeFile, readFile, access } from "fs/promises";
3
+
import { spawn } from "child_process";
4
+
import { createInterface } from "readline";
5
+
6
+
function createMainTemplate(namespace: string): string {
7
+
return `import "@typelex/emitter";
8
+
import "./externals.tsp";
9
+
10
+
namespace ${namespace}.post {
11
+
@rec("tid")
12
+
model Main {
13
+
@required text: string;
14
+
@required createdAt: datetime;
15
+
}
16
+
}
17
+
`;
18
+
}
19
+
20
+
const EXTERNALS_TSP_TEMPLATE = `import "@typelex/emitter";
21
+
22
+
// Generated by typelex
23
+
// This file is auto-generated. Do not edit manually.
24
+
`;
25
+
26
+
async function promptNamespace(): Promise<string> {
27
+
const rl = createInterface({
28
+
input: process.stdin,
29
+
output: process.stdout,
30
+
});
31
+
32
+
return new Promise((resolve) => {
33
+
rl.question("Namespace (e.g., com.example.*): ", (answer) => {
34
+
rl.close();
35
+
resolve(answer.trim());
36
+
});
37
+
});
38
+
}
39
+
40
+
/**
41
+
* Initialize a new typelex project
42
+
*/
43
+
export async function initCommand(): Promise<void> {
44
+
const cwd = process.cwd();
45
+
const typelexDir = resolve(cwd, "typelex");
46
+
const mainTspPath = resolve(typelexDir, "main.tsp");
47
+
const externalsTspPath = resolve(typelexDir, "externals.tsp");
48
+
49
+
console.log("Initializing typelex project...\n");
50
+
51
+
// Prompt for namespace
52
+
let namespace = await promptNamespace();
53
+
54
+
// Validate namespace format
55
+
while (!namespace.endsWith(".*")) {
56
+
console.error(`Error: namespace must end with .*`);
57
+
console.error(`Got: ${namespace}\n`);
58
+
namespace = await promptNamespace();
59
+
}
60
+
61
+
// Remove the .* suffix for use in template
62
+
const namespacePrefix = namespace.slice(0, -2);
63
+
64
+
// Create typelex directory
65
+
await mkdir(typelexDir, { recursive: true });
66
+
67
+
// Check if main.tsp exists and is non-empty
68
+
let shouldCreateMain = true;
69
+
try {
70
+
await access(mainTspPath);
71
+
const content = await readFile(mainTspPath, "utf-8");
72
+
if (content.trim().length > 0) {
73
+
console.log("✓ typelex/main.tsp already exists, skipping");
74
+
shouldCreateMain = false;
75
+
}
76
+
} catch {
77
+
// File doesn't exist, we'll create it
78
+
}
79
+
80
+
if (shouldCreateMain) {
81
+
await writeFile(mainTspPath, createMainTemplate(namespacePrefix), "utf-8");
82
+
console.log("✓ Created typelex/main.tsp");
83
+
}
84
+
85
+
// Always create/overwrite externals.tsp
86
+
await writeFile(externalsTspPath, EXTERNALS_TSP_TEMPLATE, "utf-8");
87
+
console.log("✓ Created typelex/externals.tsp");
88
+
89
+
// Add build script to package.json
90
+
const packageJsonPath = resolve(cwd, "package.json");
91
+
try {
92
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
93
+
if (!packageJson.scripts) {
94
+
packageJson.scripts = {};
95
+
}
96
+
if (!packageJson.scripts["build:typelex"]) {
97
+
packageJson.scripts["build:typelex"] = `typelex compile ${namespace}`;
98
+
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
99
+
console.log("✓ Added build:typelex script to package.json");
100
+
} else {
101
+
console.log("✓ build:typelex script already exists in package.json");
102
+
}
103
+
} catch (err) {
104
+
console.warn("⚠ Could not update package.json:", (err as Error).message);
105
+
}
106
+
107
+
// Install dependencies
108
+
console.log("\nInstalling dependencies...");
109
+
110
+
// Detect package manager
111
+
let packageManager = "npm";
112
+
try {
113
+
await access(resolve(cwd, "pnpm-lock.yaml"));
114
+
packageManager = "pnpm";
115
+
} catch {
116
+
try {
117
+
await access(resolve(cwd, "yarn.lock"));
118
+
packageManager = "yarn";
119
+
} catch {
120
+
// Default to npm
121
+
}
122
+
}
123
+
124
+
return new Promise((resolvePromise, reject) => {
125
+
const args = packageManager === "npm"
126
+
? ["install", "--save-dev", "@typelex/cli", "@typelex/emitter"]
127
+
: ["add", "-D", "@typelex/cli", "@typelex/emitter"];
128
+
129
+
const install = spawn(packageManager, args, {
130
+
cwd,
131
+
stdio: "inherit",
132
+
});
133
+
134
+
install.on("close", (code) => {
135
+
if (code === 0) {
136
+
console.log("\n✓ Installed @typelex/cli and @typelex/emitter");
137
+
console.log("\nTypelex initialized successfully!");
138
+
console.log("\nNext steps:");
139
+
console.log(" 1. Edit typelex/main.tsp to define your lexicons");
140
+
console.log(" 2. Run: npm run build:typelex");
141
+
resolvePromise();
142
+
} else {
143
+
process.exit(code ?? 1);
144
+
}
145
+
});
146
+
147
+
install.on("error", (err) => {
148
+
console.error("Failed to install dependencies:", err);
149
+
reject(err);
150
+
});
151
+
});
152
+
}
+2
-2
packages/example/package.json
+2
-2
packages/example/package.json
···
4
4
"private": true,
5
5
"type": "module",
6
6
"scripts": {
7
-
"build": "pnpm run build:lexicons && pnpm run build:codegen",
8
-
"build:lexicons": "typelex compile xyz.statusphere.*",
7
+
"build": "pnpm run build:typelex && pnpm run build:codegen",
8
+
"build:typelex": "typelex compile xyz.statusphere.*",
9
9
"build:codegen": "lex gen-server --yes ./src lexicons/xyz/statusphere/*.json"
10
10
},
11
11
"dependencies": {