@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.
- package/README.md +3 -0
- package/dist/index.js +365 -172
- 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
|
-
|
|
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/
|
|
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 = (
|
|
76
|
-
var
|
|
77
|
-
var
|
|
78
|
-
var
|
|
79
|
-
var
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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:
|
|
89
|
-
error:
|
|
90
|
-
warning:
|
|
91
|
-
info:
|
|
92
|
-
cached:
|
|
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
|
|
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(
|
|
336
|
+
function printHeader(text2) {
|
|
103
337
|
console.log();
|
|
104
|
-
console.log(heading(
|
|
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]")}
|
|
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
|
|
231
|
-
import
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
459
|
+
sFiles.stop(`Found ${pc3.cyan(files.length)} intent files`);
|
|
305
460
|
} catch (err) {
|
|
306
|
-
|
|
461
|
+
sFiles.stop("Failed to scan directory", 1);
|
|
307
462
|
process.exit(1);
|
|
308
463
|
}
|
|
309
464
|
if (files.length === 0) {
|
|
310
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
524
|
+
sConnect.stop(`Connected to ${pc3.cyan(providerName)}`);
|
|
355
525
|
} catch (error2) {
|
|
356
|
-
|
|
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
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
373
|
-
|
|
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
|
|
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
|
-
|
|
550
|
+
sPull.message(`Pulling ${modelName2}: ${status} ${percent}%`);
|
|
383
551
|
} else {
|
|
384
|
-
|
|
552
|
+
sPull.message(`Pulling ${modelName2}: ${status}`);
|
|
385
553
|
}
|
|
386
554
|
});
|
|
387
|
-
|
|
555
|
+
sPull.stop(`${pc3.green(modelName2)} is ready`);
|
|
388
556
|
} catch (e) {
|
|
389
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
"@
|
|
33
|
-
"@intend-it/core": "^4.0.
|
|
34
|
-
"
|
|
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",
|