@intend-it/cli 1.3.0 → 1.3.2

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 (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +365 -172
  3. package/package.json +7 -6
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
  <h1>@intend-it/cli</h1>
3
3
  <p><strong>Command-Line Interface for the Intend Programming Language</strong></p>
4
4
  <p>
5
+ <a href="https://intend.fly.dev">Documentation</a> •
5
6
  <a href="https://www.npmjs.com/package/@intend-it/cli"><img src="https://img.shields.io/npm/v/@intend-it/cli.svg" alt="npm version"></a>
6
7
  <a href="https://www.npmjs.com/package/@intend-it/cli"><img src="https://img.shields.io/npm/dm/@intend-it/cli.svg" alt="npm downloads"></a>
7
8
  <a href="https://github.com/DRFR0ST/intend/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@intend-it/cli.svg" alt="license"></a>
@@ -14,6 +15,8 @@
14
15
 
15
16
  The official CLI for building **Intend** projects. Write your intentions, let AI generate the code.
16
17
 
18
+ **[Read the full documentation 📖](https://intend.fly.dev)**
19
+
17
20
  ### ✨ Features
18
21
 
19
22
  - **🚀 Zero Config** - Works out of the box with sensible defaults
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands/init.ts
4
- import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
4
+ import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2, readdirSync } from "fs";
5
5
  import { join as join2, resolve as resolve2 } from "path";
6
6
 
7
7
  // src/config.ts
@@ -54,16 +54,247 @@ function loadConfig(configPath) {
54
54
  throw new Error(`Failed to parse config file: ${error.message}`);
55
55
  }
56
56
  }
