@aigne/doc-smith 0.8.11-beta.3 → 0.8.11-beta.5

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 (43) hide show
  1. package/.aigne/doc-smith/config.yaml +1 -3
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +21 -0
  4. package/agents/clear/index.yaml +1 -0
  5. package/agents/evaluate/index.yaml +1 -0
  6. package/agents/generate/check-d2-diagram-valid.mjs +26 -0
  7. package/agents/generate/generate-d2-diagram.yaml +23 -0
  8. package/agents/generate/merge-d2-diagram.yaml +39 -0
  9. package/agents/init/index.mjs +42 -11
  10. package/agents/publish/index.yaml +1 -0
  11. package/agents/publish/publish-docs.mjs +17 -20
  12. package/agents/translate/index.yaml +1 -0
  13. package/agents/update/generate-document.yaml +25 -0
  14. package/agents/update/index.yaml +1 -0
  15. package/agents/update/update-single-document.yaml +1 -0
  16. package/agents/utils/choose-docs.mjs +22 -11
  17. package/package.json +2 -2
  18. package/prompts/detail/{d2-chart/rules.md → d2-diagram/rules-system.md} +41 -5
  19. package/prompts/detail/d2-diagram/rules-user.md +4 -0
  20. package/prompts/detail/document-rules.md +2 -3
  21. package/prompts/detail/generate-document.md +8 -2
  22. package/prompts/detail/update-document.md +1 -3
  23. package/prompts/translate/code-block.md +16 -0
  24. package/prompts/translate/translate-document.md +116 -20
  25. package/tests/agents/init/init.test.mjs +147 -19
  26. package/tests/agents/publish/publish-docs.test.mjs +99 -0
  27. package/tests/agents/utils/check-detail-result.test.mjs +2 -15
  28. package/tests/agents/utils/choose-docs.test.mjs +2 -9
  29. package/tests/utils/auth-utils.test.mjs +1 -1
  30. package/tests/utils/d2-utils.test.mjs +4 -4
  31. package/tests/utils/deploy.test.mjs +3 -10
  32. package/tests/utils/docs-finder-utils.test.mjs +12 -0
  33. package/tests/utils/kroki-utils.test.mjs +5 -5
  34. package/tests/utils/preferences-utils.test.mjs +5 -3
  35. package/tests/utils/save-value-to-config.test.mjs +3 -1
  36. package/utils/auth-utils.mjs +4 -0
  37. package/utils/constants/index.mjs +3 -0
  38. package/utils/d2-utils.mjs +11 -6
  39. package/utils/deploy.mjs +4 -20
  40. package/utils/docs-finder-utils.mjs +12 -1
  41. package/utils/kroki-utils.mjs +5 -4
  42. package/utils/markdown-checker.mjs +0 -20
  43. /package/prompts/detail/{d2-chart → d2-diagram}/official-examples.md +0 -0
@@ -91,15 +91,8 @@ describe("deploy", () => {
91
91
 
92
92
  // Verify BrokerClient was constructed with correct config
93
93
  expect(mockBrokerClientConstructor).toHaveBeenCalledWith({
94
- baseUrl: "",
95
94
  authToken: "mock-auth-token",
96
- paymentLinkKey: "PAYMENT_LINK_ID",
97
- timeout: 300000,
98
- polling: {
99
- interval: 3000,
100
- maxAttempts: 100,
101
- backoffStrategy: "linear",
102
- },
95
+ baseUrl: "https://docsmith.aigne.io",
103
96
  });
104
97
 
105
98
  // Verify deploy was called with correct parameters
@@ -120,7 +113,6 @@ describe("deploy", () => {
120
113
  ACCESS_PREPARING: expect.any(Function),
121
114
  ACCESS_READY: expect.any(Function),
122
115
  }),
123
- onError: expect.any(Function),
124
116
  }),
125
117
  );
126
118
 
@@ -348,7 +340,8 @@ describe("deploy", () => {
348
340
  // Verify BrokerClient was constructed with empty baseUrl
349
341
  expect(mockBrokerClientConstructor).toHaveBeenCalledWith(
350
342
  expect.objectContaining({
351
- baseUrl: "",
343
+ authToken: "mock-auth-token",
344
+ baseUrl: "https://docsmith.aigne.io",
352
345
  }),
353
346
  );
354
347
  });
