@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
@@ -1,26 +1,26 @@
1
1
  <role_and_goal>
2
- You are a professional translator proficient in multiple languages, skilled in accurate and standardized bilingual conversion.
2
+
3
+ You are an **Elite Polyglot Localization and Translation Specialist** with extensive professional experience across multiple domains. Your core mission is to produce translations that are not only **100% accurate** to the source meaning but are also **natively fluent, highly readable, and culturally appropriate** in the target language.
4
+
5
+ Core Mandates:
6
+
7
+ 1. Semantic Fidelity (Accuracy): The translation must perfectly and comprehensively convey the **entire meaning, tone, and nuance** of the source text. **No omission, addition, or distortion of the original content** is permitted.
8
+ 2. Native Fluency and Style: The resulting text must adhere strictly to the target language's **grammar, syntax, and idiomatic expressions**. The translation must **sound like it was originally written by a native speaker**, completely **free of grammatical errors** or "translationese" (literal, stiff, or unnatural phrasing).
9
+ 3. Readability and Flow: The final output must be **smooth, logical, and highly readable**. Sentences must flow naturally, ensuring a pleasant and coherent reading experience for the target audience.
10
+ 4. Localization and Clarity: Where a **literal (word-for-word) translation** of a term, phrase, or idiom would be **uncommon, confusing, or ambiguous** in the target language, you must apply **localization best practices**. This means translating the **concept** into the most **idiomatic, common, and easily understandable expression** in the target language.
11
+ 5. Versatility and Scope: You are proficient in handling **any pair of requested languages** (e.g., Chinese $\leftrightarrow$ English, English $\leftrightarrow$ Japanese) and are adept at translating diverse **document types**, including but not limited to: **Technical Manuals, Business Reports, Marketing Copy/Ads, Legal Documents, Academic Papers, and General Correspondence.**
3
12
  </role_and_goal>
4
13
 
5
14
  <translation_rules>
6
15
  Translation Requirements:
7
16
 
8
- - **Accurate Conveyance**: Accurately convey the facts and context of the original text, ensuring complete coverage.
9
- - **Avoid Exaggeration**: Avoid using emotionally charged or subjective words (for example, "excited" or "shocked").
10
- - **Follow Language Standards**: Ensure correct punctuation and grammar, and express ideas naturally and fluently.
11
- - **Preserve Original Structure**: Translate only the content portions without modifying tags or introducing any extra content or punctuation. Do not add markdown syntax at the outermost level. Ensure the translated structure matches the original, preserving line breaks and blank lines from the source.
12
- - **Strictly Protect Markdown Syntax**: All Markdown syntax characters, including but not limited to `|` and `-` in tables, `*` and `-` in lists, `#` in headings, `` ` `` in code blocks, etc., must be **copied exactly**, with no modification, addition, deletion, or merging. Table separators (e.g., `|---|---|---|`) must match the original column count and format exactly, with separator columns matching table data columns.
13
- - **Follow Translation Process**: Include literal translation, optimization, and omission checking to ensure the final output meets all requirements.
14
- - **Use Terminology Reference**: Ensure accuracy and consistency of professional terminology.
15
- - **Preserve Terms**: Retain specific terms in their original form, avoiding translation.
16
-
17
- Translation Process:
18
-
19
- - **Literal Translation**: Translate the original text word by word and sentence by sentence into the target language, ensuring every word's meaning is accurately conveyed.
20
- - **Optimization**: Based on the literal translation, ensure the text stays faithful to the original meaning while making it more natural and aligned with **{{ language }}** usage.
21
- - **Check for Omissions**: Compare the original with the literal translation to correct any meaning distortions or omissions.
22
- - **Format Check**: Compare the original with the literal translation to ensure the translated content is complete. If the original is in Markdown format, verify that the format matches the original.
23
- - **Final Output**: Output the optimized translation result, ensuring compliance with the above requirements (do not output the literal translation content).
17
+ - Avoid Exaggeration: Avoid using emotionally charged or subjective words (for example, "excited" or "shocked").
18
+ - Preserve Original Structure: Translate only the content portions without modifying tags or introducing any extra content or punctuation. Do not add markdown syntax at the outermost level. Ensure the translated structure matches the original, preserving line breaks and blank lines from the source.
19
+ - Strictly Protect Markdown Syntax: All Markdown syntax characters, including but not limited to `|` and `-` in tables, `*` and `-` in lists, `#` in headings, `` ` `` in code blocks, etc., must be **copied exactly**, with no modification, addition, deletion, or merging. Table separators (e.g., `|---|---|---|`) must match the original column count and format exactly, with separator columns matching table data columns.
20
+ - Use Terminology Reference: Ensure accuracy and consistency of professional terminology.
21
+ - Preserve Terms: Retain specific terms in their original form, avoiding translation.
22
+
23
+ {% include "./code-block.md" %}
24
24
  </translation_rules>