57
- function createConfigFile(directory = process.cwd()) {
57
+ function createConfigFile(directory = process.cwd(), configOverwrites) {
58
58
  const path = join(directory, DEFAULT_CONFIG_NAME);
59
59
  if (existsSync(path)) {
60
60
  throw new Error(`Config file already exists: ${path}`);
61
61
  }
62
- writeFileSync(path, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf-8");
62
+ const config = {
63
+ ...DEFAULT_CONFIG,
64
+ ...configOverwrites,
65
+ gemini: { ...DEFAULT_CONFIG.gemini, ...configOverwrites?.gemini || {} },
66
+ ollama: { ...DEFAULT_CONFIG.ollama, ...configOverwrites?.ollama || {} }
67
+ };
68
+ writeFileSync(path, JSON.stringify(config, null, 2), "utf-8");
63
69
  }
64
70
 
65
- // src/ui.ts
71
+ // src/commands/init.ts
72
+ import * as p from "@clack/prompts";
66
73
  import pc from "picocolors";
74
+ async function initCommand(args) {
75
+ const skipInteractive = args.includes("-y") || args.includes("--yes");
76
+ if (!skipInteractive) {
77
+ p.intro(`${pc.bgCyan(pc.black(" Intend Init "))} ${pc.dim("Set up your project")}`);
78
+ }
79
+ let projectName = args.find((a) => !a.startsWith("-") && a !== "init");
80
+ if (!projectName) {
81
+ if (skipInteractive) {
82
+ projectName = ".";
83
+ } else {
84
+ projectName = await p.text({
85
+ message: "What is your project name?",
86
+ placeholder: "my-intend-app",
87
+ validate: (value) => {
88
+ if (!value)
89
+ return "Please enter a name";
90
+ if (value.match(/[^a-zA-Z0-9-_]/))
91
+ return "Name can only contain letters, numbers, dashes and underscores";
92
+ }
93
+ });
94
+ if (p.isCancel(projectName)) {
95
+ p.cancel("Operation cancelled");
96
+ process.exit(0);
97
+ }
98
+ }
99
+ }
100
+ const directory = resolve2(process.cwd(), projectName);
101
+ if (!skipInteractive && existsSync2(directory) && readdirSync(directory).length > 0) {
102
+ const confirm2 = await p.confirm({
103
+ message: `Directory ${pc.cyan(projectName)} is not empty. Continue anyway?`,
104
+ initialValue: false
105
+ });
106
+ if (p.isCancel(confirm2) || !confirm2) {
107
+ p.cancel("Operation cancelled");
108
+ process.exit(0);
109
+ }
110
+ }
111
+ let provider = "gemini";
112
+ let configOverwrites = {};
113
+ if (!skipInteractive) {
114
+ provider = await p.select({
115
+ message: "Which AI provider would you like to use?",
116
+ options: [
117
+ { value: "gemini", label: "Google Gemini", hint: "Recommended" },
118
+ { value: "ollama", label: "Ollama", hint: "Local execution" }
119
+ ]
120
+ });
121
+ if (p.isCancel(provider)) {
122
+ p.cancel("Operation cancelled");
123
+ process.exit(0);
124
+ }
125
+ configOverwrites.provider = provider;
126
+ if (provider === "gemini") {
127
+ const model = await p.select({
128
+ message: "Select a Gemini model",
129
+ options: [
130
+ { value: "gemini-3-flash-preview", label: "Gemini 3 Flash", hint: "State of the art (Preview)" },
131
+ { value: "gemini-3-pro-preview", label: "Gemini 3 Pro", hint: "Most capable (Preview)" },
132
+ { value: "gemini-2.5-pro", label: "Gemini 2.5 Pro", hint: "Most capable" },
133
+ { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", hint: "Current frontier" },
134
+ { value: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite", hint: "Fast and reliable" },
135
+ { value: "other", label: "Other (specify)" }
136
+ ]
137
+ });
138
+ if (p.isCancel(model)) {
139
+ p.cancel("Operation cancelled");
140
+ process.exit(0);
141
+ }
142
+ let finalModel = model;
143
+ if (model === "other") {
144
+ finalModel = await p.text({
145
+ message: "Enter the model name",
146
+ placeholder: "gemini-2.5-flash-lite"
147
+ });
148
+ if (p.isCancel(finalModel)) {
149
+ p.cancel("Operation cancelled");
150
+ process.exit(0);
151
+ }
152
+ }
153
+ const apiKey = await p.text({
154
+ message: "Enter your Gemini API Key (optional)",
155
+ placeholder: "Leave empty to use GEMINI_API_KEY env var"
156
+ });
157
+ if (p.isCancel(apiKey)) {
158
+ p.cancel("Operation cancelled");
159
+ process.exit(0);
160
+ }
161
+ configOverwrites.gemini = {
162
+ apiKey: apiKey || "${GEMINI_API_KEY}",
163
+ model: finalModel
164
+ };
165
+ } else {
166
+ const baseUrl = await p.text({
167
+ message: "Ollama Base URL",
168
+ initialValue: "http://localhost:11434"
169
+ });
170
+ if (p.isCancel(baseUrl)) {
171
+ p.cancel("Operation cancelled");
172
+ process.exit(0);
173
+ }
174
+ let localModels = [];
175
+ const sFetch = p.spinner();
176
+ sFetch.start("Fetching local Ollama models...");
177
+ try {
178
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/tags`);
179
+ if (response.ok) {
180
+ const data = await response.json();
181
+ if (data.models && Array.isArray(data.models)) {
182
+ localModels = data.models.map((m) => ({
183
+ value: m.name,
184
+ label: m.name
185
+ }));
186
+ }
187
+ }
188
+ sFetch.stop(localModels.length > 0 ? "Fetched local models" : "No local models found");
189
+ } catch (e) {
190
+ sFetch.stop("Could not connect to Ollama (using fallback list)");
191
+ }
192
+ const options = localModels.length > 0 ? [...localModels, { value: "other", label: "Other (specify)" }] : [
193
+ { value: "llama3.2", label: "Llama 3.2" },
194
+ { value: "mistral", label: "Mistral" },
195
+ { value: "gemma3:4b", label: "Gemma 3 4B" },
196
+ { value: "codegemma", label: "CodeGemma" },
197
+ { value: "other", label: "Other (specify)" }
198
+ ];
199
+ const model = await p.select({
200
+ message: "Select an Ollama model",
201
+ options
202
+ });
203
+ if (p.isCancel(model)) {
204
+ p.cancel("Operation cancelled");
205
+ process.exit(0);
206
+ }
207
+ let finalModel = model;
208
+ if (model === "other") {
209
+ finalModel = await p.text({
210
+ message: "Enter the model name",
211
+ placeholder: "deepseek-coder"
212
+ });
213
+ if (p.isCancel(finalModel)) {
214
+ p.cancel("Operation cancelled");
215
+ process.exit(0);
216
+ }
217
+ }
218
+ configOverwrites.ollama = {
219
+ model: finalModel,
220
+ baseUrl
221
+ };
222
+ }
223
+ }
224
+ const s = p.spinner();
225
+ s.start("Initializing project...");
226
+ if (!existsSync2(directory)) {
227
+ mkdirSync(directory, { recursive: true });
228
+ }
229
+ const srcDir = join2(directory, "src");
230
+ const intentsDir = join2(srcDir, "intents");
231
+ if (!existsSync2(intentsDir)) {
232
+ mkdirSync(intentsDir, { recursive: true });
233
+ }
234
+ const outDir = join2(directory, "out");
235
+ if (!existsSync2(outDir)) {
236
+ mkdirSync(outDir, { recursive: true });
237
+ }
238
+ try {
239
+ createConfigFile(directory, configOverwrites);
240
+ } catch (error) {
241
+ s.stop(`${pc.red("Error:")} ${error.message}`);
242
+ process.exit(1);
243
+ }
244
+ const typesPath = join2(srcDir, "types.ts");
245
+ if (!existsSync2(typesPath)) {
246
+ const typesContent = `export interface User {
247
+ name: string;
248
+ id: number;
249
+ createdAt: Date;
250
+ }`;
251
+ writeFileSync2(typesPath, typesContent, "utf-8");
252
+ }
253
+ const examplePath = join2(intentsDir, "example.intent");
254
+ if (!existsSync2(examplePath)) {
255
+ const exampleContent = `import { User } from "../types";
256
+
257
+ export intent CreateUser(name: string) -> User {
258
+ step "Generate a random ID" => const id
259
+ step "Create a User object with name, id, and current date" => const user
260
+ return user
261
+ }
262
+
263
+ export entry intent Main() -> void {
264
+ step "Call CreateUser with name 'Intender'" => const user
265
+ step "Log 'Created user:' and the user object to console"
266
+ }
267
+ `;
268
+ writeFileSync2(examplePath, exampleContent, "utf-8");
269
+ }
270
+ const gitignorePath = join2(directory, ".gitignore");
271
+ if (!existsSync2(gitignorePath)) {
272
+ const gitignoreContent = `node_modules/
273
+ dist/
274
+ out/
275
+ .intend/
276
+ .env
277
+ .DS_Store
278
+ `;
279
+ writeFileSync2(gitignorePath, gitignoreContent, "utf-8");
280
+ }
281
+ s.stop(pc.green("Project initialized successfully!"));
282
+ p.note(`${pc.white("1.")} cd ${pc.cyan(pc.bold(projectName))}
283
+ ` + `${pc.white("2.")} ${provider === "gemini" ? "Set " + pc.cyan(pc.bold("GEMINI_API_KEY")) + " in .env or shell" : "Ensure " + pc.cyan(pc.bold("Ollama")) + " is running"}
284
+ ` + `${pc.white("3.")} RUN ${pc.cyan(pc.bold("intend build"))} to generate code`, "Next Steps");
285
+ p.outro(`Happy coding with ${pc.cyan("Intend")}! ✨`);
286
+ }
287
+
288
+ // src/commands/build.ts
289
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync2, copyFileSync } from "fs";
290
+ import { execSync } from "child_process";
291
+ import { join as join3, resolve as resolve3, dirname, basename } from "path";
292
+ import { AICodeGenerator, FileSystemCAS, computeHash, OllamaProvider } from "@intend-it/core";
293
+ import { parseToAST } from "@intend-it/parser";
294
+ import { readdirSync as readdirSync2, statSync } from "fs";
295
+
296
+ // src/ui.ts
297
+ import pc2 from "picocolors";
67
298
  import ora from "ora";
68
299
  import { createRequire } from "module";
69
300
  var require2 = createRequire(import.meta.url);
@@ -72,36 +303,39 @@ var BRAND = {
72
303
  name: "intend",
73
304
  version: pkg.version
74
305
  };
75
- var heading = (text) => pc.bold(text);
76
- var success = (text) => pc.green(text);
77
- var error = (text) => pc.red(text);
78
- var warn = (text) => pc.yellow(text);
79
- var accent = (text) => pc.cyan(text);
80
- var dim = (text) => pc.dim(text);
81
- var bullet = (text) => ` ${dim("›")} ${text}`;
82
- var path = (p) => dim(p);
83
- var num = (n) => pc.bold(pc.white(String(n)));
84
- var duration = (ms) => dim(`${(ms / 1000).toFixed(2)}s`);
85
- var cmd = (name) => pc.cyan(name);
306
+ var heading = (text2) => pc2.bold(text2);
307
+ var error = (text2) => pc2.red(text2);
308
+ var dim = (text2) => pc2.dim(text2);
309
+ var path = (p2) => dim(p2);
310
+ var duration = (ms) => {
311
+ if (ms < 1000)
312
+ return dim(`${Math.round(ms)}ms`);
313
+ if (ms < 60000)
314
+ return dim(`${(ms / 1000).toFixed(2)}s`);
315
+ const minutes = Math.floor(ms / 60000);
316
+ const seconds = (ms % 60000 / 1000).toFixed(0);
317
+ return dim(`${minutes}m ${seconds}s`);
318
+ };
319
+ var cmd = (name) => pc2.cyan(name);
86
320
  var label = (name, value) => `${dim(name + ":")} ${value}`;
87
321
  var icons = {
88
- success: pc.green("✓"),
89
- error: pc.red("✗"),
90
- warning: pc.yellow("!"),
91
- info: pc.cyan("i"),
92
- cached: pc.blue("⚡"),
322
+ success: pc2.green("✓"),
323
+ error: pc2.red("✗"),
324
+ warning: pc2.yellow("!"),
325
+ info: pc2.cyan("i"),
326
+ cached: pc2.blue("⚡"),
93
327
  arrow: dim("→")
94
328
  };
95
- function spinner(text) {
329
+ function spinner2(text2) {
96
330
  return ora({
97
- text,
331
+ text: text2,
98
332
  color: "cyan",
99
333
  spinner: "dots"
100
334
  });
101
335
  }
102
- function printHeader(text) {
336
+ function printHeader(text2) {
103
337
  console.log();
104
- console.log(heading(text));
338
+ console.log(heading(text2));
105
339
  }
106
340
  function printError(message, detail) {
107
341
  console.log();
@@ -110,12 +344,6 @@ function printError(message, detail) {
110
344
  console.log(` ${dim(detail)}`);
111
345
  }
112
346
  }
113
- function printSuccess(message) {
114
- console.log(`${icons.success} ${success(message)}`);
115
- }
116
- function printWarning(message) {
117
- console.log(`${icons.warning} ${warn(message)}`);
118
- }
119
347
  function newline() {
120
348
  console.log();
121
349
  }
@@ -128,7 +356,7 @@ function printHelp() {
128
356
  console.log(` ${dim("$")} intend ${dim("<command>")} ${dim("[options]")}`);
129
357
  console.log();
130
358
  console.log(heading("Commands"));
131
- console.log(` ${cmd("init")} ${dim("[dir]")} Initialize a new project`);
359
+ console.log(` ${cmd("init")} ${dim("[dir] [--yes]")} Initialize a new project`);
132
360
  console.log(` ${cmd("build")} Build the project`);
133
361
  console.log(` ${cmd("watch")} Watch and rebuild on changes`);
134
362
  console.log(` ${cmd("parse")} ${dim("<file>")} Parse a file to AST/CST`);
@@ -145,96 +373,11 @@ function printHelp() {
145
373
  console.log();
146
374
  }
147
375
 
148
- // src/commands/init.ts
149
- async function initCommand(args) {
150
- const directory = args[1] ? resolve2(process.cwd(), args[1]) : process.cwd();
151
- printHeader("Initialize Project");
152
- console.log(label("Directory", path(directory)));
153
- newline();
154
- const spinner2 = spinner("Creating project structure...").start();
155
- if (!existsSync2(directory)) {
156
- mkdirSync(directory, { recursive: true });
157
- }
158
- const srcDir = join2(directory, "src", "intents");
159
- if (!existsSync2(srcDir)) {
160
- mkdirSync(srcDir, { recursive: true });
161
- }
162
- const outDir = join2(directory, "out");
163
- if (!existsSync2(outDir)) {
164
- mkdirSync(outDir, { recursive: true });
165
- }
166
- spinner2.succeed(dim("Project structure created"));
167
- console.log(bullet(`${dim("src/intents/")} ${dim("(intent files)")}`));
168
- console.log(bullet(`${dim("out/")} ${dim("(generated code)")}`));
169
- const configSpinner = spinner("Creating configuration...").start();
170
- try {
171
- createConfigFile(directory);
172
- configSpinner.succeed(dim("intend.config.json created"));
173
- } catch (error2) {
174
- configSpinner.stopAndPersist({
175
- symbol: icons.warning,
176
- text: warn(error2.message)
177
- });
178
- }
179
- const typesPath = join2(srcDir, "types.ts");
180
- if (!existsSync2(typesPath)) {
181
- const typesContent = `export interface User {
182
- name: string;
183
- id: number;
184
- createdAt: Date;
185
- }`;
186
- writeFileSync2(typesPath, typesContent, "utf-8");
187
- console.log(bullet(`${dim("src/intents/types.ts")} ${dim("(typescript definition)")}`));
188
- }
189
- const examplePath = join2(srcDir, "example.intent");
190
- if (!existsSync2(examplePath)) {
191
- const exampleContent = `import { User } from "./types";
192
-
193
- export intent CreateUser(name: string) -> User {
194
- step "Generate a random ID" => const id
195
- step "Create a User object with name, id, and current date" => const user
196
- return user
197
- }
198
-
199
- export entry intent Main() -> void {
200
- step "Call CreateUser with name 'Intender'" => const user
201
- step "Log 'Created user:' and the user object to console"
202
- }
203
- `;
204
- writeFileSync2(examplePath, exampleContent, "utf-8");
205
- console.log(bullet(`${dim("src/intents/example.intent")} ${dim("(example intent)")}`));
206
- }
207
- const gitignorePath = join2(directory, ".gitignore");
208
- if (!existsSync2(gitignorePath)) {
209
- const gitignoreContent = `node_modules/
210
- dist/
211
- out/
212
- .intend/
213
- .env
214
- .DS_Store
215
- `;
216
- writeFileSync2(gitignorePath, gitignoreContent, "utf-8");
217
- console.log(bullet(`${dim(".gitignore")} ${dim("(git config)")}`));
218
- }
219
- newline();
220
- printSuccess("Project initialized");
221
- newline();
222
- console.log(heading("Next Steps"));
223
- console.log(bullet(`Configure provider in ${accent("intend.config.json")}`));
224
- console.log(bullet(`Set ${accent("GEMINI_API_KEY")} env var ${dim("(if using Gemini)")}`));
225
- console.log(bullet(`Run ${accent("intend build")} to generate code`));
226
- newline();
227
- }
228
-
229
376
  // src/commands/build.ts
230
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync2, copyFileSync } from "fs";
231
- import { join as join3, resolve as resolve3, dirname, basename } from "path";
232
- import { AICodeGenerator, FileSystemCAS, computeHash, OllamaProvider } from "@intend-it/core";
233
- import * as readline from "readline";
234
- import { parseToAST } from "@intend-it/parser";
235
- import { readdirSync, statSync } from "fs";
377
+ import * as p2 from "@clack/prompts";
378
+ import pc3 from "picocolors";
236
379
  function findIntentFiles(dir, fileList = []) {
237
- const files = readdirSync(dir);
380
+ const files = readdirSync2(dir);
238
381
  files.forEach((file) => {
239
382
  const filePath = join3(dir, file);
240
383
  const stat = statSync(filePath);
@@ -255,7 +398,7 @@ function copyResources(src, dest) {
255
398
  return;
256
399
  if (!existsSync3(dest))
257
400
  mkdirSync2(dest, { recursive: true });
258
- const entries = readdirSync(src, { withFileTypes: true });
401
+ const entries = readdirSync2(src, { withFileTypes: true });
259
402
  for (const entry of entries) {
260
403
  const srcPath = join3(src, entry.name);
261
404
  const destPath = join3(dest, entry.name);
@@ -271,6 +414,10 @@ function copyResources(src, dest) {
271
414
  }
272
415
  }
273
416
  async function buildCommand(options) {
417
+ const isWatch = options.watch;
418
+ if (!isWatch) {
419
+ p2.intro(`${pc3.bgCyan(pc3.black(" Intend Build "))} ${pc3.dim("Generating TypeScript")}`);
420
+ }
274
421
  const config = loadConfig(options.config);
275
422
  if (options.output)
276
423
  config.outDir = options.output;
@@ -289,31 +436,56 @@ async function buildCommand(options) {
289
436
  const outDir = resolve3(process.cwd(), config.outDir);
290
437
  const providerName = config.provider || "gemini";
291
438
  if (config.provider === "gemini" && (!config.gemini || !config.gemini.apiKey)) {
292
- printError("Gemini API key required", "Set GEMINI_API_KEY env var or add to intend.config.json");
439
+ if (isWatch) {
440
+ printError("Gemini API key required", "Set GEMINI_API_KEY env var or add to intend.config.json");
441
+ } else {
442
+ p2.cancel(`${pc3.red("Error:")} Gemini API key required. Set GEMINI_API_KEY env var or add to intend.config.json`);
443
+ }
293
444
  process.exit(1);
294
445
  }
295
- printHeader("Build");
296
- console.log(label("Source", path(sourceDir)));
297
- console.log(label("Output", path(outDir)));
298
- console.log(label("Provider", accent(providerName)));
299
- newline();
300
- const scanSpinner = spinner("Scanning for .intent files...").start();
446
+ const modelName = providerName === "ollama" ? config.ollama?.model || "llama3" : config.gemini?.model || "gemini-2.5-flash-lite";
447
+ if (!isWatch) {
448
+ console.log(`${pc3.dim("Source: ")}${pc3.cyan(sourceDir)}`);
449
+ console.log(`${pc3.dim("Output: ")}${pc3.cyan(outDir)}`);
450
+ console.log(`${pc3.dim("Provider: ")}${pc3.cyan(providerName)}`);
451
+ console.log(`${pc3.dim("Model: ")}${pc3.cyan(modelName)}`);
452
+ console.log("");
453
+ }
454
+ const sFiles = p2.spinner();
455
+ sFiles.start("Scanning for .intent files...");
301
456
  let files = [];
302
457
  try {
303
458
  files = findIntentFiles(sourceDir);
304
- scanSpinner.succeed(dim(`Found ${num(files.length)} intent file${files.length !== 1 ? "s" : ""}`));
459
+ sFiles.stop(`Found ${pc3.cyan(files.length)} intent files`);
305
460
  } catch (err) {
306
- scanSpinner.fail(error(`Failed to scan ${sourceDir}`));
461
+ sFiles.stop("Failed to scan directory", 1);
307
462
  process.exit(1);
308
463
  }
309
464
  if (files.length === 0) {
310
- printWarning("No .intent files found");
465
+ p2.log.warn("No .intent files found");
311
466
  return;
312
467
  }
313
468
  const startTime = Date.now();
314
469
  const projectContext = new Map;
315
470
  const fileData = new Map;
316
471
  let parseErrors = 0;
472
+ let entryPointFile = null;
473
+ let entryPointName = null;
474
+ for (const [file, ast] of projectContext.entries()) {
475
+ for (const intent of ast.intents) {
476
+ if (intent.entryPoint) {
477
+ if (entryPointFile) {
478
+ p2.log.error(`Multiple entry points found:
479
+ 1. ${pc3.cyan(entryPointName)} in ${pc3.dim(basename(entryPointFile))}
480
+ 2. ${pc3.cyan(intent.name)} in ${pc3.dim(basename(file))}`);
481
+ p2.cancel("Only one 'entry' intent is allowed per project.");
482
+ process.exit(1);
483
+ }
484
+ entryPointFile = file;
485
+ entryPointName = intent.name;
486
+ }
487
+ }
488
+ }
317
489
  for (const file of files) {
318
490
  try {
319
491
  const content = readFileSync2(file, "utf-8");
@@ -322,8 +494,8 @@ async function buildCommand(options) {
322
494
  const hash = computeHash(content, config);
323
495
  fileData.set(file, { content, hash, ast: astResult.ast });
324
496
  } catch (err) {
325
- console.log(` ${icons.error} ${dim("Parse failed:")} ${basename(file)}`);
326
- console.log(` ${dim(err.message)}`);
497
+ p2.log.error(`Parse failed: ${pc3.dim(basename(file))}
498
+ ${pc3.red(err.message)}`);
327
499
  parseErrors++;
328
500
  }
329
501
  }
@@ -340,20 +512,18 @@ async function buildCommand(options) {
340
512
  projectContext,
341
513
  debug: options.debug
342
514
  });
343
- const connectSpinner = spinner(`Connecting to ${providerName}...`).start();
515
+ const sConnect = p2.spinner();
516
+ sConnect.start(`Connecting to ${providerName}...`);
344
517
  try {
345
518
  const connected = await generator.testConnection();
346
519
  if (!connected) {
347
- if (config.provider === "ollama") {
348
- connectSpinner.fail(error("Failed to connect to Ollama. Is it running?"));
349
- } else {
350
- connectSpinner.fail(error("Failed to connect to Gemini API"));
351
- }
520
+ const msg = config.provider === "ollama" ? "Failed to connect to Ollama. Is it running?" : "Failed to connect to Gemini API";
521
+ sConnect.stop(msg, 1);
352
522
  process.exit(1);
353
523
  }
354
- connectSpinner.succeed(dim(`Connected to ${providerName}`));
524
+ sConnect.stop(`Connected to ${pc3.cyan(providerName)}`);
355
525
  } catch (error2) {
356
- connectSpinner.fail(error(`Connection failed: ${error2.message}`));
526
+ sConnect.stop(`Connection failed: ${error2.message}`, 1);
357
527
  process.exit(1);
358
528
  }
359
529
  const provider = generator.getProvider();
@@ -362,31 +532,29 @@ async function buildCommand(options) {
362
532
  if (typeof ollama.checkModelExists === "function") {
363
533
  const exists = await ollama.checkModelExists();
364
534
  if (!exists) {
365
- const modelName = config.ollama?.model || "llama3";
366
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
367
- console.log();
368
- const answer = await new Promise((resolve4) => {
369
- rl.question(` ${icons.info} Model '${accent(modelName)}' is not found locally.
370
- ${dim("Do you want to pull it now? [Y/n]")} `, resolve4);
535
+ const modelName2 = config.ollama?.model || "llama3";
536
+ const shouldPull = await p2.confirm({
537
+ message: `Model ${pc3.cyan(modelName2)} not found locally. Pull it now?`,
538
+ initialValue: true
371
539
  });
372
- rl.close();
373
- if (answer.toLowerCase() === "n") {
374
- printError("Model missing, cannot proceed.");
540
+ if (p2.isCancel(shouldPull) || !shouldPull) {
541
+ p2.cancel("Operation cancelled. Model missing.");
375
542
  process.exit(1);
376
543
  }
377
- const pullSpinner = spinner(`Pulling ${modelName}...`).start();
544
+ const sPull = p2.spinner();
545
+ sPull.start(`Pulling ${modelName2}...`);
378
546
  try {
379
547
  await ollama.pullModel((status, completed, total) => {
380
548
  if (completed && total) {
381
549
  const percent = Math.round(completed / total * 100);
382
- pullSpinner.text = `Pulling ${modelName}: ${status} ${percent}%`;
550
+ sPull.message(`Pulling ${modelName2}: ${status} ${percent}%`);
383
551
  } else {
384
- pullSpinner.text = `Pulling ${modelName}: ${status}`;
552
+ sPull.message(`Pulling ${modelName2}: ${status}`);
385
553
  }
386
554
  });
387
- pullSpinner.succeed(`${modelName} ready`);
555
+ sPull.stop(`${pc3.green(modelName2)} is ready`);
388
556
  } catch (e) {
389
- pullSpinner.fail(`Failed to pull model: ${e.message}`);
557
+ sPull.stop(`Failed to pull model: ${e.message}`, 1);
390
558
  process.exit(1);
391
559
  }
392
560
  }
@@ -397,12 +565,15 @@ async function buildCommand(options) {
397
565
  let successCount = 0;
398
566
  let failCount = parseErrors;
399
567
  let cachedCount = 0;
568
+ const generatedFiles = [];
400
569
  if (!existsSync3(outDir)) {
401
570
  mkdirSync2(outDir, { recursive: true });
402
571
  }
403
572
  copyResources(sourceDir, outDir);
404
- newline();
405
- console.log(heading("Compiling"));
573
+ if (!isWatch) {
574
+ console.log(pc3.bold(`
575
+ \uD83D\uDE80 Compiling`));
576
+ }
406
577
  for (const file of files) {
407
578
  if (!fileData.has(file))
408
579
  continue;
@@ -412,17 +583,15 @@ async function buildCommand(options) {
412
583
  if (!existsSync3(outDirForFile)) {
413
584
  mkdirSync2(outDirForFile, { recursive: true });
414
585
  }
415
- const fileSpinner = spinner(`${relativePath}`).start();
586
+ const sFile = p2.spinner();
587
+ sFile.start(`${pc3.dim(relativePath)}`);
416
588
  const fileStart = Date.now();
417
589
  try {
418
590
  const { hash, ast } = fileData.get(file);
419
591
  const cached = await cas.get(hash);
420
592
  let finalCode;
421
593
  if (!options.force && cached) {
422
- fileSpinner.stopAndPersist({
423
- symbol: icons.cached,
424
- text: `${dim(relativePath)} ${dim("(cached)")}`
425
- });
594
+ sFile.stop(`${pc3.dim(relativePath)} ${pc3.blue("(cached)")}`);
426
595
  finalCode = cached.code;
427
596
  cachedCount++;
428
597
  } else {
@@ -430,25 +599,49 @@ async function buildCommand(options) {
430
599
  finalCode = generated.code;
431
600
  await cas.put(hash, finalCode);
432
601
  const fileDuration = Date.now() - fileStart;
433
- fileSpinner.succeed(`${relativePath} ${dim(`${fileDuration}ms`)}`);
602
+ sFile.stop(`${relativePath} ${duration(fileDuration)}`);
434
603
  }
435
604
  writeFileSync3(outFile, finalCode, "utf-8");
605
+ generatedFiles.push(outFile);
436
606
  successCount++;
437
607
  } catch (error2) {
438
- fileSpinner.fail(`${relativePath} ${dim(error2.message)}`);
608
+ sFile.stop(`${pc3.red(relativePath)} ${pc3.dim(error2.message)}`, 1);
439
609
  failCount++;
440
610
  }
441
611
  }
442
612
  const totalDuration = Date.now() - startTime;
443
- newline();
444
- console.log(heading("Summary"));
445
- console.log(` ${icons.success} ${num(successCount)} compiled ${cachedCount > 0 ? dim(`(${cachedCount} cached)`) : ""}`);
446
- if (failCount > 0) {
447
- console.log(` ${icons.error} ${num(failCount)} failed`);
613
+ if (!isWatch) {
614
+ p2.note(`${pc3.green(icons.success)} ${pc3.bold(successCount)} compiled ${cachedCount > 0 ? pc3.dim(`(${cachedCount} cached)`) : ""}
615
+ ` + (failCount > 0 ? `${pc3.red(icons.error)} ${pc3.bold(failCount)} failed
616
+ ` : "") + `${pc3.white(icons.arrow)} ${pc3.white(duration(totalDuration))}`, "Summary");
617
+ const entryFiles = files.filter((f) => {
618
+ const ast = fileData.get(f)?.ast;
619
+ return ast?.intents.some((i) => i.entryPoint);
620
+ }).map((f) => {
621
+ const rel = f.substring(sourceDir.length + 1).replace(/\.intent$/, ".ts");
622
+ return join3(config.outDir, rel);
623
+ });
624
+ let runCmd = "npx tsx";
625
+ try {
626
+ execSync("bun --version", { stdio: "ignore" });
627
+ runCmd = "bun run";
628
+ } catch (e) {
629
+ runCmd = "npx tsx";
630
+ }
631
+ if (entryFiles.length > 0) {
632
+ p2.note(entryFiles.map((f) => `${pc3.white("$")} ${pc3.cyan(pc3.bold(`${runCmd} ${f}`))}`).join(`
633
+ `), "Next Steps: Run your code");
634
+ } else if (successCount > 0) {
635
+ const firstFilePath = files.find((f) => fileData.has(f));
636
+ const relPath = firstFilePath ? firstFilePath.substring(sourceDir.length + 1).replace(/\.intent$/, ".ts") : "";
637
+ const outPath = join3(config.outDir, relPath);
638
+ p2.note(`${pc3.white("$")} ${pc3.cyan(pc3.bold(`${runCmd} ${outPath}`))}
639
+
640
+ ` + `${pc3.white("Tip: Add ")}${pc3.yellow("entry")}${pc3.white(" to an intent to make it the main entry point.")}`, "Next Steps: Run your code");
641
+ }
642
+ p2.outro("Build finished! ✨");
448
643
  }
449
- console.log(` ${icons.arrow} ${duration(totalDuration)}`);
450
- newline();
451
- if (failCount > 0) {
644
+ if (failCount > 0 && !isWatch) {
452
645
  process.exit(1);
453
646
  }
454
647
  }
@@ -463,7 +656,7 @@ async function watchCommand(options) {
463
656
  console.log(label("Watching", path(sourceDir)));
464
657
  newline();
465
658
  await buildCommand(options).catch(() => {});
466
- const watchSpinner = spinner("Waiting for changes...").start();
659
+ const watchSpinner = spinner2("Waiting for changes...").start();
467
660
  let isBuilding = false;
468
661
  let debounceTimer = null;
469
662
  watch(sourceDir, { recursive: true }, (eventType, filename) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intend-it/cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "CLI for the Intend programming language",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "README.md"
13
13
  ],
14
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",
15
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --external picocolors --external ora --external @clack/prompts --external @intend-it/parser --external @intend-it/core",
16
16
  "clean": "rm -rf dist",
17
17
  "dev": "bun run src/index.ts"
18
18
  },
@@ -29,10 +29,11 @@
29
29
  ],
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
- "@intend-it/parser": "^1.3.0",
33
- "@intend-it/core": "^4.0.0",
34
- "picocolors": "^1.1.1",
35
- "ora": "^8.1.1"
32
+ "@clack/prompts": "^0.11.0",
33
+ "@intend-it/core": "^4.0.2",
34
+ "@intend-it/parser": "^1.3.2",
35
+ "ora": "^8.1.1",
36
+ "picocolors": "^1.1.1"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@types/bun": "latest",