@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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/dist/index.js +84 -16
  3. 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
- step "Generate a random ID" => const id
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 "Call CreateUser with name 'Intender'" => const user
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) => dim(`${(ms / 1000).toFixed(2)}s`);
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 cached = await cas.get(hash);
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 && cached) {
569
- sFile.stop(`${pc3.dim(relativePath)} ${pc3.blue("(cached)")}`);
570
- finalCode = cached.code;
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 generated = await generator.generate(ast, file);
574
- finalCode = generated.code;
575
- await cas.put(hash, finalCode);
576
- const fileDuration = Date.now() - fileStart;
577
- sFile.stop(`${relativePath} ${pc3.dim(`${fileDuration}ms`)}`);
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.1",
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.1",
34
- "@intend-it/parser": "^1.3.1",
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
  },