25
25
 
26
26
 
@@ -57,6 +57,9 @@ Terms to preserve (do not translate):
57
57
  </terms>
58
58
 
59
59
  <example>
60
+ <example_item>
61
+ **Special Note**: Keep table separators `|---|---|---|` unchanged from the original
62
+
60
63
  <before_translate>
61
64
  | Name | Type | Description |
62
65
  |---|---|---|
@@ -71,9 +74,10 @@ Terms to preserve (do not translate):
71
74
  | `id` | `string` | 要更新的 Webhook 端点的唯一标识符。 |
72
75
  | `data` | `PartialDeep<ABTNodeClient.WebhookEndpointStateInput>` | 包含要更新的 Webhook 端点字段的对象。 |
73
76
  </after_translate>
77
+ </example_item>
74
78
 
75
- **Special Note**: Keep table separators `|---|---|---|` unchanged from the original
76
-
79
+ <example_item>
80
+ **Special Note**: All x-field component attributes must maintain the original format. Only translate the description content within data-desc attributes or x-field-desc elements
77
81
  <before_translate>
78
82
 
79
83
  <x-field data-name="teamDid" data-type="string" data-required="true" data-desc="The DID of the team or Blocklet managing the webhook."></x-field>
@@ -89,8 +93,100 @@ Terms to preserve (do not translate):
89
93
  <x-field-desc markdown>您的 **API 密钥**,用于身份验证。请从 `设置 > API 密钥` 部分生成一个。</x-field-desc>
90
94
  </x-field>
91
95
  </after_translate>
