@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.
- package/README.md +3 -0
- package/dist/index.js +339 -171
- 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,31 @@ var BRAND = {
|
|
|
72
303
|
name: "intend",
|
|
73
304
|
version: pkg.version
|
|
74
305
|
};
|
|
75
|
-
var heading = (
|
|
76
|
-
var
|
|
77
|
-
var
|
|
78
|
-
var
|
|
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) =>
|
|
311
|
+
var cmd = (name) => pc2.cyan(name);
|
|
86
312
|
var label = (name, value) => `${dim(name + ":")} ${value}`;
|
|
87
313
|
var icons = {
|
|
88
|
-
success:
|
|
89
|
-
error:
|
|
90
|
-
warning:
|
|
91
|
-
info:
|
|
92
|
-
cached:
|
|
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
|
|
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(
|
|
328
|
+
function printHeader(text2) {
|
|
103
329
|
console.log();
|
|
104
|
-
console.log(heading(
|
|
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]")}
|
|
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
|
|
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";
|
|
369
|
+
import * as p2 from "@clack/prompts";
|
|
370
|
+
import pc3 from "picocolors";
|
|
236
371
|
function findIntentFiles(dir, fileList = []) {
|
|
237
|
-
const files =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
451
|
+
sFiles.stop(`Found ${pc3.cyan(files.length)} intent files`);
|
|
305
452
|
} catch (err) {
|
|
306
|
-
|
|
453
|
+
sFiles.stop("Failed to scan directory", 1);
|
|
307
454
|
process.exit(1);
|
|
308
455
|
}
|
|
309
456
|
if (files.length === 0) {
|
|
310
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
499
|
+
sConnect.stop(`Connected to ${pc3.cyan(providerName)}`);
|
|
355
500
|
} catch (error2) {
|
|
356
|
-
|
|
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
|
|
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);
|
|
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
|
-
|
|
373
|
-
|
|
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
|
|
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
|
-
|
|
525
|
+
sPull.message(`Pulling ${modelName2}: ${status} ${percent}%`);
|
|
383
526
|
} else {
|
|
384
|
-
|
|
527
|
+
sPull.message(`Pulling ${modelName2}: ${status}`);
|
|
385
528
|
}
|
|
386
529
|
});
|
|
387
|
-
|
|
530
|
+
sPull.stop(`${pc3.green(modelName2)} is ready`);
|
|
388
531
|
} catch (e) {
|
|
389
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
"@
|
|
33
|
-
"@intend-it/core": "^4.0.
|
|
34
|
-
"
|
|
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",
|