@intend-it/cli 1.0.1

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 +483 -0
  2. package/package.json +42 -0
package/dist/index.js ADDED
@@ -0,0 +1,483 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/init.ts
4
+ import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
5
+ import { join as join2, resolve as resolve2 } from "path";
6
+
7
+ // src/config.ts
8
+ import { existsSync, readFileSync, writeFileSync } from "fs";
9
+ import { resolve, join } from "path";
10
+ var DEFAULT_CONFIG_NAME = "intend.config.json";
11
+ var DEFAULT_CONFIG = {
12
+ sourceDir: "./src/intents",
13
+ outDir: "./out",
14
+ provider: "gemini",
15
+ gemini: {
16
+ apiKey: "${GEMINI_API_KEY}",
17
+ model: "gemini-flash-latest",
18
+ temperature: 0.2
19
+ },
20
+ ollama: {
21
+ model: "llama3",
22
+ baseUrl: "http://localhost:11434",
23
+ temperature: 0.2
24
+ },
25
+ include: ["**/*.intent"],
26
+ exclude: ["**/*.test.intent"]
27
+ };
28
+ function loadConfig(configPath) {
29
+ const path = configPath ? resolve(process.cwd(), configPath) : resolve(process.cwd(), DEFAULT_CONFIG_NAME);
30
+ if (!existsSync(path)) {
31
+ if (configPath) {
32
+ throw new Error(`Config file not found: ${path}`);
33
+ }
34
+ const config = { ...DEFAULT_CONFIG };
35
+ if (process.env.GEMINI_API_KEY) {
36
+ config.gemini = { ...config.gemini, apiKey: process.env.GEMINI_API_KEY };
37
+ }
38
+ return config;
39
+ }
40
+ try {
41
+ const content = readFileSync(path, "utf-8");
42
+ const userConfig = JSON.parse(content);
43
+ const config = {
44
+ ...DEFAULT_CONFIG,
45
+ ...userConfig,
46
+ gemini: { ...DEFAULT_CONFIG.gemini, ...userConfig.gemini || {} },
47
+ ollama: { ...DEFAULT_CONFIG.ollama, ...userConfig.ollama || {} }
48
+ };
49
+ if (config.gemini && config.gemini.apiKey === "${GEMINI_API_KEY}" && process.env.GEMINI_API_KEY) {
50
+ config.gemini.apiKey = process.env.GEMINI_API_KEY;
51
+ }
52
+ return config;
53
+ } catch (error) {
54
+ throw new Error(`Failed to parse config file: ${error.message}`);
55
+ }
56
+ }
57
+ function createConfigFile(directory = process.cwd()) {
58
+ const path = join(directory, DEFAULT_CONFIG_NAME);
59
+ if (existsSync(path)) {
60
+ throw new Error(`Config file already exists: ${path}`);
61
+ }
62
+ writeFileSync(path, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf-8");
63
+ }
64
+
65
+ // src/ui.ts
66
+ import pc from "picocolors";
67
+ import ora from "ora";
68
+ var BRAND = {
69
+ name: "intend",
70
+ version: "0.0.1"
71
+ };
72
+ var heading = (text) => pc.bold(text);
73
+ var success = (text) => pc.green(text);
74
+ var error = (text) => pc.red(text);
75
+ var warn = (text) => pc.yellow(text);
76
+ var accent = (text) => pc.cyan(text);
77
+ var dim = (text) => pc.dim(text);
78
+ var bullet = (text) => ` ${dim("›")} ${text}`;
79
+ var path = (p) => dim(p);
80
+ var num = (n) => pc.bold(pc.white(String(n)));
81
+ var duration = (ms) => dim(`${(ms / 1000).toFixed(2)}s`);
82
+ var cmd = (name) => pc.cyan(name);
83
+ var label = (name, value) => `${dim(name + ":")} ${value}`;
84
+ var icons = {
85
+ success: pc.green("✓"),
86
+ error: pc.red("✗"),
87
+ warning: pc.yellow("!"),
88
+ info: pc.cyan("i"),
89
+ cached: pc.blue("⚡"),
90
+ arrow: dim("→")
91
+ };
92
+ function spinner(text) {
93
+ return ora({
94
+ text,
95
+ color: "cyan",
96
+ spinner: "dots"
97
+ });
98
+ }
99
+ function printHeader(text) {
100
+ console.log();
101
+ console.log(heading(text));
102
+ }
103
+ function printError(message, detail) {
104
+ console.log();
105
+ console.log(`${icons.error} ${error(message)}`);
106
+ if (detail) {
107
+ console.log(` ${dim(detail)}`);
108
+ }
109
+ }
110
+ function printSuccess(message) {
111
+ console.log(`${icons.success} ${success(message)}`);
112
+ }
113
+ function printWarning(message) {
114
+ console.log(`${icons.warning} ${warn(message)}`);
115
+ }
116
+ function newline() {
117
+ console.log();
118
+ }
119
+ function printHelp() {
120
+ console.log();
121
+ console.log(`${heading("intend")} ${dim(`v${BRAND.version}`)}`);
122
+ console.log(dim("AI-Powered Intent Compiler"));
123
+ console.log();
124
+ console.log(heading("Usage"));
125
+ console.log(` ${dim("$")} intend ${dim("<command>")} ${dim("[options]")}`);
126
+ console.log();
127
+ console.log(heading("Commands"));
128
+ console.log(` ${cmd("init")} ${dim("[dir]")} Initialize a new project`);
129
+ console.log(` ${cmd("build")} Build the project`);
130
+ console.log(` ${cmd("watch")} Watch and rebuild on changes`);
131
+ console.log(` ${cmd("parse")} ${dim("<file>")} Parse a file to AST/CST`);
132
+ console.log();
133
+ console.log(heading("Options"));
134
+ console.log(` ${cmd("--config")} ${dim("<path>")} Path to config file`);
135
+ console.log(` ${cmd("--output")} ${dim("<dir>")} Output directory`);
136
+ console.log(` ${cmd("--provider")} ${dim("<name>")} AI provider ${dim("(gemini | ollama)")}`);
137
+ console.log(` ${cmd("--api-key")} ${dim("<key>")} Gemini API key`);
138
+ console.log(` ${cmd("--force")} Force regeneration (skip cache)`);
139
+ console.log(` ${cmd("--help")} Show this help`);
140
+ console.log();
141
+ }
142
+
143
+ // src/commands/init.ts
144
+ async function initCommand(args) {
145
+ const directory = args[1] ? resolve2(process.cwd(), args[1]) : process.cwd();
146
+ printHeader("Initialize Project");
147
+ console.log(label("Directory", path(directory)));
148
+ newline();
149
+ const spinner2 = spinner("Creating project structure...").start();
150
+ if (!existsSync2(directory)) {
151
+ mkdirSync(directory, { recursive: true });
152
+ }
153
+ const srcDir = join2(directory, "src", "intents");
154
+ if (!existsSync2(srcDir)) {
155
+ mkdirSync(srcDir, { recursive: true });
156
+ }
157
+ const outDir = join2(directory, "out");
158
+ if (!existsSync2(outDir)) {
159
+ mkdirSync(outDir, { recursive: true });
160
+ }
161
+ spinner2.succeed(dim("Project structure created"));
162
+ console.log(bullet(`${dim("src/intents/")} ${dim("(intent files)")}`));
163
+ console.log(bullet(`${dim("out/")} ${dim("(generated code)")}`));
164
+ const configSpinner = spinner("Creating configuration...").start();
165
+ try {
166
+ createConfigFile(directory);
167
+ configSpinner.succeed(dim("intend.config.json created"));
168
+ } catch (error2) {
169
+ configSpinner.stopAndPersist({
170
+ symbol: icons.warning,
171
+ text: warn(error2.message)
172
+ });
173
+ }
174
+ const examplePath = join2(srcDir, "example.intent");
175
+ if (!existsSync2(examplePath)) {
176
+ const exampleContent = `export intent Greeting(name: string) -> string {
177
+ step "Create greeting message" => const message
178
+ ensure message is defined
179
+ }`;
180
+ writeFileSync2(examplePath, exampleContent, "utf-8");
181
+ console.log(bullet(`${dim("src/intents/example.intent")} ${dim("(example)")}`));
182
+ }
183
+ newline();
184
+ printSuccess("Project initialized");
185
+ newline();
186
+ console.log(heading("Next Steps"));
187
+ console.log(bullet(`Configure provider in ${accent("intend.config.json")}`));
188
+ console.log(bullet(`Set ${accent("GEMINI_API_KEY")} env var ${dim("(if using Gemini)")}`));
189
+ console.log(bullet(`Run ${accent("intend build")} to generate code`));
190
+ newline();
191
+ }
192
+
193
+ // src/commands/build.ts
194
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync2 } from "fs";
195
+ import { join as join3, resolve as resolve3, basename } from "path";
196
+ import { AICodeGenerator, FileSystemCAS, computeHash } from "@intend-it/core";
197
+ import { parseToAST } from "@intend-it/parser";
198
+ import { readdirSync, statSync } from "fs";
199
+ function findIntentFiles(dir, fileList = []) {
200
+ const files = readdirSync(dir);
201
+ files.forEach((file) => {
202
+ const filePath = join3(dir, file);
203
+ const stat = statSync(filePath);
204
+ if (stat.isDirectory()) {
205
+ if (file !== "node_modules" && file !== ".git") {
206
+ findIntentFiles(filePath, fileList);
207
+ }
208
+ } else {
209
+ if (file.endsWith(".intent")) {
210
+ fileList.push(filePath);
211
+ }
212
+ }
213
+ });
214
+ return fileList;
215
+ }
216
+ async function buildCommand(options) {
217
+ const config = loadConfig(options.config);
218
+ if (options.output)
219
+ config.outDir = options.output;
220
+ if (options.apiKey && config.gemini)
221
+ config.gemini.apiKey = options.apiKey;
222
+ if (options.provider)
223
+ config.provider = options.provider;
224
+ const sourceDir = resolve3(process.cwd(), config.sourceDir);
225
+ const outDir = resolve3(process.cwd(), config.outDir);
226
+ const providerName = config.provider || "gemini";
227
+ if (config.provider === "gemini" && (!config.gemini || !config.gemini.apiKey)) {
228
+ printError("Gemini API key required", "Set GEMINI_API_KEY env var or add to intend.config.json");
229
+ process.exit(1);
230
+ }
231
+ printHeader("Build");
232
+ console.log(label("Source", path(sourceDir)));
233
+ console.log(label("Output", path(outDir)));
234
+ console.log(label("Provider", accent(providerName)));
235
+ newline();
236
+ const scanSpinner = spinner("Scanning for .intent files...").start();
237
+ let files = [];
238
+ try {
239
+ files = findIntentFiles(sourceDir);
240
+ scanSpinner.succeed(dim(`Found ${num(files.length)} intent file${files.length !== 1 ? "s" : ""}`));
241
+ } catch (err) {
242
+ scanSpinner.fail(error(`Failed to scan ${sourceDir}`));
243
+ process.exit(1);
244
+ }
245
+ if (files.length === 0) {
246
+ printWarning("No .intent files found");
247
+ return;
248
+ }
249
+ const startTime = Date.now();
250
+ const projectContext = new Map;
251
+ const fileData = new Map;
252
+ let parseErrors = 0;
253
+ for (const file of files) {
254
+ try {
255
+ const content = readFileSync2(file, "utf-8");
256
+ const astResult = parseToAST(content);
257
+ projectContext.set(file, astResult.ast);
258
+ const hash = computeHash(content, config);
259
+ fileData.set(file, { content, hash, ast: astResult.ast });
260
+ } catch (err) {
261
+ console.log(` ${icons.error} ${dim("Parse failed:")} ${basename(file)}`);
262
+ console.log(` ${dim(err.message)}`);
263
+ parseErrors++;
264
+ }
265
+ }
266
+ const generator = new AICodeGenerator({
267
+ provider: config.provider,
268
+ geminiConfig: config.gemini ? {
269
+ apiKey: config.gemini.apiKey,
270
+ model: config.gemini.model,
271
+ temperature: config.gemini.temperature
272
+ } : undefined,
273
+ ollamaConfig: config.ollama,
274
+ mode: "full",
275
+ maxAttempts: options.attempts,
276
+ projectContext
277
+ });
278
+ const connectSpinner = spinner(`Connecting to ${providerName}...`).start();
279
+ try {
280
+ const connected = await generator.testConnection();
281
+ if (!connected) {
282
+ if (config.provider === "ollama") {
283
+ connectSpinner.fail(error("Failed to connect to Ollama. Is it running?"));
284
+ } else {
285
+ connectSpinner.fail(error("Failed to connect to Gemini API"));
286
+ }
287
+ process.exit(1);
288
+ }
289
+ connectSpinner.succeed(dim(`Connected to ${providerName}`));
290
+ } catch (error2) {
291
+ connectSpinner.fail(error(`Connection failed: ${error2.message}`));
292
+ process.exit(1);
293
+ }
294
+ const casDir = resolve3(process.cwd(), ".intend", "store");
295
+ const cas = new FileSystemCAS(casDir);
296
+ let successCount = 0;
297
+ let failCount = parseErrors;
298
+ let cachedCount = 0;
299
+ if (!existsSync3(outDir)) {
300
+ mkdirSync2(outDir, { recursive: true });
301
+ }
302
+ newline();
303
+ console.log(heading("Compiling"));
304
+ for (const file of files) {
305
+ if (!fileData.has(file))
306
+ continue;
307
+ const relativePath = file.substring(sourceDir.length + 1);
308
+ const fileName = basename(file, ".intent");
309
+ const outFile = join3(outDir, `${fileName}.ts`);
310
+ const fileSpinner = spinner(`${relativePath}`).start();
311
+ const fileStart = Date.now();
312
+ try {
313
+ const { hash, ast } = fileData.get(file);
314
+ const cached = await cas.get(hash);
315
+ let finalCode;
316
+ if (!options.force && cached) {
317
+ fileSpinner.stopAndPersist({
318
+ symbol: icons.cached,
319
+ text: `${dim(relativePath)} ${dim("(cached)")}`
320
+ });
321
+ finalCode = cached.code;
322
+ cachedCount++;
323
+ } else {
324
+ const generated = await generator.generate(ast, file);
325
+ finalCode = generated.code;
326
+ await cas.put(hash, finalCode);
327
+ const fileDuration = Date.now() - fileStart;
328
+ fileSpinner.succeed(`${relativePath} ${dim(`${fileDuration}ms`)}`);
329
+ }
330
+ writeFileSync3(outFile, finalCode, "utf-8");
331
+ successCount++;
332
+ } catch (error2) {
333
+ fileSpinner.fail(`${relativePath} ${dim(error2.message)}`);
334
+ failCount++;
335
+ }
336
+ }
337
+ const totalDuration = Date.now() - startTime;
338
+ newline();
339
+ console.log(heading("Summary"));
340
+ console.log(` ${icons.success} ${num(successCount)} compiled ${cachedCount > 0 ? dim(`(${cachedCount} cached)`) : ""}`);
341
+ if (failCount > 0) {
342
+ console.log(` ${icons.error} ${num(failCount)} failed`);
343
+ }
344
+ console.log(` ${icons.arrow} ${duration(totalDuration)}`);
345
+ newline();
346
+ if (failCount > 0) {
347
+ process.exit(1);
348
+ }
349
+ }
350
+
351
+ // src/commands/watch.ts
352
+ import { watch } from "fs";
353
+ import { resolve as resolve4 } from "path";
354
+ async function watchCommand(options) {
355
+ printHeader("Watch Mode");
356
+ const config = loadConfig(options.config);
357
+ const sourceDir = resolve4(process.cwd(), config.sourceDir);
358
+ console.log(label("Watching", path(sourceDir)));
359
+ newline();
360
+ await buildCommand(options).catch(() => {});
361
+ const watchSpinner = spinner("Waiting for changes...").start();
362
+ let isBuilding = false;
363
+ let debounceTimer = null;
364
+ watch(sourceDir, { recursive: true }, (eventType, filename) => {
365
+ if (!filename || !filename.endsWith(".intent"))
366
+ return;
367
+ if (debounceTimer)
368
+ clearTimeout(debounceTimer);
369
+ debounceTimer = setTimeout(async () => {
370
+ if (isBuilding)
371
+ return;
372
+ isBuilding = true;
373
+ watchSpinner.stop();
374
+ console.log();
375
+ console.log(`${icons.info} ${dim("Change detected:")} ${filename}`);
376
+ try {
377
+ await buildCommand(options);
378
+ } catch (error2) {} finally {
379
+ isBuilding = false;
380
+ watchSpinner.start("Waiting for changes...");
381
+ }
382
+ }, 500);
383
+ });
384
+ }
385
+
386
+ // src/index.ts
387
+ import { parse, parseToAST as parseToAST2 } from "@intend-it/parser";
388
+ import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
389
+ import { resolve as resolve5 } from "path";
390
+ var args = process.argv.slice(2);
391
+ var cmd2 = args[0];
392
+ if (!cmd2 || cmd2 === "--help" || cmd2 === "-h") {
393
+ printHelp();
394
+ process.exit(0);
395
+ }
396
+ switch (cmd2) {
397
+ case "init":
398
+ initCommand(args).catch(handleError);
399
+ break;
400
+ case "build":
401
+ handleBuild(args).catch(handleError);
402
+ break;
403
+ case "watch":
404
+ handleWatch(args).catch(handleError);
405
+ break;
406
+ case "parse":
407
+ handleParse(args).catch(handleError);
408
+ break;
409
+ default:
410
+ printError(`Unknown command: ${cmd2}`);
411
+ printHelp();
412
+ process.exit(1);
413
+ }
414
+ async function handleBuild(args2) {
415
+ const options = {};
416
+ for (let i = 1;i < args2.length; i++) {
417
+ if (args2[i] === "--output")
418
+ options.output = args2[i + 1];
419
+ if (args2[i] === "--config")
420
+ options.config = args2[i + 1];
421
+ if (args2[i] === "--api-key")
422
+ options.apiKey = args2[i + 1];
423
+ if (args2[i] === "--provider")
424
+ options.provider = args2[i + 1];
425
+ if (args2[i] === "--watch")
426
+ options.watch = true;
427
+ if (args2[i] === "--force")
428
+ options.force = true;
429
+ if (args2[i].startsWith("--attempts="))
430
+ options.attempts = parseInt(args2[i].split("=")[1]);
431
+ }
432
+ if (options.watch) {
433
+ await watchCommand(options);
434
+ } else {
435
+ await buildCommand(options);
436
+ }
437
+ }
438
+ async function handleWatch(args2) {
439
+ const options = { watch: true };
440
+ for (let i = 1;i < args2.length; i++) {
441
+ if (args2[i] === "--output")
442
+ options.output = args2[i + 1];
443
+ if (args2[i] === "--config")
444
+ options.config = args2[i + 1];
445
+ if (args2[i] === "--api-key")
446
+ options.apiKey = args2[i + 1];
447
+ if (args2[i] === "--provider")
448
+ options.provider = args2[i + 1];
449
+ }
450
+ await watchCommand(options);
451
+ }
452
+ async function handleParse(args2) {
453
+ if (args2.length < 2) {
454
+ printError("Missing file argument", "Usage: intend parse <file.intent> [--cst]");
455
+ process.exit(1);
456
+ }
457
+ const filePath = resolve5(process.cwd(), args2[1]);
458
+ const showCst = args2.includes("--cst");
459
+ if (!existsSync4(filePath)) {
460
+ printError(`File not found: ${filePath}`);
461
+ process.exit(1);
462
+ }
463
+ const content = readFileSync3(filePath, "utf-8");
464
+ if (showCst) {
465
+ const result = parse(content);
466
+ if (result.lexingErrors.length > 0) {
467
+ printError("Lexing errors");
468
+ console.log(dim(JSON.stringify(result.lexingErrors, null, 2)));
469
+ } else if (result.parsingErrors.length > 0) {
470
+ printError("Parsing errors");
471
+ console.log(dim(JSON.stringify(result.parsingErrors, null, 2)));
472
+ } else {
473
+ console.log(JSON.stringify(result.cst, null, 2));
474
+ }
475
+ } else {
476
+ const result = parseToAST2(content);
477
+ console.log(JSON.stringify(result.ast, null, 2));
478
+ }
479
+ }
480
+ function handleError(err) {
481
+ printError(err.message || String(err));
482
+ process.exit(1);
483
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@intend-it/cli",
3
+ "version": "1.0.1",
4
+ "description": "CLI for the Intend programming language",
5
+ "type": "module",
6
+ "bin": {
7
+ "intend": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --external picocolors --external ora --external @intend-it/parser --external @intend-it/core",
16
+ "clean": "rm -rf dist",
17
+ "dev": "bun run src/index.ts"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/DRFR0ST/intend.git",
22
+ "directory": "packages/cli"
23
+ },
24
+ "keywords": [
25
+ "intend",
26
+ "cli",
27
+ "ai",
28
+ "compiler"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@intend-it/parser": "workspace:*",
33
+ "@intend-it/core": "workspace:*",
34
+ "picocolors": "^1.1.1",
35
+ "ora": "^8.1.1"
36
+ },
37
+ "devDependencies": {
38
+ "@types/bun": "latest",
39
+ "@types/node": "^20.0.0",
40
+ "typescript": "^5.0.0"
41
+ }
42
+ }