@@ -15,6 +15,7 @@ import {
15
15
  describe("docs-finder-utils", () => {
16
16
  let readdirSpy;
17
17
  let readFileSpy;
18
+ let accessSpy;
18
19
  let joinSpy;
19
20
  let consoleWarnSpy;
20
21
 
@@ -22,6 +23,7 @@ describe("docs-finder-utils", () => {
22
23
  // Mock file system operations
23
24
  readdirSpy = spyOn(fs, "readdir").mockResolvedValue([]);
24
25
  readFileSpy = spyOn(fs, "readFile").mockResolvedValue("test content");
26
+ accessSpy = spyOn(fs, "access").mockResolvedValue(undefined);
25
27
  joinSpy = spyOn(path, "join").mockImplementation((...paths) => paths.join("/"));
26
28
 
27
29
  // Mock console methods
@@ -31,6 +33,7 @@ describe("docs-finder-utils", () => {
31
33
  afterEach(() => {
32
34
  readdirSpy?.mockRestore();
33
35
  readFileSpy?.mockRestore();
36
+ accessSpy?.mockRestore();
34
37
  joinSpy?.mockRestore();
35
38
  consoleWarnSpy?.mockRestore();
36
39
  });
@@ -606,11 +609,20 @@ describe("docs-finder-utils", () => {
606
609
  });
607
610
 
608
611
  test("getMainLanguageFiles should handle readdir errors", async () => {
612
+ accessSpy.mockResolvedValue(undefined); // Directory exists
609
613
  readdirSpy.mockRejectedValue(new Error("Permission denied"));
610
614
 
611
615
  await expect(getMainLanguageFiles("/denied", "en")).rejects.toThrow("Permission denied");
612
616
  });
613
617
 
618
+ test("getMainLanguageFiles should throw non-ENOENT access errors", async () => {
619
+ const permissionError = new Error("Permission denied");
620
+ permissionError.code = "EACCES";
621
+ accessSpy.mockRejectedValue(permissionError);
622
+
623
+ await expect(getMainLanguageFiles("/denied", "en")).rejects.toThrow("Permission denied");
624
+ });
625
+
614
626
  test("processSelectedFiles should handle empty document structure", async () => {
615
627
  readFileSpy.mockResolvedValue("content");
616
628
 
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
 
7
7
  import Debug from "debug";
8
8
 
9
- import { TMP_ASSETS_DIR } from "../../utils/constants/index.mjs";
9
+ import { DOC_SMITH_DIR, TMP_ASSETS_DIR, TMP_DIR } from "../../utils/constants/index.mjs";
10
10
  import {
11
11
  beforePublishHook,
12
12
  checkD2Content,
@@ -451,7 +451,7 @@ E -> F
451
451
  try {
452
452
  await checkD2Content({ content });
453
453
 
454
- const assetDir = path.join(process.cwd(), ".aigne", "doc-smith", ".tmp", "assets", "d2");
454
+ const assetDir = path.join(process.cwd(), DOC_SMITH_DIR, TMP_DIR, TMP_ASSETS_DIR, "d2");
455
455
  const files = await readdir(assetDir);
456
456
  const d2File = files.find((file) => file.endsWith(".d2"));
457
457
  expect(d2File).toBeDefined();
@@ -470,7 +470,7 @@ E -> F
470
470
  try {
471
471
  await ensureTmpDir();
472
472
 
473
- const tmpDir = path.join(tempDir, ".aigne", "doc-smith", ".tmp");
473
+ const tmpDir = path.join(tempDir, DOC_SMITH_DIR, TMP_DIR);
474
474
  const gitignorePath = path.join(tmpDir, ".gitignore");
475
475
 
476
476
  expect(existsSync(tmpDir)).toBe(true);
@@ -491,7 +491,7 @@ E -> F
491
491
  // First call
492
492
  await ensureTmpDir();
493
493
 
494
- const tmpDir = path.join(tempDir, ".aigne", "doc-smith", ".tmp");
494
+ const tmpDir = path.join(tempDir, DOC_SMITH_DIR, TMP_DIR);
495
495
  const gitignorePath = path.join(tmpDir, ".gitignore");
496
496
 
497
497
  // Modify .gitignore to test if it gets overwritten
@@ -632,7 +632,7 @@ E -> F
632
632
  await Promise.all(promises);
633
633
 
634
634
  // Should only create directory once
635
- const tmpDir = path.join(tempDir, ".aigne", "doc-smith", ".tmp");
635
+ const tmpDir = path.join(tempDir, DOC_SMITH_DIR, TMP_DIR);
636
636
  expect(existsSync(tmpDir)).toBe(true);
637
637
 
638
638
  // .gitignore should be created properly
@@ -3,6 +3,8 @@ import { existsSync } from "node:fs";
3
3
  import { mkdir, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+
7
+ import { DOC_SMITH_DIR } from "../../utils/constants/index.mjs";
6
8
  import {
7
9
  addPreferenceRule,
8
10
  deactivateRule,
@@ -44,7 +46,7 @@ describe("preferences-utils", () => {
44
46
 
45
47
  test("should read existing preferences file", async () => {
46
48
  // Create preferences directory and file
47
- const prefsDir = join(testDir, ".aigne", "doc-smith");
49
+ const prefsDir = join(testDir, DOC_SMITH_DIR);
48
50
  await mkdir(prefsDir, { recursive: true });
49
51
 
50
52
  await writeFile(
@@ -69,7 +71,7 @@ describe("preferences-utils", () => {
69
71
 
70
72
  test("should handle malformed YAML gracefully", async () => {
71
73
  // Create preferences directory and invalid file
72
- const prefsDir = join(testDir, ".aigne", "doc-smith");
74
+ const prefsDir = join(testDir, DOC_SMITH_DIR);
73
75
  await mkdir(prefsDir, { recursive: true });
74
76
 
75
77
  await writeFile(join(prefsDir, "preferences.yml"), "invalid: yaml: content: [", "utf8");
@@ -85,7 +87,7 @@ describe("preferences-utils", () => {
85
87
 
86
88
  writePreferences(testPreferences);
87
89
 
88
- const prefsDir = join(testDir, ".aigne", "doc-smith");
90
+ const prefsDir = join(testDir, DOC_SMITH_DIR);
89
91
  expect(existsSync(prefsDir)).toBe(true);
90
92
  expect(existsSync(join(prefsDir, "preferences.yml"))).toBe(true);
91
93
  });
@@ -5,6 +5,8 @@ import fsPromisesDefault, * as fsPromises from "node:fs/promises";
5
5
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
6
  import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+
9
+ import { DOC_SMITH_DIR } from "../../utils/constants/index.mjs";
8
10
  import { saveValueToConfig } from "../../utils/utils.mjs";
9
11
 
10
12
  const __filename = fileURLToPath(import.meta.url);
@@ -12,7 +14,7 @@ const __dirname = dirname(__filename);
12
14
 
13
15
  // Test directory for isolated testing
14
16
  const TEST_DIR = join(__dirname, "temp-config-test");
15
- const TEST_CONFIG_DIR = join(TEST_DIR, ".aigne", "doc-smith");
17
+ const TEST_CONFIG_DIR = join(TEST_DIR, DOC_SMITH_DIR);
16
18
  const TEST_CONFIG_PATH = join(TEST_CONFIG_DIR, "config.yaml");
17
19
 
18
20
  // Store original working directory
@@ -15,6 +15,7 @@ import {
15
15
  } from "./blocklet.mjs";
16
16
  import {
17
17
  BLOCKLET_ADD_COMPONENT_DOCS,
18
+ DEFAULT_APP_URL,
18
19
  DISCUSS_KIT_DID,
19
20
  DISCUSS_KIT_STORE_URL,
20
21
  DOC_OFFICIAL_ACCESS_TOKEN,
@@ -97,6 +98,9 @@ export async function getAccessToken(appUrl, ltToken = "") {
97
98
  appLogo: "https://docsmith.aigne.io/image-bin/uploads/9645caf64b4232699982c4d940b03b90.svg",
98
99
  openPage: (pageUrl) => {
99
100
  const url = new URL(pageUrl);
101
+ if (url.hostname !== DEFAULT_APP_URL) {
102
+ url.searchParams.set("required_roles", "owner,admin");
103
+ }
100
104
  if (ltToken) {
101
105
  url.searchParams.set("__lt", ltToken);
102
106
  }
@@ -334,6 +334,9 @@ export const PAYMENT_KIT_DID = "z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk";
334
334
 
335
335
  export const DOC_OFFICIAL_ACCESS_TOKEN = "DOC_OFFICIAL_ACCESS_TOKEN";
336
336
 
337
+ // Default application URL for the document deployment website.
338
+ export const DEFAULT_APP_URL = "https://docsmith.aigne.io";
339
+
337
340
  // Discuss Kit related URLs
338
341
  export const DISCUSS_KIT_STORE_URL =
339
342
  "https://store.blocklet.dev/blocklets/z8ia1WEiBZ7hxURf6LwH21Wpg99vophFwSJdu";
@@ -17,6 +17,8 @@ import { debug } from "./debug.mjs";
17
17
  import { iconMap } from "./icon-map.mjs";
18
18
  import { getContentHash } from "./utils.mjs";
19
19
 
20
+ const codeBlockRegex = /```d2.*\n([\s\S]*?)```/g;
21
+
20
22
  export async function getChart({ content, strict }) {
21
23
  const d2 = new D2();
22
24
  const iconUrlList = Object.keys(iconMap);
@@ -60,7 +62,7 @@ export async function getChart({ content, strict }) {
60
62
  } catch (err) {
61
63
  if (strict) throw err;
62
64
 
63
- console.error("Failed to generate D2 chart. Content:", content, "Error:", err);
65
+ console.error("Failed to generate D2 diagram. Content:", content, "Error:", err);
64
66
  return null;
65
67
  } finally {
66
68
  d2.worker.terminate();
@@ -73,8 +75,6 @@ export async function saveAssets({ markdown, docsDir }) {
73
75
  return markdown;
74
76
  }
75
77
 
76
- const codeBlockRegex = /```d2.*\n([\s\S]*?)```/g;
77
-
78
78
  const { replaced } = await runIterator({
79
79
  input: markdown,
80
80
  regexp: codeBlockRegex,
@@ -90,7 +90,7 @@ export async function saveAssets({ markdown, docsDir }) {
90
90
  debug("Found assets cache, skipping generation", svgPath);
91
91
  } else {
92
92
  try {
93
- debug("start generate d2 chart", svgPath);
93
+ debug("start generate d2 diagram", svgPath);
94
94
  if (debug.enabled) {
95
95
  const d2FileName = `${getContentHash(d2Content)}.d2`;
96
96
  const d2Path = path.join(assetDir, d2FileName);
@@ -102,7 +102,7 @@ export async function saveAssets({ markdown, docsDir }) {
102
102
  await fs.writeFile(svgPath, svg, { encoding: "utf8" });
103
103
  }
104
104
  } catch (error) {
105
- debug("Failed to generate D2 chart. Content:", d2Content, "Error:", error);
105
+ debug("Failed to generate D2 diagram. Content:", d2Content, "Error:", error);
106
106
  return _code;
107
107
  }
108
108
  }
@@ -156,7 +156,12 @@ async function runIterator({ input, regexp, fn = () => {}, options, replace = fa
156
156
  };
157
157
  }
158
158
 
159
- export async function checkContent({ content }) {
159
+ export async function checkContent({ content: _content }) {
160
+ const matches = Array.from(_content.matchAll(codeBlockRegex));
161
+ let content = _content;
162
+ if (matches.length > 0) {
163
+ content = matches[0][1];
164
+ }
160
165
  await ensureTmpDir();
161
166
  const assetDir = path.join(DOC_SMITH_DIR, TMP_DIR, TMP_ASSETS_DIR, "d2");
162
167
  await fs.ensureDir(assetDir);
package/utils/deploy.mjs CHANGED
@@ -2,10 +2,11 @@ import { BrokerClient, STEPS } from "@blocklet/payment-broker-client/node";
2
2
  import chalk from "chalk";
3
3
  import open from "open";
4
4
  import { getOfficialAccessToken } from "./auth-utils.mjs";
5
+ import { DEFAULT_APP_URL } from "./constants/index.mjs";
5
6
  import { saveValueToConfig } from "./utils.mjs";
6
7
 
7
8
  // ==================== Configuration ====================
8
- const BASE_URL = process.env.DOC_SMITH_BASE_URL || "";
9
+ const BASE_URL = process.env.DOC_SMITH_BASE_URL || DEFAULT_APP_URL;
9
10
  const SUCCESS_MESSAGE = {
10
11
  en: "Congratulations! Your website has been successfully installed. You can return to the command-line tool to continue the next steps.",
11
12
  zh: "恭喜您,你的网站已安装成功!可以返回命令行工具继续后续操作!",
@@ -24,26 +25,14 @@ export async function deploy(id, cachedUrl) {
24
25
  throw new Error("Failed to get official access token");
25
26
  }
26
27
 
27
- const client = new BrokerClient({
28
- baseUrl: BASE_URL,
29
- authToken,
30
- paymentLinkKey: "PAYMENT_LINK_ID",
31
- timeout: 300000,
32
- polling: {
33
- interval: 3000,
34
- maxAttempts: 100,
35
- backoffStrategy: "linear",
36
- },
37
- });
28
+ const client = new BrokerClient({ baseUrl: BASE_URL, authToken });
38
29
 
39
30
  console.log(`🚀 Starting deployment...`);
40
31
 
41
32
  const result = await client.deploy({
42
33
  cachedCheckoutId: id,
43
34
  cachedPaymentUrl: cachedUrl,
44
- pageInfo: {
45
- successMessage: SUCCESS_MESSAGE,
46
- },
35
+ pageInfo: { successMessage: SUCCESS_MESSAGE },
47
36
  hooks: {
48
37
  [STEPS.PAYMENT_PENDING]: async ({ sessionId, paymentUrl, isResuming }) => {
49
38
  console.log(`⏳ Step 1/4: Waiting for payment...`);
@@ -86,11 +75,6 @@ export async function deploy(id, cachedUrl) {
86
75
  }
87
76
  },
88
77
  },
89
-
90
- onError: (error, step) => {
91
- console.error(`${chalk.red("❌")} Deployment failed at ${step || "unknown step"}:`);
92
- console.error(` ${error.message}`);
93
- },
94
78
  });
95
79
 
96
80
  const { appUrl, homeUrl, subscriptionUrl, dashboardUrl, vendors } = result;
@@ -1,4 +1,4 @@
1
- import { readdir, readFile } from "node:fs/promises";
1
+ import { access, readdir, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  /**
@@ -120,6 +120,17 @@ export async function readFileContent(docsDir, fileName) {
120
120
  * @returns {Promise<string[]>} Array of main language .md files ordered by documentExecutionStructure
121
121
  */
122
122
  export async function getMainLanguageFiles(docsDir, locale, documentExecutionStructure = null) {
123
+ // Check if docsDir exists
124
+ try {
125
+ await access(docsDir);
126
+ } catch (error) {
127
+ if (error.code === "ENOENT") {
128
+ return [];
129
+ }
130
+
131
+ throw error;
132
+ }
133
+
123
134
  const files = await readdir(docsDir);
124
135
 
125
136
  // Filter for main language .md files (exclude _sidebar.md)
@@ -8,6 +8,7 @@ import { joinURL } from "ufo";
8
8
 
9
9
  import {
10
10
  D2_CONFIG,
11
+ DOC_SMITH_DIR,
11
12
  FILE_CONCURRENCY,
12
13
  KROKI_CONCURRENCY,
13
14
  TMP_ASSETS_DIR,
@@ -76,7 +77,7 @@ export async function saveD2Assets({ markdown, docsDir }) {
76
77
  debug("Found assets cache, skipping generation", svgPath);
77
78
  } else {
78
79
  try {
79
- debug("start generate d2 chart", svgPath);
80
+ debug("Start generate d2 diagram", svgPath);
80
81
  if (debug.enabled) {
81
82
  const d2FileName = `${getContentHash(d2Content)}.d2`;
82
83
  const d2Path = path.join(assetDir, d2FileName);
@@ -88,7 +89,7 @@ export async function saveD2Assets({ markdown, docsDir }) {
88
89
  await fs.writeFile(svgPath, svg, { encoding: "utf8" });
89
90
  }
90
91
  } catch (error) {
91
- debug("Failed to generate D2 chart:", error);
92
+ debug("Failed to generate D2 diagram:", error);
92
93
  return _code;
93
94
  }
94
95
  }
@@ -144,7 +145,7 @@ async function runIterator({ input, regexp, fn = () => {}, options, replace = fa
144
145
 
145
146
  export async function checkD2Content({ content }) {
146
147
  await ensureTmpDir();
147
- const assetDir = path.join(".aigne", "doc-smith", TMP_DIR, TMP_ASSETS_DIR, "d2");
148
+ const assetDir = path.join(DOC_SMITH_DIR, TMP_DIR, TMP_ASSETS_DIR, "d2");
148
149
  await fs.ensureDir(assetDir);
149
150
  const d2Content = [D2_CONFIG, content].join("\n");
150
151
  const fileName = `${getContentHash(d2Content)}.svg`;
@@ -166,7 +167,7 @@ export async function checkD2Content({ content }) {
166
167
  }
167
168
 
168
169
  export async function ensureTmpDir() {
169
- const tmpDir = path.join(".aigne", "doc-smith", TMP_DIR);
170
+ const tmpDir = path.join(DOC_SMITH_DIR, TMP_DIR);
170
171
  if (!(await fs.pathExists(path.join(tmpDir, ".gitignore")))) {
171
172
  await fs.ensureDir(tmpDir);
172
173
  await fs.writeFile(path.join(tmpDir, ".gitignore"), "**/*", { encoding: "utf8" });
@@ -1,14 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import pMap from "p-map";
4
3
  import remarkGfm from "remark-gfm";
5
4
  import remarkLint from "remark-lint";
6
5
  import remarkParse from "remark-parse";
7
6
  import { unified } from "unified";
8
7
  import { visit } from "unist-util-visit";
9
8
  import { VFile } from "vfile";
10
- import { KROKI_CONCURRENCY } from "./constants/index.mjs";
11
- import { checkContent, isValidCode } from "./d2-utils.mjs";
12
9
  import { validateMermaidSyntax } from "./mermaid-validator.mjs";
13
10
 
14
11
  /**
@@ -378,7 +375,6 @@ export async function checkMarkdown(markdown, source = "content", options = {})
378
375
 
379
376
  // Check mermaid code blocks and other custom validations
380
377
  const mermaidChecks = [];
381
- const d2ChecksList = [];
382
378
  visit(ast, "code", (node) => {
383
379
  if (node.lang) {
384
380
  const line = node.position?.start?.line || "unknown";
@@ -467,13 +463,6 @@ export async function checkMarkdown(markdown, source = "content", options = {})
467
463
  specialCharMatch = nodeWithSpecialCharsRegex.exec(mermaidContent);
468
464
  }
469
465
  }
470
- if (isValidCode(node.lang)) {
471
- d2ChecksList.push({
472
- content: node.value,
473
- line,
474
- });
475
- }
476
- // TODO: @zhanghan need to check correctness of every code language
477
466
  }
478
467
  });
479
468
 
@@ -524,15 +513,6 @@ export async function checkMarkdown(markdown, source = "content", options = {})
524
513
 
525
514
  // Wait for all mermaid checks to complete
526
515
  await Promise.all(mermaidChecks);
527
- await pMap(
528
- d2ChecksList,
529
- async ({ content, line }) =>
530
- checkContent({ content }).catch((err) => {
531
- const errorMessage = err?.message || String(err) || "Unknown d2 syntax error";
532
- errorMessages.push(`Found D2 syntax error in ${source} at line ${line}: ${errorMessage}`);
533
- }),
534
- { concurrency: KROKI_CONCURRENCY },
535
- );
536
516
 
537
517
  // Run markdown linting rules
538
518
  await processor.run(ast, file);