96
+ </example_item>
97
+
98
+ <example_item>
99
+ **Special Note**: In code blocks, only translate comments while keeping all other code content (variables, functions, syntax) unchanged
100
+
101
+ <before_translate>
102
+ ```xxx
103
+ // Initialize the API client
104
+ const client = new APIClient({
105
+ apiKey: 'your-api-key', // Replace with your actual API key
106
+ baseUrl: 'https://api.example.com'
107
+ });
108
+
109
+ const errorMessage = 'Failed to fetch user data';
110
+ const successMessage = 'User data retrieved successfully';
111
+
112
+ // Send request to get user data
113
+ async function getUserData(userId) {
114
+ console.log('Starting user data fetch for ID:', userId);
115
+
116
+ try {
117
+ // Fetch user information from the API
118
+ const result = await client.get(`/users/${userId}`);
119
+ console.log('API response received');
120
+ console.log(successMessage);
121
+ return result;
122
+ } catch (error) {
123
+ console.error('Error occurred:', error.message);
124
+ throw new Error(errorMessage);
125
+ }
126
+ }
127
+ ```
128
+ </before_translate>
129
+
130
+ <after_translate>
131
+ ```xxx
132
+ // 初始化 API 客户端
133
+ const client = new APIClient({
134
+ apiKey: 'your-api-key', // 替换为您的实际 API 密钥
135
+ baseUrl: 'https://api.example.com'
136
+ });
137
+
138
+ const errorMessage = 'Failed to fetch user data';
139
+ const successMessage = 'User data retrieved successfully';
140
+
141
+ // 发送请求获取用户数据
142
+ async function getUserData(userId) {
143
+ console.log('Starting user data fetch for ID:', userId);
144
+
145
+ try {
146
+ // 从 API 获取用户信息
147
+ const result = await client.get(`/users/${userId}`);
148
+ console.log('API response received');
149
+ console.log(successMessage);
150
+ return result;
151
+ } catch (error) {
152
+ console.error('Error occurred:', error.message);
153
+ throw new Error(errorMessage);
154
+ }
155
+ }
156
+ ```
157
+ </after_translate>
158
+ </example_item>
159
+
160
+ <example_item>
161
+ **Special Note**: **Command execution and log printing** should untranslated
162
+
163
+ <before_translate>
164
+ ```text Timeout Error Message
165
+ Blocklet Server failed to stop within 5 minutes
166
+ You can stop blocklet server with blocklet stop --force
167
+ ```
168
+
169
+ ```bash Success Output
170
+ $ cli log
171
+
172
+ Cache for server cleared: [list of cleared cache keys]
173
+ ```
174
+ </before_translate>
175
+
176
+ <after_translate>
177
+ ```text 超时错误消息
178
+ Blocklet Server failed to stop within 5 minutes
179
+ You can stop blocklet server with blocklet stop --force
180
+ ```
181
+
182
+ ```bash 成功输出
183
+ $ cli log
184
+
185
+ Cache for server cleared: [list of cleared cache keys]
186
+ ```
187
+ </after_translate>
188
+ </example_item>
92
189
 
93
- **Special Note**: All x-field component attributes must maintain the original format. Only translate the description content within data-desc attributes or x-field-desc elements
94
190
  </example>
95
191
 
96
192
  Original text as follows:
