@intend-it/cli 1.3.0 → 1.3.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 (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +339 -171
  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,31 @@ 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)));
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);
84
310
  var duration = (ms) => dim(`${(ms / 1000).toFixed(2)}s`);
85
- var cmd = (name) => pc.cyan(name);
311
+ var cmd = (name) => pc2.cyan(name);
86
312
  var label = (name, value) => `${dim(name + ":")} ${value}`;
87
313
  var icons = {
88
- success: pc.green("✓"),
89
- error: pc.red("✗"),
90
- warning: pc.yellow("!"),
91
- info: pc.cyan("i"),
92
- cached: pc.blue("⚡"),
314
+ success: pc2.green("✓"),
315
+ error: pc2.red("✗"),
316
+ warning: pc2.yellow("!"),
317
+ info: pc2.cyan("i"),
318
+ cached: pc2.blue("⚡"),
93
319
  arrow: dim("→")
94
320
  };
95
- function spinner(text) {
321
+ function spinner2(text2) {
96
322
  return ora({
97
- text,
323
+ text: text2,
98
324
  color: "cyan",
99
325
  spinner: "dots"
100
326
  });
101
327
  }
102
- function printHeader(text) {
328
+ function printHeader(text2) {
103
329
  console.log();
104
- console.log(heading(text));
330
+ console.log(heading(text2));
105
331
  }
106
332
  function printError(message, detail) {
107
333
  console.log();
@@ -110,12 +336,6 @@ function printError(message, detail) {
110
336
  console.log(` ${dim(detail)}`);
111
337
  }
112
338
  }
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
339
  function newline() {
120
340
  console.log();
121
341
  }
@@ -128,7 +348,7 @@ function printHelp() {
128
348
  console.log(` ${dim("$")} intend ${dim("<command>")} ${dim("[options]")}`);
129
349
  console.log();
130
350
  console.log(heading("Commands"));
131
- console.log(` ${cmd("init")} ${dim("[dir]")} Initialize a new project`);
351
+ console.log(` ${cmd("init")} ${dim("[dir] [--yes]")} Initialize a new project`);
132
352
  console.log(` ${cmd("build")} Build the project`);
133
353
  console.log(` ${cmd("watch")} Watch and rebuild on changes`);
134
354
  console.log(` ${cmd("parse")} ${dim("<file>")} Parse a file to AST/CST`);
@@ -145,96 +365,11 @@ function printHelp() {
145
365
  console.log();
146
366
  }
147
367
 
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
368
  // 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";
369
+ import * as p2 from "@clack/prompts";
370
+ import pc3 from "picocolors";
236
371
  function findIntentFiles(dir, fileList = []) {
237
- const files = readdirSync(dir);
372
+ const files = readdirSync2(dir);
238
373
  files.forEach((file) => {
239
374
  const filePath = join3(dir, file);
240
375
  const stat = statSync(filePath);
@@ -255,7 +390,7 @@ function copyResources(src, dest) {
255
390
  return;
256
391
  if (!existsSync3(dest))
257
392
  mkdirSync2(dest, { recursive: true });
258
- const entries = readdirSync(src, { withFileTypes: true });
393
+ const entries = readdirSync2(src, { withFileTypes: true });
259
394
  for (const entry of entries) {
260
395
  const srcPath = join3(src, entry.name);
261
396
  const destPath = join3(dest, entry.name);
@@ -271,6 +406,10 @@ function copyResources(src, dest) {
271
406
  }
272
407
  }
273
408
  async function buildCommand(options) {
409
+ const isWatch = options.watch;
410
+ if (!isWatch) {
411
+ p2.intro(`${pc3.bgCyan(pc3.black(" Intend Build "))} ${pc3.dim("Generating TypeScript")}`);
412
+ }
274
413
  const config = loadConfig(options.config);
275
414
  if (options.output)
276
415
  config.outDir = options.output;
@@ -289,25 +428,33 @@ async function buildCommand(options) {
289
428
  const outDir = resolve3(process.cwd(), config.outDir);
290
429
  const providerName = config.provider || "gemini";
291
430
  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");
431
+ if (isWatch) {
432
+ printError("Gemini API key required", "Set GEMINI_API_KEY env var or add to intend.config.json");
433
+ } else {
434
+ p2.cancel(`${pc3.red("Error:")} Gemini API key required. Set GEMINI_API_KEY env var or add to intend.config.json`);
435
+ }
293
436
  process.exit(1);
294
437
  }
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();
438
+ const modelName = providerName === "ollama" ? config.ollama?.model || "llama3" : config.gemini?.model || "gemini-2.5-flash-lite";
439
+ if (!isWatch) {
440
+ console.log(`${pc3.dim("Source: ")}${pc3.cyan(sourceDir)}`);
441
+ console.log(`${pc3.dim("Output: ")}${pc3.cyan(outDir)}`);
442
+ console.log(`${pc3.dim("Provider: ")}${pc3.cyan(providerName)}`);
443
+ console.log(`${pc3.dim("Model: ")}${pc3.cyan(modelName)}`);
444
+ console.log("");
445
+ }
446
+ const sFiles = p2.spinner();
447
+ sFiles.start("Scanning for .intent files...");
301
448
  let files = [];
302
449
  try {
303
450
  files = findIntentFiles(sourceDir);
304
- scanSpinner.succeed(dim(`Found ${num(files.length)} intent file${files.length !== 1 ? "s" : ""}`));
451
+ sFiles.stop(`Found ${pc3.cyan(files.length)} intent files`);
305
452
  } catch (err) {
306
- scanSpinner.fail(error(`Failed to scan ${sourceDir}`));
453
+ sFiles.stop("Failed to scan directory", 1);
307
454
  process.exit(1);
308
455
  }
309
456
  if (files.length === 0) {
310
- printWarning("No .intent files found");
457
+ p2.log.warn("No .intent files found");
311
458
  return;
312
459
  }
313
460
  const startTime = Date.now();
@@ -322,8 +469,8 @@ async function buildCommand(options) {
322
469
  const hash = computeHash(content, config);
323
470
  fileData.set(file, { content, hash, ast: astResult.ast });
324
471
  } catch (err) {
325
- console.log(` ${icons.error} ${dim("Parse failed:")} ${basename(file)}`);
326
- console.log(` ${dim(err.message)}`);
472
+ p2.log.error(`Parse failed: ${pc3.dim(basename(file))}
473
+ ${pc3.red(err.message)}`);
327
474
  parseErrors++;
328
475
  }
329
476
  }
@@ -340,20 +487,18 @@ async function buildCommand(options) {
340
487
  projectContext,
341
488
  debug: options.debug
342
489
  });
343
- const connectSpinner = spinner(`Connecting to ${providerName}...`).start();
490
+ const sConnect = p2.spinner();
491
+ sConnect.start(`Connecting to ${providerName}...`);
344
492
  try {
345
493
  const connected = await generator.testConnection();
346
494
  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
- }
495
+ const msg = config.provider === "ollama" ? "Failed to connect to Ollama. Is it running?" : "Failed to connect to Gemini API";
496
+ sConnect.stop(msg, 1);
352
497
  process.exit(1);
353
498
  }
354
- connectSpinner.succeed(dim(`Connected to ${providerName}`));
499
+ sConnect.stop(`Connected to ${pc3.cyan(providerName)}`);
355
500
  } catch (error2) {
356
- connectSpinner.fail(error(`Connection failed: ${error2.message}`));
501
+ sConnect.stop(`Connection failed: ${error2.message}`, 1);
357
502
  process.exit(1);
358
503
  }
359
504
  const provider = generator.getProvider();
@@ -362,31 +507,29 @@ async function buildCommand(options) {
362
507
  if (typeof ollama.checkModelExists === "function") {
363
508
  const exists = await ollama.checkModelExists();
364
509
  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);
510
+ const modelName2 = config.ollama?.model || "llama3";
511
+ const shouldPull = await p2.confirm({
512
+ message: `Model ${pc3.cyan(modelName2)} not found locally. Pull it now?`,
513
+ initialValue: true
371
514
  });
372
- rl.close();
373
- if (answer.toLowerCase() === "n") {
374
- printError("Model missing, cannot proceed.");
515
+ if (p2.isCancel(shouldPull) || !shouldPull) {
516
+ p2.cancel("Operation cancelled. Model missing.");
375
517
  process.exit(1);
376
518
  }
377
- const pullSpinner = spinner(`Pulling ${modelName}...`).start();
519
+ const sPull = p2.spinner();
520
+ sPull.start(`Pulling ${modelName2}...`);
378
521
  try {
379
522
  await ollama.pullModel((status, completed, total) => {
380
523
  if (completed && total) {
381
524
  const percent = Math.round(completed / total * 100);
382
- pullSpinner.text = `Pulling ${modelName}: ${status} ${percent}%`;
525
+ sPull.message(`Pulling ${modelName2}: ${status} ${percent}%`);
383
526
  } else {
384
- pullSpinner.text = `Pulling ${modelName}: ${status}`;
527
+ sPull.message(`Pulling ${modelName2}: ${status}`);
385
528
  }
386
529
  });
387
- pullSpinner.succeed(`${modelName} ready`);
530
+ sPull.stop(`${pc3.green(modelName2)} is ready`);
388
531
  } catch (e) {
389
- pullSpinner.fail(`Failed to pull model: ${e.message}`);
532
+ sPull.stop(`Failed to pull model: ${e.message}`, 1);
390
533
  process.exit(1);
391
534
  }
392
535
  }
@@ -397,12 +540,15 @@ async function buildCommand(options) {
397
540
  let successCount = 0;
398
541
  let failCount = parseErrors;
399
542
  let cachedCount = 0;
543
+ const generatedFiles = [];
400
544
  if (!existsSync3(outDir)) {
401
545
  mkdirSync2(outDir, { recursive: true });
402
546
  }
403
547
  copyResources(sourceDir, outDir);
404
- newline();
405
- console.log(heading("Compiling"));
548
+ if (!isWatch) {
549
+ console.log(pc3.bold(`
550
+ \uD83D\uDE80 Compiling`));
551
+ }
406
552
  for (const file of files) {
407
553
  if (!fileData.has(file))
408
554
  continue;
@@ -412,17 +558,15 @@ async function buildCommand(options) {
412
558
  if (!existsSync3(outDirForFile)) {
413
559
  mkdirSync2(outDirForFile, { recursive: true });
414
560
  }
415
- const fileSpinner = spinner(`${relativePath}`).start();
561
+ const sFile = p2.spinner();
562
+ sFile.start(`${pc3.dim(relativePath)}`);
416
563
  const fileStart = Date.now();
417
564
  try {
418
565
  const { hash, ast } = fileData.get(file);
419
566
  const cached = await cas.get(hash);
420
567
  let finalCode;
421
568
  if (!options.force && cached) {
422
- fileSpinner.stopAndPersist({
423
- symbol: icons.cached,
424
- text: `${dim(relativePath)} ${dim("(cached)")}`
425
- });
569
+ sFile.stop(`${pc3.dim(relativePath)} ${pc3.blue("(cached)")}`);
426
570
  finalCode = cached.code;
427
571
  cachedCount++;
428
572
  } else {
@@ -430,25 +574,49 @@ async function buildCommand(options) {
430
574
  finalCode = generated.code;
431
575
  await cas.put(hash, finalCode);
432
576
  const fileDuration = Date.now() - fileStart;
433
- fileSpinner.succeed(`${relativePath} ${dim(`${fileDuration}ms`)}`);
577
+ sFile.stop(`${relativePath} ${pc3.dim(`${fileDuration}ms`)}`);
434
578
  }
435
579
  writeFileSync3(outFile, finalCode, "utf-8");
580
+ generatedFiles.push(outFile);
436
581
  successCount++;
437
582
  } catch (error2) {
438
- fileSpinner.fail(`${relativePath} ${dim(error2.message)}`);
583
+ sFile.stop(`${pc3.red(relativePath)} ${pc3.dim(error2.message)}`, 1);
439
584
  failCount++;
440
585
  }
441
586
  }
442
587
  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`);
588
+ if (!isWatch) {
589
+ p2.note(`${pc3.green(icons.success)} ${pc3.bold(successCount)} compiled ${cachedCount > 0 ? pc3.dim(`(${cachedCount} cached)`) : ""}
590
+ ` + (failCount > 0 ? `${pc3.red(icons.error)} ${pc3.bold(failCount)} failed
591
+ ` : "") + `${pc3.white(icons.arrow)} ${pc3.white(duration(totalDuration))}`, "Summary");
592
+ const entryFiles = files.filter((f) => {
593
+ const ast = fileData.get(f)?.ast;
594
+ return ast?.intents.some((i) => i.entryPoint);
595
+ }).map((f) => {
596
+ const rel = f.substring(sourceDir.length + 1).replace(/\.intent$/, ".ts");
597
+ return join3(config.outDir, rel);
598
+ });
599
+ let runCmd = "npx tsx";
600
+ try {
601
+ execSync("bun --version", { stdio: "ignore" });
602
+ runCmd = "bun run";
603
+ } catch (e) {
604
+ runCmd = "npx tsx";
605
+ }
606
+ if (entryFiles.length > 0) {
607
+ p2.note(entryFiles.map((f) => `${pc3.white("$")} ${pc3.cyan(pc3.bold(`${runCmd} ${f}`))}`).join(`
608
+ `), "Next Steps: Run your code");
609
+ } else if (successCount > 0) {
610
+ const firstFilePath = files.find((f) => fileData.has(f));
611
+ const relPath = firstFilePath ? firstFilePath.substring(sourceDir.length + 1).replace(/\.intent$/, ".ts") : "";
612
+ const outPath = join3(config.outDir, relPath);
613
+ p2.note(`${pc3.white("$")} ${pc3.cyan(pc3.bold(`${runCmd} ${outPath}`))}
614
+
615
+ ` + `${pc3.white("Tip: Add ")}${pc3.yellow("entry")}${pc3.white(" to an intent to make it the main entry point.")}`, "Next Steps: Run your code");
616
+ }
617
+ p2.outro("Build finished! ✨");
448
618
  }
449
- console.log(` ${icons.arrow} ${duration(totalDuration)}`);
450
- newline();
451
- if (failCount > 0) {
619
+ if (failCount > 0 && !isWatch) {
452
620
  process.exit(1);
453
621
  }
454
622
  }
@@ -463,7 +631,7 @@ async function watchCommand(options) {
463
631
  console.log(label("Watching", path(sourceDir)));
464
632
  newline();
465
633
  await buildCommand(options).catch(() => {});
466
- const watchSpinner = spinner("Waiting for changes...").start();
634
+ const watchSpinner = spinner2("Waiting for changes...").start();
467
635
  let isBuilding = false;
468
636
  let debounceTimer = null;
469
637
  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.1",
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.1",
34
+ "@intend-it/parser": "^1.3.1",
35
+ "ora": "^8.1.1",
36
+ "picocolors": "^1.1.1"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@types/bun": "latest",