@intend-it/cli 1.3.1 → 1.3.3
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 +11 -0
- package/dist/index.js +84 -16
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -65,6 +65,17 @@ Creates:
|
|
|
65
65
|
- `intend.config.json` - Project configuration
|
|
66
66
|
- `src/intents/` - Directory for `.intent` files
|
|
67
67
|
- `out/` - Output directory for generated TypeScript
|
|
68
|
+
- `intend.lock` - Deterministic build lockfile (commit this!)
|
|
69
|
+
|
|
70
|
+
### 🔒 Build Lockfile (`intend.lock`)
|
|
71
|
+
|
|
72
|
+
To ensure that your builds are deterministic and fast, Intend uses a lockfile system.
|
|
73
|
+
|
|
74
|
+
When you run `intend build`, the CLI calculates a hash of your `.intent` source code and your configuration. If a match is found in `intend.lock`, the compiler uses the cached implementation instead of calling the AI provider.
|
|
75
|
+
|
|
76
|
+
- **Fast Rebuilds**: Near-instant builds for unchanged files.
|
|
77
|
+
- **Stable Production**: Your code won't change in CI/CD unless you explicitly change the intention.
|
|
78
|
+
- **Version Control**: You should always commit `intend.lock` to your repository.
|
|
68
79
|
|
|
69
80
|
### `intend build`
|
|
70
81
|
|
package/dist/index.js
CHANGED
|
@@ -254,14 +254,30 @@ async function initCommand(args) {
|
|
|
254
254
|
if (!existsSync2(examplePath)) {
|
|
255
255
|
const exampleContent = `import { User } from "../types";
|
|
256
256
|
|
|
257
|
+
export intent GenerateRandomId() -> string {
|
|
258
|
+
step "Generate a random numeric ID" => const id: number
|
|
259
|
+
return id
|
|
260
|
+
}
|
|
261
|
+
|
|
257
262
|
export intent CreateUser(name: string) -> User {
|
|
258
|
-
|
|
259
|
-
step "Create a User object with name, id, and current date" => const user
|
|
263
|
+
GenerateRandomId() => const id
|
|
264
|
+
step "Create a User object with name, id, and current date" => const user: User
|
|
265
|
+
|
|
260
266
|
return user
|
|
261
267
|
}
|
|
262
268
|
|
|
269
|
+
// Entry point
|
|
263
270
|
export entry intent Main() -> void {
|
|
264
|
-
step "
|
|
271
|
+
step "Using console, ask the user for their name" => const name
|
|
272
|
+
|
|
273
|
+
CreateUser(name) => const user
|
|
274
|
+
|
|
275
|
+
invariant "Throw an error if the user.name is 'sudo' or 'root'"
|
|
276
|
+
|
|
277
|
+
ensure user.name is not empty
|
|
278
|
+
ensure user.id is not empty
|
|
279
|
+
ensure user.dateCreated is not undefined
|
|
280
|
+
|
|
265
281
|
step "Log 'Created user:' and the user object to console"
|
|
266
282
|
}
|
|
267
283
|
`;
|
|
@@ -289,7 +305,7 @@ out/
|
|
|
289
305
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync2, copyFileSync } from "fs";
|
|
290
306
|
import { execSync } from "child_process";
|
|
291
307
|
import { join as join3, resolve as resolve3, dirname, basename } from "path";
|
|
292
|
-
import { AICodeGenerator, FileSystemCAS, computeHash, OllamaProvider } from "@intend-it/core";
|
|
308
|
+
import { AICodeGenerator, FileSystemCAS, computeHash, OllamaProvider, LockfileManager } from "@intend-it/core";
|
|
293
309
|
import { parseToAST } from "@intend-it/parser";
|
|
294
310
|
import { readdirSync as readdirSync2, statSync } from "fs";
|
|
295
311
|
|
|
@@ -307,7 +323,15 @@ var heading = (text2) => pc2.bold(text2);
|
|
|
307
323
|
var error = (text2) => pc2.red(text2);
|
|
308
324
|
var dim = (text2) => pc2.dim(text2);
|
|
309
325
|
var path = (p2) => dim(p2);
|
|
310
|
-
var duration = (ms) =>
|
|
326
|
+
var duration = (ms) => {
|
|
327
|
+
if (ms < 1000)
|
|
328
|
+
return dim(`${Math.round(ms)}ms`);
|
|
329
|
+
if (ms < 60000)
|
|
330
|
+
return dim(`${(ms / 1000).toFixed(2)}s`);
|
|
331
|
+
const minutes = Math.floor(ms / 60000);
|
|
332
|
+
const seconds = (ms % 60000 / 1000).toFixed(0);
|
|
333
|
+
return dim(`${minutes}m ${seconds}s`);
|
|
334
|
+
};
|
|
311
335
|
var cmd = (name) => pc2.cyan(name);
|
|
312
336
|
var label = (name, value) => `${dim(name + ":")} ${value}`;
|
|
313
337
|
var icons = {
|
|
@@ -461,6 +485,23 @@ async function buildCommand(options) {
|
|
|
461
485
|
const projectContext = new Map;
|
|
462
486
|
const fileData = new Map;
|
|
463
487
|
let parseErrors = 0;
|
|
488
|
+
let entryPointFile = null;
|
|
489
|
+
let entryPointName = null;
|
|
490
|
+
for (const [file, ast] of projectContext.entries()) {
|
|
491
|
+
for (const intent of ast.intents) {
|
|
492
|
+
if (intent.entryPoint) {
|
|
493
|
+
if (entryPointFile) {
|
|
494
|
+
p2.log.error(`Multiple entry points found:
|
|
495
|
+
1. ${pc3.cyan(entryPointName)} in ${pc3.dim(basename(entryPointFile))}
|
|
496
|
+
2. ${pc3.cyan(intent.name)} in ${pc3.dim(basename(file))}`);
|
|
497
|
+
p2.cancel("Only one 'entry' intent is allowed per project.");
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
entryPointFile = file;
|
|
501
|
+
entryPointName = intent.name;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
464
505
|
for (const file of files) {
|
|
465
506
|
try {
|
|
466
507
|
const content = readFileSync2(file, "utf-8");
|
|
@@ -537,9 +578,13 @@ ${pc3.red(err.message)}`);
|
|
|
537
578
|
}
|
|
538
579
|
const casDir = resolve3(process.cwd(), ".intend", "store");
|
|
539
580
|
const cas = new FileSystemCAS(casDir);
|
|
581
|
+
const lockfilePath = resolve3(process.cwd(), "intend.lock");
|
|
582
|
+
const lockfile = new LockfileManager(lockfilePath);
|
|
583
|
+
let lockfileUpdated = false;
|
|
540
584
|
let successCount = 0;
|
|
541
585
|
let failCount = parseErrors;
|
|
542
586
|
let cachedCount = 0;
|
|
587
|
+
let lockedCount = 0;
|
|
543
588
|
const generatedFiles = [];
|
|
544
589
|
if (!existsSync3(outDir)) {
|
|
545
590
|
mkdirSync2(outDir, { recursive: true });
|
|
@@ -562,19 +607,39 @@ ${pc3.red(err.message)}`);
|
|
|
562
607
|
sFile.start(`${pc3.dim(relativePath)}`);
|
|
563
608
|
const fileStart = Date.now();
|
|
564
609
|
try {
|
|
565
|
-
const { hash, ast } = fileData.get(file);
|
|
566
|
-
const
|
|
610
|
+
const { content: sourceContent, hash, ast } = fileData.get(file);
|
|
611
|
+
const configHash = computeHash("", config);
|
|
612
|
+
const sourceOnlyHash = computeHash(sourceContent, {});
|
|
613
|
+
const lockedCode = lockfile.getEntry(relativePath, sourceOnlyHash, configHash);
|
|
567
614
|
let finalCode;
|
|
568
|
-
if (!options.force &&
|
|
569
|
-
sFile.stop(`${pc3.dim(relativePath)} ${pc3.
|
|
570
|
-
finalCode =
|
|
615
|
+
if (!options.force && lockedCode) {
|
|
616
|
+
sFile.stop(`${pc3.dim(relativePath)} ${pc3.green("(locked)")}`);
|
|
617
|
+
finalCode = lockedCode;
|
|
618
|
+
lockedCount++;
|
|
571
619
|
cachedCount++;
|
|
572
620
|
} else {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
621
|
+
const cached = await cas.get(hash);
|
|
622
|
+
if (!options.force && cached) {
|
|
623
|
+
sFile.stop(`${pc3.dim(relativePath)} ${pc3.blue("(cached)")}`);
|
|
624
|
+
finalCode = cached.code;
|
|
625
|
+
cachedCount++;
|
|
626
|
+
lockfile.setEntry(relativePath, sourceOnlyHash, configHash, finalCode, {
|
|
627
|
+
model: modelName,
|
|
628
|
+
provider: providerName
|
|
629
|
+
});
|
|
630
|
+
lockfileUpdated = true;
|
|
631
|
+
} else {
|
|
632
|
+
const generated = await generator.generate(ast, file);
|
|
633
|
+
finalCode = generated.code;
|
|
634
|
+
await cas.put(hash, finalCode);
|
|
635
|
+
lockfile.setEntry(relativePath, sourceOnlyHash, configHash, finalCode, {
|
|
636
|
+
model: modelName,
|
|
637
|
+
provider: providerName
|
|
638
|
+
});
|
|
639
|
+
lockfileUpdated = true;
|
|
640
|
+
const fileDuration = Date.now() - fileStart;
|
|
641
|
+
sFile.stop(`${relativePath} ${duration(fileDuration)}`);
|
|
642
|
+
}
|
|
578
643
|
}
|
|
579
644
|
writeFileSync3(outFile, finalCode, "utf-8");
|
|
580
645
|
generatedFiles.push(outFile);
|
|
@@ -584,9 +649,12 @@ ${pc3.red(err.message)}`);
|
|
|
584
649
|
failCount++;
|
|
585
650
|
}
|
|
586
651
|
}
|
|
652
|
+
if (lockfileUpdated) {
|
|
653
|
+
lockfile.save();
|
|
654
|
+
}
|
|
587
655
|
const totalDuration = Date.now() - startTime;
|
|
588
656
|
if (!isWatch) {
|
|
589
|
-
p2.note(`${pc3.green(icons.success)} ${pc3.bold(successCount)} compiled ${cachedCount > 0 ? pc3.dim(`(${cachedCount} cached)`) : ""}
|
|
657
|
+
p2.note(`${pc3.green(icons.success)} ${pc3.bold(successCount)} compiled ${cachedCount > 0 ? pc3.dim(`(${lockedCount} locked, ${cachedCount - lockedCount} cached)`) : ""}
|
|
590
658
|
` + (failCount > 0 ? `${pc3.red(icons.error)} ${pc3.bold(failCount)} failed
|
|
591
659
|
` : "") + `${pc3.white(icons.arrow)} ${pc3.white(duration(totalDuration))}`, "Summary");
|
|
592
660
|
const entryFiles = files.filter((f) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intend-it/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "CLI for the Intend programming language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^0.11.0",
|
|
33
|
-
"@intend-it/core": "^4.0.
|
|
34
|
-
"@intend-it/parser": "^1.3.
|
|
33
|
+
"@intend-it/core": "^4.0.3",
|
|
34
|
+
"@intend-it/parser": "^1.3.3",
|
|
35
35
|
"ora": "^8.1.1",
|
|
36
36
|
"picocolors": "^1.1.1"
|
|
37
37
|
},
|