@@ -986,11 +986,12 @@ describe("init", () => {
986
986
  const mockResponses = {
987
987
  checkbox_1: ["getStarted", "findAnswers"], // Document purpose
988
988
  checkbox_2: ["developers"], // Target audience
989
- select_3: "domainFamiliar", // Reader knowledge level
990
- select_4: "balancedCoverage", // Documentation depth
991
- select_5: "en", // Primary language
992
- checkbox_6: ["zh", "ja"], // Translation languages
993
- input_7: join(tempDir, "docs"), // Documentation directory
989
+ input_3: "Custom rules for documentation", // Custom rules
990
+ select_4: "domainFamiliar", // Reader knowledge level
991
+ select_5: "balancedCoverage", // Documentation depth
992
+ select_6: "en", // Primary language
993
+ checkbox_7: ["zh", "ja"], // Translation languages
994
+ input_8: join(tempDir, "docs"), // Documentation directory
994
995
  search: "", // Source paths (empty to finish)
995
996
  };
996
997
 
@@ -1023,6 +1024,7 @@ describe("init", () => {
1023
1024
 
1024
1025
  expect(config.documentPurpose).toEqual(["getStarted", "findAnswers"]);
1025
1026
  expect(config.targetAudienceTypes).toEqual(["developers"]);
1027
+ expect(config.rules).toBe("Custom rules for documentation");
1026
1028
  expect(config.readerKnowledgeLevel).toBe("domainFamiliar");
1027
1029
  expect(config.documentationDepth).toBe("balancedCoverage");
1028
1030
  expect(config.locale).toBe("en");
@@ -1042,11 +1044,12 @@ describe("init", () => {
1042
1044
  checkbox_1: ["mixedPurpose"], // Document purpose - triggers follow-up
1043
1045
  checkbox: ["completeTasks", "findAnswers"], // Top priorities after mixedPurpose
1044
1046
  checkbox_2: ["developers", "devops"], // Target audience
1045
- select_3: "experiencedUsers", // Reader knowledge level
1046
- select_4: "comprehensive", // Documentation depth
1047
- select_5: "zh-CN", // Primary language
1048
- checkbox_6: ["en"], // Translation languages
1049
- input_7: join(tempDir, "documentation"), // Documentation directory
1047
+ input_3: "Custom rules for documentation", // Custom rules
1048
+ select_4: "experiencedUsers", // Reader knowledge level
1049
+ select_5: "comprehensive", // Documentation depth
1050
+ select_6: "zh-CN", // Primary language
1051
+ checkbox_7: ["en"], // Translation languages
1052
+ input_8: join(tempDir, "documentation"), // Documentation directory
1050
1053
  search: "", // Source paths (empty to finish)
1051
1054
  };
1052
1055
 
@@ -1071,6 +1074,7 @@ describe("init", () => {
1071
1074
 
1072
1075
  expect(config.documentPurpose).toEqual(["completeTasks", "findAnswers"]);
1073
1076
  expect(config.targetAudienceTypes).toEqual(["developers", "devops"]);
1077
+ expect(config.rules).toBe("Custom rules for documentation");
1074
1078
  expect(config.readerKnowledgeLevel).toBe("experiencedUsers");
1075
1079
  expect(config.documentationDepth).toBe("comprehensive");
1076
1080
  expect(config.locale).toBe("zh-CN");
@@ -1087,11 +1091,12 @@ describe("init", () => {
1087
1091
  const mockResponses = {
1088
1092
  checkbox_1: ["getStarted"], // Document purpose
1089
1093
  checkbox_2: ["endUsers"], // Target audience
1090
- select_3: "completeBeginners", // Reader knowledge level
1091
- select_4: "essentialOnly", // Documentation depth
1092
- select_5: "en", // Primary language
1093
- checkbox_6: [], // No translation languages
1094
- input_7: join(tempDir, "simple-docs"), // Documentation directory
1094
+ input_3: "Custom rules for documentation", // Custom rules
1095
+ select_4: "completeBeginners", // Reader knowledge level
1096
+ select_5: "essentialOnly", // Documentation depth
1097
+ select_6: "en", // Primary language
1098
+ checkbox_7: [], // No translation languages
1099
+ input_8: join(tempDir, "simple-docs"), // Documentation directory
1095
1100
  search: "", // Source paths (empty to finish)
1096
1101
  };
1097
1102
 
@@ -1115,6 +1120,7 @@ describe("init", () => {
1115
1120
 
1116
1121
  expect(config.documentPurpose).toEqual(["getStarted"]);
1117
1122
  expect(config.targetAudienceTypes).toEqual(["endUsers"]);
1123
+ expect(config.rules).toBe("Custom rules for documentation");
1118
1124
  expect(config.readerKnowledgeLevel).toBe("completeBeginners");
1119
1125
  expect(config.documentationDepth).toBe("essentialOnly");
1120
1126
  expect(config.locale).toBe("en");
@@ -1298,7 +1304,7 @@ describe("init", () => {
1298
1304
  let validateCalled = false;
1299
1305
  const mockPrompts = {
1300
1306
  checkbox: (options) => {
1301
- if (options.message.includes("[1/8]") && options.validate) {
1307
+ if (options.message.includes("[1/9]") && options.validate) {
1302
1308
  // Test the validation function directly
1303
1309
  const validationResult = options.validate([]);
1304
1310
  expect(validationResult).toBe(
@@ -1336,10 +1342,10 @@ describe("init", () => {
1336
1342
  let audienceValidateCalled = false;
1337
1343
  const mockPrompts = {
1338
1344
  checkbox: (options) => {
1339
- if (options.message.includes("[1/8]")) {
1345
+ if (options.message.includes("[1/9]")) {
1340
1346
  return Promise.resolve(["getStarted"]); // Valid document purpose
1341
1347
  }
1342
- if (options.message.includes("[2/8]") && options.validate) {
1348
+ if (options.message.includes("[2/9]") && options.validate) {
1343
1349
  // Test the validation function for target audience
1344
1350
  const validationResult = options.validate([]);
1345
1351
  expect(validationResult).toBe("Please choose at least one audience.");
@@ -1374,7 +1380,7 @@ describe("init", () => {
1374
1380
  let priorityValidateCalled = false;
1375
1381
  const mockPrompts = {
1376
1382
  checkbox: (options) => {
1377
- if (options.message.includes("[1/8]")) {
1383
+ if (options.message.includes("[1/9]")) {
1378
1384
  return Promise.resolve(["mixedPurpose"]); // Trigger follow-up question
1379
1385
  }
1380
1386
  // This is the follow-up priority selection
@@ -1438,6 +1444,128 @@ describe("init", () => {
1438
1444
  });
1439
1445
  });
1440
1446
 
1447
+ describe("checkOnly parameter", () => {
1448
+ test("should exit silently when checkOnly is true and config exists", async () => {
1449
+ const tempDir = await createTempDir();
1450
+
1451
+ try {
1452
+ // Create existing config file
1453
+ const configPath = join(tempDir, "config.yaml");
1454
+ const existingConfig = `projectName: "Test Project"
1455
+ locale: "en"
1456
+ documentPurpose:
1457
+ - getStarted
1458
+ targetAudienceTypes:
1459
+ - developers`;
1460
+ await fs.writeFile(configPath, existingConfig, "utf8");
1461
+
1462
+ // The function should continue normally when config exists
1463
+ // No mocking needed as it doesn't log or exit in this case
1464
+ const result = await init(
1465
+ {
1466
+ outputPath: tempDir,
1467
+ fileName: "config.yaml",
1468
+ checkOnly: true,
1469
+ },
1470
+ { prompts: {} }, // Options not needed for checkOnly
1471
+ );
1472
+
1473
+ // Should return loaded config
1474
+ expect(result).toBeDefined();
1475
+ expect(result.projectName).toBe("Test Project");
1476
+ expect(result.locale).toBe("en");
1477
+ } finally {
1478
+ await cleanupTempDir(tempDir);
1479
+ }
1480
+ });
1481
+
1482
+ test("should show reminder and exit when checkOnly is true and config doesn't exist", async () => {
1483
+ const tempDir = await createTempDir();
1484
+
1485
+ try {
1486
+ // Mock console.log and process.exit
1487
+ const originalLog = console.log;
1488
+ const originalExit = process.exit;
1489
+ const logMessages = [];
1490
+ let exitCalled = false;
1491
+ let exitCode = null;
1492
+
1493
+ console.log = (...args) => logMessages.push(args.join(" "));
1494
+ process.exit = (code) => {
1495
+ exitCalled = true;
1496
+ exitCode = code;
1497
+ throw new Error("process.exit called"); // Prevent actual exit
1498
+ };
1499
+
1500
+ try {
1501
+ await expect(
1502
+ init(
1503
+ {
1504
+ outputPath: tempDir,
1505
+ fileName: "config.yaml",
1506
+ checkOnly: true,
1507
+ },
1508
+ { prompts: {} },
1509
+ ),
1510
+ ).rejects.toThrow("process.exit called");
1511
+
1512
+ // Should log reminder messages
1513
+ expect(logMessages.some((msg) => msg.includes("No configuration found"))).toBe(true);
1514
+ expect(logMessages.some((msg) => msg.includes("aigne doc init"))).toBe(true);
1515
+
1516
+ // Should call process.exit(0)
1517
+ expect(exitCalled).toBe(true);
1518
+ expect(exitCode).toBe(0);
1519
+ } finally {
1520
+ console.log = originalLog;
1521
+ process.exit = originalExit;
1522
+ }
1523
+ } finally {
1524
+ await cleanupTempDir(tempDir);
1525
+ }
1526
+ });
1527
+
1528
+ test("should not interfere with normal workflow when checkOnly is false", async () => {
1529
+ const tempDir = await createTempDir();
1530
+
1531
+ try {
1532
+ const mockPrompts = createMockPrompts({
1533
+ checkbox_1: ["getStarted"],
1534
+ checkbox_2: ["developers"],
1535
+ select_3: "domainFamiliar",
1536
+ select_4: "balancedCoverage",
1537
+ select_5: "en",
1538
+ checkbox_6: [],
1539
+ input_7: join(tempDir, "docs"),
1540
+ search: "",
1541
+ });
1542
+
1543
+ const options = { prompts: mockPrompts };
1544
+
1545
+ const result = await init(
1546
+ {
1547
+ outputPath: tempDir,
1548
+ fileName: "config.yaml",
1549
+ checkOnly: false, // Explicit false
1550
+ },
1551
+ options,
1552
+ );
1553
+
1554
+ expect(result).toBeDefined();
1555
+
1556
+ // Check that config file was created normally
1557
+ const configPath = join(tempDir, "config.yaml");
1558
+ const configExists = await fs
1559
+ .access(configPath)
1560
+ .then(() => true)
1561
+ .catch(() => false);
1562
+ expect(configExists).toBe(true);
1563
+ } finally {
1564
+ await cleanupTempDir(tempDir);
1565
+ }
1566
+ });
1567
+ });
1568
+
1441
1569
  describe("Advanced source path scenarios", () => {
1442
1570
  test("should handle source path validation and duplicate detection", async () => {
1443
1571
  const tempDir = await createTempDir();
@@ -15,6 +15,7 @@ import publishDocs from "../../../agents/publish/publish-docs.mjs";
15
15
  import * as authUtils from "../../../utils/auth-utils.mjs";
16
16
  import * as d2Utils from "../../../utils/d2-utils.mjs";
17
17
  import * as utils from "../../../utils/utils.mjs";
18
+ import * as deployUtils from "../../../utils/deploy.mjs";
18
19
 
19
20
  // Mock all external dependencies
20
21
  const mockPublishDocs = {
@@ -51,6 +52,7 @@ describe("publish-docs", () => {
51
52
  let getGithubRepoUrlSpy;
52
53
  let loadConfigFromFileSpy;
53
54
  let saveValueToConfigSpy;
55
+ let deploySpy;
54
56
 
55
57
  beforeAll(() => {
56
58
  // Apply mocks for external dependencies only
@@ -98,6 +100,10 @@ describe("publish-docs", () => {
98
100
  );
99
101
  loadConfigFromFileSpy = spyOn(utils, "loadConfigFromFile").mockResolvedValue({});
100
102
  saveValueToConfigSpy = spyOn(utils, "saveValueToConfig").mockResolvedValue();
103
+ deploySpy = spyOn(deployUtils, "deploy").mockResolvedValue({
104
+ appUrl: "https://deployed.example.com",
105
+ token: "deploy-token",
106
+ });
101
107
 
102
108
  mockOptions = {
103
109
  prompts: {
@@ -125,6 +131,7 @@ describe("publish-docs", () => {
125
131
  getGithubRepoUrlSpy?.mockRestore();
126
132
  loadConfigFromFileSpy?.mockRestore();
127
133
  saveValueToConfigSpy?.mockRestore();
134
+ deploySpy?.mockRestore();
128
135
  });
129
136
 
130
137
  // BASIC FUNCTIONALITY TESTS
@@ -593,6 +600,98 @@ describe("publish-docs", () => {
593
600
  expect(mockOptions.prompts.select).not.toHaveBeenCalled();
594
601
  });
595
602
 
603
+ // RESUME PREVIOUS WEBSITE SETUP TESTS
604
+ test("should show resume option when checkoutId exists in config", async () => {
605
+ loadConfigFromFileSpy.mockResolvedValue({
606
+ checkoutId: "cached-checkout-123",
607
+ });
608
+ mockOptions.prompts.select.mockResolvedValue("default");
609
+
610
+ await publishDocs(
611
+ {
612
+ docsDir: "./docs",
613
+ appUrl: "https://docsmith.aigne.io",
614
+ },
615
+ mockOptions,
616
+ );
617
+
618
+ expect(mockOptions.prompts.select).toHaveBeenCalledWith(
619
+ expect.objectContaining({
620
+ message: "Select platform to publish your documents:",
621
+ choices: expect.arrayContaining([
622
+ expect.objectContaining({
623
+ name: expect.stringContaining("Resume previous website setup"),
624
+ value: "new-instance-continue",
625
+ }),
626
+ ]),
627
+ }),
628
+ );
629
+
630
+ // Verify the exact text content
631
+ const selectCall = mockOptions.prompts.select.mock.calls[0][0];
632
+ const resumeChoice = selectCall.choices.find(
633
+ (choice) => choice.value === "new-instance-continue",
634
+ );
635
+ expect(resumeChoice.name).toContain("Resume previous website setup");
636
+ expect(resumeChoice.name).toContain("Already paid.");
637
+ expect(resumeChoice.name).toContain(
638
+ "Continue where you left off. Your payment has already been processed.",
639
+ );
640
+ });
641
+
642
+ test("should not show resume option when no checkoutId in config", async () => {
643
+ loadConfigFromFileSpy.mockResolvedValue({});
644
+ mockOptions.prompts.select.mockResolvedValue("default");
645
+
646
+ await publishDocs(
647
+ {
648
+ docsDir: "./docs",
649
+ appUrl: "https://docsmith.aigne.io",
650
+ },
651
+ mockOptions,
652
+ );
653
+
654
+ expect(mockOptions.prompts.select).toHaveBeenCalledWith(
655
+ expect.objectContaining({
656
+ message: "Select platform to publish your documents:",
657
+ choices: expect.not.arrayContaining([
658
+ expect.objectContaining({
659
+ value: "new-instance-continue",
660
+ }),
661
+ ]),
662
+ }),
663
+ );
664
+ });
665
+
666
+ test("should handle resume previous website setup selection", async () => {
667
+ deploySpy.mockResolvedValue({
668
+ appUrl: "https://resumed.example.com",
669
+ token: "resume-token",
670
+ });
671
+
672
+ loadConfigFromFileSpy.mockResolvedValue({
673
+ checkoutId: "cached-checkout-123",
674
+ paymentUrl: "https://payment.example.com",
675
+ });
676
+ mockOptions.prompts.select.mockResolvedValue("new-instance-continue");
677
+
678
+ const consoleSpy = spyOn(console, "log").mockImplementation(() => {});
679
+
680
+ await publishDocs(
681
+ {
682
+ docsDir: "./docs",
683
+ appUrl: "https://docsmith.aigne.io",
684
+ },
685
+ mockOptions,
686
+ );
687
+
688
+ expect(consoleSpy).toHaveBeenCalledWith("\nResuming your previous website setup...");
689
+ expect(deploySpy).toHaveBeenCalledWith("cached-checkout-123", "https://payment.example.com");
690
+ expect(getAccessTokenSpy).toHaveBeenCalledWith("https://resumed.example.com", "resume-token");
691
+
692
+ consoleSpy.mockRestore();
693
+ });
694
+
596
695
  test("should handle URL validation edge cases", async () => {
597
696
  loadConfigFromFileSpy.mockResolvedValue({});
598
697
  mockOptions.prompts.select.mockResolvedValue("custom");
@@ -229,7 +229,7 @@ set -euo pipefail
229
229
  function deploy_app() {
230
230
  local app_name="$1"
231
231
  local version="$2"
232
-
232
+
233
233
  echo "Deploying $app_name version $version"
234
234
  docker run --rm "$app_name:$version"
235
235
  }
@@ -337,19 +337,6 @@ This document demonstrates various programming language code blocks.`;
337
337
  });
338
338
  });
339
339
 
340
- describe("D2 syntax validation", () => {
341
- test("should handle D2 syntax errors", async () => {
342
- const documentStructure = [];
343
- const reviewContent =
344
- "```d2\n" +
345
- "invalid d2 syntax {{\n" + // Malformed D2
346
- "```\n\n" +
347
- "This has proper structure.";
348
- const result = await checkDetailResult({ documentStructure, reviewContent });
349
- expect(result.isApproved).toBe(false);
350
- });
351
- });
352
-
353
340
  describe("Advanced table edge cases", () => {
354
341
  test("should handle empty table cells correctly", async () => {
355
342
  const documentStructure = [];
@@ -660,7 +647,7 @@ flowchart TD
660
647
  A["1. Create Backend Implementation<br>api/src/providers/"]
661
648
  B["2. Add Backend Configuration<br>api/src/providers/models.ts"]
662
649
  C["3. Update Frontend Selector<br>src/pages/config/ai-providers/"]
663
-
650
+
664
651
  A --> B --> C
665
652
  \`\`\`
666
653
 
@@ -183,12 +183,7 @@ describe("chooseDocs utility", () => {
183
183
  locale: "en",
184
184
  };
185
185
 
186
- await expect(chooseDocs(input, mockOptions)).rejects.toThrow(
187
- "Please provide a docs parameter to specify which documents to update",
188
- );
189
- expect(consoleErrorSpy).toHaveBeenCalledWith(
190
- new Error("No documents found in the docs directory"),
191
- );
186
+ await expect(chooseDocs(input, mockOptions)).rejects.toThrow();
192
187
  });
193
188
 
194
189
  test("should throw error when no documents selected interactively", async () => {
@@ -202,9 +197,7 @@ describe("chooseDocs utility", () => {
202
197
  locale: "en",
203
198
  };
204
199
 
205
- await expect(chooseDocs(input, mockOptions)).rejects.toThrow(
206
- "Please provide a docs parameter to specify which documents to update",
207
- );
200
+ await expect(chooseDocs(input, mockOptions)).rejects.toThrow();
208
201
  });
209
202
 
210
203
  // CHECKBOX VALIDATION TESTS
@@ -336,7 +336,7 @@ describe("auth-utils", () => {
336
336
 
337
337
  // Test that openPage calls the mock open function
338
338
  await capturedOpenPage("https://auth.example.com");
339
- expect(mockOpen).toHaveBeenCalledWith("https://auth.example.com/");
339
+ expect(mockOpen).toHaveBeenCalledWith("https://auth.example.com/?required_roles=owner%2Cadmin");
340
340
  });
341
341
 
342
342
  test("should handle authorization failure", async () => {
@@ -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
  checkContent,
@@ -346,7 +346,7 @@ E -> F
346
346
  try {
347
347
  await checkContent({ content });
348
348
 
349
- const assetDir = path.join(process.cwd(), ".aigne", "doc-smith", ".tmp", "assets", "d2");
349
+ const assetDir = path.join(process.cwd(), DOC_SMITH_DIR, TMP_DIR, TMP_ASSETS_DIR, "d2");
350
350
  const files = await readdir(assetDir);
351
351
  const d2File = files.find((file) => file.endsWith(".d2"));
352
352
  expect(d2File).toBeDefined();
@@ -365,7 +365,7 @@ E -> F
365
365
  try {
366
366
  await ensureTmpDir();
367
367
 
368
- const tmpDir = path.join(tempDir, ".aigne", "doc-smith", ".tmp");
368
+ const tmpDir = path.join(tempDir, DOC_SMITH_DIR, TMP_DIR);
369
369
  const gitignorePath = path.join(tmpDir, ".gitignore");
370
370
 
371
371
  expect(existsSync(tmpDir)).toBe(true);
@@ -386,7 +386,7 @@ E -> F
386
386
  // First call
387
387
  await ensureTmpDir();
388
388
 
389
- const tmpDir = path.join(tempDir, ".aigne", "doc-smith", ".tmp");
389
+ const tmpDir = path.join(tempDir, DOC_SMITH_DIR, TMP_DIR);
390
390
  const gitignorePath = path.join(tmpDir, ".gitignore");
391
391
 
392
392
  // Modify .gitignore to test if it gets overwritten