@aigne/doc-smith 0.8.4 → 0.8.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.
- package/CHANGELOG.md +12 -0
- package/agents/input-generator.mjs +12 -6
- package/agents/publish-docs.mjs +53 -4
- package/docs/_sidebar.md +1 -1
- package/package.json +1 -1
- package/tests/input-generator.test.mjs +2 -2
- package/utils/auth-utils.mjs +9 -2
- package/utils/blocklet.mjs +25 -6
- package/utils/constants.mjs +2 -0
- package/utils/deploy.mjs +404 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.5](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.4...v0.8.5) (2025-09-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* support publish docs to enterprise spaces ([#82](https://github.com/AIGNE-io/aigne-doc-smith/issues/82)) ([35b577a](https://github.com/AIGNE-io/aigne-doc-smith/commit/35b577ac0f2c1b860a23185054a55bada3742e8e))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Miscellaneous Chores
|
|
12
|
+
|
|
13
|
+
* release 0.8.5 ([7a60a03](https://github.com/AIGNE-io/aigne-doc-smith/commit/7a60a03f91a20f378e94b12dd32a6a8b0a4bede5))
|
|
14
|
+
|
|
3
15
|
## [0.8.4](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.3...v0.8.4) (2025-09-09)
|
|
4
16
|
|
|
5
17
|
|
|
@@ -385,12 +385,18 @@ export function generateYAML(input) {
|
|
|
385
385
|
// Generate comments and structure
|
|
386
386
|
let yaml = "# Project information for documentation publishing\n";
|
|
387
387
|
|
|
388
|
-
// Serialize the project info section safely
|
|
389
|
-
const projectSection = yamlStringify(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
388
|
+
// Serialize the project info section safely with string quoting
|
|
389
|
+
const projectSection = yamlStringify(
|
|
390
|
+
{
|
|
391
|
+
projectName: config.projectName,
|
|
392
|
+
projectDesc: config.projectDesc,
|
|
393
|
+
projectLogo: config.projectLogo,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
quotingType: '"',
|
|
397
|
+
defaultStringType: "QUOTE_DOUBLE",
|
|
398
|
+
},
|
|
399
|
+
).trim();
|
|
394
400
|
|
|
395
401
|
yaml += `${projectSection}\n\n`;
|
|
396
402
|
|
package/agents/publish-docs.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "fs-extra";
|
|
|
5
5
|
|
|
6
6
|
import { getAccessToken } from "../utils/auth-utils.mjs";
|
|
7
7
|
import { DISCUSS_KIT_STORE_URL, TMP_DIR, TMP_DOCS_DIR } from "../utils/constants.mjs";
|
|
8
|
+
import { deploy } from "../utils/deploy.mjs";
|
|
8
9
|
import { beforePublishHook, ensureTmpDir } from "../utils/kroki-utils.mjs";
|
|
9
10
|
import { getGithubRepoUrl, loadConfigFromFile, saveValueToConfig } from "../utils/utils.mjs";
|
|
10
11
|
|
|
@@ -17,6 +18,8 @@ export default async function publishDocs(
|
|
|
17
18
|
// move work dir to tmp-dir
|
|
18
19
|
await ensureTmpDir();
|
|
19
20
|
|
|
21
|
+
const hasDocSmithBaseUrl = !!process.env.DOC_SMITH_BASE_URL;
|
|
22
|
+
|
|
20
23
|
const docsDir = join(".aigne", "doc-smith", TMP_DIR, TMP_DOCS_DIR);
|
|
21
24
|
await fs.rm(docsDir, { recursive: true, force: true });
|
|
22
25
|
await fs.mkdir(docsDir, {
|
|
@@ -42,18 +45,41 @@ export default async function publishDocs(
|
|
|
42
45
|
const isDefaultAppUrl = appUrl === DEFAULT_APP_URL;
|
|
43
46
|
const hasAppUrlInConfig = config?.appUrl;
|
|
44
47
|
|
|
48
|
+
let token = "";
|
|
49
|
+
|
|
45
50
|
if (!useEnvAppUrl && isDefaultAppUrl && !hasAppUrlInConfig) {
|
|
51
|
+
const hasCachedCheckoutId = !!config?.checkoutId;
|
|
46
52
|
const choice = await options.prompts.select({
|
|
47
53
|
message: "Select platform to publish your documents:",
|
|
48
54
|
choices: [
|
|
49
55
|
{
|
|
50
|
-
name:
|
|
56
|
+
name:
|
|
57
|
+
chalk.blue("Publish to docsmith.aigne.io") +
|
|
58
|
+
" - free, but your documents will be publicly accessible, recommended for open-source projects",
|
|
51
59
|
value: "default",
|
|
52
60
|
},
|
|
53
61
|
{
|
|
54
|
-
name: "Publish to your
|
|
62
|
+
name: `${chalk.blue("Publish to your existing website")} - use your current website`,
|
|
55
63
|
value: "custom",
|
|
56
64
|
},
|
|
65
|
+
...(hasCachedCheckoutId && hasDocSmithBaseUrl
|
|
66
|
+
? [
|
|
67
|
+
{
|
|
68
|
+
name:
|
|
69
|
+
chalk.yellow("Continue your previous website setup") +
|
|
70
|
+
" - resume from where you left off",
|
|
71
|
+
value: "new-instance-continue",
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
: []),
|
|
75
|
+
...(hasDocSmithBaseUrl
|
|
76
|
+
? [
|
|
77
|
+
{
|
|
78
|
+
name: `${chalk.blue("Publish to a new website")} - we'll help you set up a new website`,
|
|
79
|
+
value: "new-instance",
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
57
83
|
],
|
|
58
84
|
});
|
|
59
85
|
|
|
@@ -63,7 +89,7 @@ export default async function publishDocs(
|
|
|
63
89
|
`Start here to run your own website:\n${chalk.cyan(DISCUSS_KIT_STORE_URL)}\n`,
|
|
64
90
|
);
|
|
65
91
|
const userInput = await options.prompts.input({
|
|
66
|
-
message: "Please enter your
|
|
92
|
+
message: "Please enter your website URL:",
|
|
67
93
|
validate: (input) => {
|
|
68
94
|
try {
|
|
69
95
|
// Check if input contains protocol, if not, prepend https://
|
|
@@ -77,10 +103,31 @@ export default async function publishDocs(
|
|
|
77
103
|
});
|
|
78
104
|
// Ensure appUrl has protocol
|
|
79
105
|
appUrl = userInput.includes("://") ? userInput : `https://${userInput}`;
|
|
106
|
+
} else if (hasDocSmithBaseUrl && ["new-instance", "new-instance-continue"].includes(choice)) {
|
|
107
|
+
// Deploy a new Discuss Kit service
|
|
108
|
+
try {
|
|
109
|
+
let id = "";
|
|
110
|
+
let paymentUrl = "";
|
|
111
|
+
if (choice === "new-instance-continue") {
|
|
112
|
+
id = config?.checkoutId;
|
|
113
|
+
paymentUrl = config?.paymentUrl;
|
|
114
|
+
console.log(`\nResuming your previous website setup...`);
|
|
115
|
+
} else {
|
|
116
|
+
console.log(`\nCreating a new doc website for your documentation...`);
|
|
117
|
+
}
|
|
118
|
+
const { appUrl: homeUrl, token: ltToken } = (await deploy(id, paymentUrl)) || {};
|
|
119
|
+
|
|
120
|
+
appUrl = homeUrl;
|
|
121
|
+
token = ltToken;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const errorMsg = error?.message || "Unknown error occurred";
|
|
124
|
+
console.error(`${chalk.red("❌ Failed to publish to website:")} ${errorMsg}`);
|
|
125
|
+
return { message: `❌ Publish failed: ${errorMsg}` };
|
|
126
|
+
}
|
|
80
127
|
}
|
|
81
128
|
}
|
|
82
129
|
|
|
83
|
-
const accessToken = await getAccessToken(appUrl);
|
|
130
|
+
const accessToken = await getAccessToken(appUrl, token);
|
|
84
131
|
|
|
85
132
|
process.env.DOC_ROOT_DIR = docsDir;
|
|
86
133
|
|
|
@@ -138,6 +185,8 @@ export default async function publishDocs(
|
|
|
138
185
|
} catch (error) {
|
|
139
186
|
message = `❌ Failed to publish docs: ${error.message}`;
|
|
140
187
|
}
|
|
188
|
+
saveValueToConfig("checkoutId", "", "Checkout ID for document deployment service");
|
|
189
|
+
|
|
141
190
|
// clean up tmp work dir
|
|
142
191
|
await fs.rm(docsDir, { recursive: true, force: true });
|
|
143
192
|
return message ? { message } : {};
|
package/docs/_sidebar.md
CHANGED
package/package.json
CHANGED
|
@@ -913,7 +913,7 @@ describe("generateYAML", () => {
|
|
|
913
913
|
// Should always include these sections
|
|
914
914
|
expect(result).toContain("# Project information for documentation publishing");
|
|
915
915
|
expect(result).toContain("# Documentation Configuration");
|
|
916
|
-
expect(result).toContain("projectName:
|
|
916
|
+
expect(result).toContain('"projectName":');
|
|
917
917
|
expect(result).toContain("documentPurpose:");
|
|
918
918
|
expect(result).toContain("targetAudienceTypes:");
|
|
919
919
|
expect(result).toContain("locale:");
|
|
@@ -1287,7 +1287,7 @@ describe("init", () => {
|
|
|
1287
1287
|
|
|
1288
1288
|
// Config should be generated since original was empty
|
|
1289
1289
|
const configContent = await fs.readFile(configPath, "utf8");
|
|
1290
|
-
expect(configContent).toContain("projectName:
|
|
1290
|
+
expect(configContent).toContain('"projectName":');
|
|
1291
1291
|
expect(configContent).toContain("locale: en");
|
|
1292
1292
|
} finally {
|
|
1293
1293
|
await cleanupTempDir(tempDir);
|
package/utils/auth-utils.mjs
CHANGED
|
@@ -25,7 +25,7 @@ const WELLKNOWN_SERVICE_PATH_PREFIX = "/.well-known/service";
|
|
|
25
25
|
* @param {string} appUrl - The application URL
|
|
26
26
|
* @returns {Promise<string>} - The access token
|
|
27
27
|
*/
|
|
28
|
-
export async function getAccessToken(appUrl) {
|
|
28
|
+
export async function getAccessToken(appUrl, ltToken = "") {
|
|
29
29
|
const DOC_SMITH_ENV_FILE = join(homedir(), ".aigne", "doc-smith-connected.yaml");
|
|
30
30
|
const { hostname } = new URL(appUrl);
|
|
31
31
|
|
|
@@ -92,7 +92,14 @@ export async function getAccessToken(appUrl) {
|
|
|
92
92
|
closeOnSuccess: true,
|
|
93
93
|
appName: "AIGNE DocSmith",
|
|
94
94
|
appLogo: "https://docsmith.aigne.io/image-bin/uploads/a7910a71364ee15a27e86f869ad59009.svg",
|
|
95
|
-
openPage: (pageUrl) =>
|
|
95
|
+
openPage: (pageUrl) => {
|
|
96
|
+
const url = new URL(pageUrl);
|
|
97
|
+
if (ltToken) {
|
|
98
|
+
url.searchParams.set("__lt", ltToken);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
open(url.toString());
|
|
102
|
+
},
|
|
96
103
|
});
|
|
97
104
|
|
|
98
105
|
accessToken = result.accessKeySecret;
|
package/utils/blocklet.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { joinURL } from "ufo";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Custom error class for invalid blocklet application URLs
|
|
3
5
|
*/
|
|
@@ -23,17 +25,14 @@ export class ComponentNotFoundError extends Error {
|
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
export async function
|
|
27
|
-
const
|
|
28
|
-
const blockletJsUrl = `${url.origin}/__blocklet__.js?type=json`;
|
|
28
|
+
export async function getComponentInfo(appUrl) {
|
|
29
|
+
const blockletJsUrl = joinURL(appUrl, "__blocklet__.js?type=json");
|
|
29
30
|
|
|
30
31
|
let blockletJs;
|
|
31
32
|
try {
|
|
32
33
|
blockletJs = await fetch(blockletJsUrl, {
|
|
33
34
|
method: "GET",
|
|
34
|
-
headers: {
|
|
35
|
-
Accept: "application/json",
|
|
36
|
-
},
|
|
35
|
+
headers: { Accept: "application/json" },
|
|
37
36
|
});
|
|
38
37
|
} catch (error) {
|
|
39
38
|
throw new InvalidBlockletError(appUrl, null, error.message);
|
|
@@ -50,6 +49,12 @@ export async function getComponentMountPoint(appUrl, did) {
|
|
|
50
49
|
throw new InvalidBlockletError(appUrl, null, "Invalid JSON response");
|
|
51
50
|
}
|
|
52
51
|
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getComponentMountPoint(appUrl, did) {
|
|
56
|
+
const config = await getComponentInfo(appUrl);
|
|
57
|
+
|
|
53
58
|
const component = config.componentMountPoints?.find((component) => component.did === did);
|
|
54
59
|
if (!component) {
|
|
55
60
|
throw new ComponentNotFoundError(did, appUrl);
|
|
@@ -57,3 +62,17 @@ export async function getComponentMountPoint(appUrl, did) {
|
|
|
57
62
|
|
|
58
63
|
return component.mountPoint;
|
|
59
64
|
}
|
|
65
|
+
|
|
66
|
+
export async function getComponentInfoWithMountPoint(appUrl, did) {
|
|
67
|
+
const config = await getComponentInfo(appUrl);
|
|
68
|
+
|
|
69
|
+
const component = config.componentMountPoints?.find((component) => component.did === did);
|
|
70
|
+
if (!component) {
|
|
71
|
+
throw new ComponentNotFoundError(did, appUrl);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
...config,
|
|
76
|
+
mountPoint: component.mountPoint,
|
|
77
|
+
};
|
|
78
|
+
}
|
package/utils/constants.mjs
CHANGED
|
@@ -330,6 +330,8 @@ export const DEPTH_RECOMMENDATION_LOGIC = {
|
|
|
330
330
|
// Component mount point ID for Discuss Kit
|
|
331
331
|
export const DISCUSS_KIT_DID = "z8ia1WEiBZ7hxURf6LwH21Wpg99vophFwSJdu";
|
|
332
332
|
|
|
333
|
+
export const PAYMENT_KIT_DID = "z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk";
|
|
334
|
+
|
|
333
335
|
// Discuss Kit related URLs
|
|
334
336
|
export const DISCUSS_KIT_STORE_URL =
|
|
335
337
|
"https://store.blocklet.dev/blocklets/z8ia1WEiBZ7hxURf6LwH21Wpg99vophFwSJdu";
|
package/utils/deploy.mjs
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { joinURL } from "ufo";
|
|
3
|
+
import { getComponentInfo, getComponentInfoWithMountPoint } from "./blocklet.mjs";
|
|
4
|
+
import { PAYMENT_KIT_DID } from "./constants.mjs";
|
|
5
|
+
import { saveValueToConfig } from "./utils.mjs";
|
|
6
|
+
|
|
7
|
+
// ==================== URL Configuration ====================
|
|
8
|
+
const BASE_URL = process.env.DOC_SMITH_BASE_URL || "";
|
|
9
|
+
|
|
10
|
+
// ==================== Timeout Configuration ====================
|
|
11
|
+
const INTERVAL_MS = 3000; // 3 seconds between each check
|
|
12
|
+
const TIMEOUTS = {
|
|
13
|
+
paymentWait: 300, // Step 2: Payment wait timeout (5 minutes)
|
|
14
|
+
installation: 300, // Step 3: Installation timeout (5 minutes)
|
|
15
|
+
serviceStart: 300, // Step 4: Service startup timeout (5 minutes)
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ==================== Utility Functions ====================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generic polling utility with timeout and retry logic
|
|
22
|
+
* @param {Object} options - Polling configuration
|
|
23
|
+
* @param {Function} options.checkCondition - Async function that returns result if condition met, null/false if not
|
|
24
|
+
* @param {number} options.maxAttempts - Maximum number of attempts
|
|
25
|
+
* @param {number} options.intervalMs - Interval between attempts in milliseconds
|
|
26
|
+
* @param {string} options.timeoutMessage - Error message for timeout
|
|
27
|
+
* @param {string} options.stepName - Name of the step for logging (optional)
|
|
28
|
+
* @returns {Promise<any>} Result from checkCondition when successful
|
|
29
|
+
*/
|
|
30
|
+
async function pollWithTimeout({
|
|
31
|
+
checkCondition,
|
|
32
|
+
maxAttempts,
|
|
33
|
+
intervalMs = INTERVAL_MS,
|
|
34
|
+
timeoutMessage,
|
|
35
|
+
stepName = "Operation",
|
|
36
|
+
}) {
|
|
37
|
+
let attempts = 0;
|
|
38
|
+
|
|
39
|
+
while (attempts < maxAttempts) {
|
|
40
|
+
attempts++;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const result = await checkCondition();
|
|
44
|
+
if (result !== null && result !== false) {
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
} catch (_error) {
|
|
48
|
+
// Log error for debugging but continue retrying unless it's the last attempt
|
|
49
|
+
if (attempts === maxAttempts) {
|
|
50
|
+
throw new Error(`${timeoutMessage} (${stepName} failed after ${maxAttempts} attempts)`);
|
|
51
|
+
}
|
|
52
|
+
// Continue retrying for non-fatal errors
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If this is the last attempt, don't wait - just exit the loop
|
|
56
|
+
if (attempts === maxAttempts) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wait before retry
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If we reach here, all attempts were exhausted
|
|
65
|
+
throw new Error(`${timeoutMessage} (${stepName} timed out after ${maxAttempts} attempts)`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ==================== API Endpoints ====================
|
|
69
|
+
const API_ENDPOINTS = {
|
|
70
|
+
createCheckout: `/api/checkout-sessions/start`,
|
|
71
|
+
paymentPage: `/checkout/pay/{id}`,
|
|
72
|
+
orderStatus: `/api/vendors/order/{id}/status`,
|
|
73
|
+
orderDetail: `/api/vendors/order/{id}/detail`,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
let prefix = "";
|
|
77
|
+
let paymentLinkId = "";
|
|
78
|
+
/**
|
|
79
|
+
* Deploy a new Discuss Kit service and return the installation URL
|
|
80
|
+
* @returns {Promise<string>} - The URL of the deployed service
|
|
81
|
+
*/
|
|
82
|
+
export async function deploy(id, cachedUrl) {
|
|
83
|
+
const { mountPoint, PAYMENT_LINK_ID } = await getComponentInfoWithMountPoint(
|
|
84
|
+
BASE_URL,
|
|
85
|
+
PAYMENT_KIT_DID,
|
|
86
|
+
);
|
|
87
|
+
prefix = mountPoint;
|
|
88
|
+
paymentLinkId = PAYMENT_LINK_ID;
|
|
89
|
+
|
|
90
|
+
if (!PAYMENT_LINK_ID) {
|
|
91
|
+
const { PAYMENT_LINK_ID: id } = await getComponentInfoWithMountPoint(
|
|
92
|
+
joinURL(BASE_URL, mountPoint),
|
|
93
|
+
PAYMENT_KIT_DID,
|
|
94
|
+
);
|
|
95
|
+
paymentLinkId = id;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Step 1: Create payment link and open
|
|
99
|
+
const cachedCheckoutId = await checkCacheCheckoutId(id);
|
|
100
|
+
let checkoutId = cachedCheckoutId;
|
|
101
|
+
let paymentUrl = cachedUrl;
|
|
102
|
+
if (!cachedCheckoutId) {
|
|
103
|
+
const { checkoutId: newCheckoutId, paymentUrl: newPaymentUrl } = await createPaymentSession();
|
|
104
|
+
checkoutId = newCheckoutId;
|
|
105
|
+
paymentUrl = newPaymentUrl;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!paymentUrl) {
|
|
109
|
+
paymentUrl = joinURL(BASE_URL, prefix, API_ENDPOINTS.paymentPage.replace("{id}", checkoutId));
|
|
110
|
+
}
|
|
111
|
+
if (cachedCheckoutId !== checkoutId) {
|
|
112
|
+
await openBrowser(paymentUrl);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Step 2: Wait for payment completion
|
|
116
|
+
console.log(`${chalk.blue("⏳")} Step 1/4: Waiting for payment...`);
|
|
117
|
+
console.log(`${chalk.blue("🔗")} Payment link: ${chalk.cyan(paymentUrl)}\n`);
|
|
118
|
+
await pollPaymentStatus(checkoutId);
|
|
119
|
+
saveValueToConfig("checkoutId", checkoutId, "Checkout ID for document deployment service");
|
|
120
|
+
saveValueToConfig("paymentUrl", paymentUrl, "Payment URL for document deployment service");
|
|
121
|
+
|
|
122
|
+
// Step 3: Wait for service installation
|
|
123
|
+
console.log(`${chalk.blue("📦")} Step 2/4: Installing service...`);
|
|
124
|
+
const readyVendors = await waitInstallation(checkoutId);
|
|
125
|
+
|
|
126
|
+
// Step 4: Wait for service startup
|
|
127
|
+
console.log(`${chalk.blue("🚀")} Step 3/4: Starting service...`);
|
|
128
|
+
const runningVendors = await waitServiceRunning(readyVendors);
|
|
129
|
+
|
|
130
|
+
// Step 5: Get final URL
|
|
131
|
+
console.log(`${chalk.blue("🌐")} Step 4/4: Getting service URL...`);
|
|
132
|
+
const urlInfo = await getDashboardAndUrl(checkoutId, runningVendors);
|
|
133
|
+
const { appUrl, homeUrl, token } = urlInfo || {};
|
|
134
|
+
|
|
135
|
+
console.log(
|
|
136
|
+
`\n${chalk.blue("🔗")} Your website is available at: ${chalk.cyan(homeUrl || appUrl)}\n`,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
appUrl,
|
|
141
|
+
homeUrl,
|
|
142
|
+
token,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if there is a cached checkoutId
|
|
148
|
+
*/
|
|
149
|
+
async function checkCacheCheckoutId(checkoutId) {
|
|
150
|
+
try {
|
|
151
|
+
if (!checkoutId) {
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const orderStatusUrl = joinURL(
|
|
156
|
+
BASE_URL,
|
|
157
|
+
prefix,
|
|
158
|
+
API_ENDPOINTS.orderStatus.replace("{id}", checkoutId),
|
|
159
|
+
);
|
|
160
|
+
const response = await fetch(orderStatusUrl);
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const data = await response.json();
|
|
167
|
+
|
|
168
|
+
if (data.error) {
|
|
169
|
+
throw new Error(data.error);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check payment status and vendors status
|
|
173
|
+
const isPaid = data.payment_status === "paid";
|
|
174
|
+
|
|
175
|
+
return isPaid ? checkoutId : "";
|
|
176
|
+
} catch (_error) {
|
|
177
|
+
saveValueToConfig("checkoutId", "", "Checkout ID for document deployment service");
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create payment session - Step 1
|
|
184
|
+
*/
|
|
185
|
+
async function createPaymentSession() {
|
|
186
|
+
// 1. Call payment API
|
|
187
|
+
if (!paymentLinkId) {
|
|
188
|
+
throw new Error("Payment link ID not found");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const createCheckoutId = joinURL(BASE_URL, prefix, API_ENDPOINTS.createCheckout, paymentLinkId);
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetch(createCheckoutId, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { "Content-Type": "application/json" },
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
needShortUrl: true,
|
|
198
|
+
metadata: {
|
|
199
|
+
page_info: {
|
|
200
|
+
has_vendor: true,
|
|
201
|
+
success_message: {
|
|
202
|
+
en: "Congratulations! Your website has been successfully installed. You can return to the command-line tool to continue the next steps.",
|
|
203
|
+
zh: "恭喜您,你的网站已安装成功!可以返回命令行工具继续后续操作!",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const data = await response.json();
|
|
215
|
+
const checkoutId = data.checkoutSession.id;
|
|
216
|
+
const paymentUrl = data.paymentUrl;
|
|
217
|
+
|
|
218
|
+
return { checkoutId, paymentUrl };
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(
|
|
221
|
+
`${chalk.red("❌")} Failed to create payment session:`,
|
|
222
|
+
error.message,
|
|
223
|
+
createCheckoutId,
|
|
224
|
+
);
|
|
225
|
+
throw new Error(`Failed to create payment session: ${error.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Open browser with payment URL
|
|
231
|
+
*/
|
|
232
|
+
async function openBrowser(paymentUrl) {
|
|
233
|
+
const { default: open } = await import("open");
|
|
234
|
+
try {
|
|
235
|
+
await open(paymentUrl);
|
|
236
|
+
} catch (_error) {
|
|
237
|
+
console.log(`${chalk.yellow("⚠️ Could not open browser automatically.")}`);
|
|
238
|
+
console.log(`${chalk.blue("Please manually open this URL:")} ${chalk.cyan(paymentUrl)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Wait for payment completion - Step 2 (5 minute timeout)
|
|
244
|
+
*/
|
|
245
|
+
async function pollPaymentStatus(checkoutId) {
|
|
246
|
+
return pollWithTimeout({
|
|
247
|
+
checkCondition: async () => {
|
|
248
|
+
const orderStatusUrl = joinURL(
|
|
249
|
+
BASE_URL,
|
|
250
|
+
prefix,
|
|
251
|
+
API_ENDPOINTS.orderStatus.replace("{id}", checkoutId),
|
|
252
|
+
);
|
|
253
|
+
const response = await fetch(orderStatusUrl);
|
|
254
|
+
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const data = await response.json();
|
|
260
|
+
|
|
261
|
+
if (data.error) {
|
|
262
|
+
throw new Error(data.error);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check payment status and vendors status
|
|
266
|
+
const isPaid = data.payment_status === "paid";
|
|
267
|
+
if (isPaid) {
|
|
268
|
+
return data.vendors;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return null; // Not ready yet, continue polling
|
|
272
|
+
},
|
|
273
|
+
maxAttempts: Math.ceil((TIMEOUTS.paymentWait * 1000) / INTERVAL_MS),
|
|
274
|
+
intervalMs: INTERVAL_MS,
|
|
275
|
+
timeoutMessage: "Payment timeout - please complete payment within 5 minutes",
|
|
276
|
+
stepName: "Payment",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Wait for installation completion - Step 3
|
|
282
|
+
*/
|
|
283
|
+
async function waitInstallation(checkoutId) {
|
|
284
|
+
return pollWithTimeout({
|
|
285
|
+
checkCondition: async () => {
|
|
286
|
+
const orderStatusUrl = joinURL(
|
|
287
|
+
BASE_URL,
|
|
288
|
+
prefix,
|
|
289
|
+
API_ENDPOINTS.orderStatus.replace("{id}", checkoutId),
|
|
290
|
+
);
|
|
291
|
+
const response = await fetch(orderStatusUrl);
|
|
292
|
+
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
|
|
299
|
+
if (data.error) {
|
|
300
|
+
throw new Error(data.error);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check if all vendors meet conditions: progress >= 80 and appUrl exists
|
|
304
|
+
const isInstalled = data.vendors?.every((vendor) => vendor.progress >= 80 && vendor.appUrl);
|
|
305
|
+
if (isInstalled) {
|
|
306
|
+
return data.vendors;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return null; // Not ready yet, continue polling
|
|
310
|
+
},
|
|
311
|
+
maxAttempts: Math.ceil((TIMEOUTS.installation * 1000) / INTERVAL_MS),
|
|
312
|
+
intervalMs: INTERVAL_MS,
|
|
313
|
+
timeoutMessage: "Installation timeout - services failed to install within 5 minutes",
|
|
314
|
+
stepName: "Installation",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Wait for service to start running - Step 4
|
|
320
|
+
*/
|
|
321
|
+
async function waitServiceRunning(readyVendors) {
|
|
322
|
+
return pollWithTimeout({
|
|
323
|
+
checkCondition: async () => {
|
|
324
|
+
// Check running status of all vendors concurrently
|
|
325
|
+
const vendorChecks = readyVendors.map(async (vendor) => {
|
|
326
|
+
try {
|
|
327
|
+
const blockletInfo = await getComponentInfo(vendor.appUrl);
|
|
328
|
+
|
|
329
|
+
if (blockletInfo.status === "running") {
|
|
330
|
+
return vendor;
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
} catch (_error) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const results = await Promise.all(vendorChecks);
|
|
339
|
+
const runningVendors = results.filter((vendor) => vendor !== null);
|
|
340
|
+
|
|
341
|
+
if (runningVendors.length === readyVendors.length) {
|
|
342
|
+
return runningVendors;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return null; // Not ready yet, continue polling
|
|
346
|
+
},
|
|
347
|
+
maxAttempts: Math.ceil((TIMEOUTS.serviceStart * 1000) / INTERVAL_MS),
|
|
348
|
+
intervalMs: INTERVAL_MS,
|
|
349
|
+
timeoutMessage: "Service start timeout - services failed to start within 5 minutes",
|
|
350
|
+
stepName: "Service Start",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get final URL - Step 5
|
|
356
|
+
*/
|
|
357
|
+
async function getDashboardAndUrl(checkoutId, runningVendors) {
|
|
358
|
+
try {
|
|
359
|
+
// 5. Get order details
|
|
360
|
+
const orderDetailUrl = joinURL(
|
|
361
|
+
BASE_URL,
|
|
362
|
+
prefix,
|
|
363
|
+
API_ENDPOINTS.orderDetail.replace("{id}", checkoutId),
|
|
364
|
+
);
|
|
365
|
+
const response = await fetch(orderDetailUrl);
|
|
366
|
+
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const data = await response.json();
|
|
372
|
+
|
|
373
|
+
if (data.vendors.length === 0) {
|
|
374
|
+
throw new Error("No vendors found in order details");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Wait 3 seconds
|
|
378
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
379
|
+
|
|
380
|
+
// Return the appUrl of the first vendor (usually only one)
|
|
381
|
+
const appUrl = runningVendors[0]?.appUrl;
|
|
382
|
+
if (!appUrl) {
|
|
383
|
+
throw new Error("No app URL found in order details");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
appUrl,
|
|
388
|
+
dashboardUrl: data.vendors[0]?.dashboardUrl,
|
|
389
|
+
homeUrl: data.vendors[0]?.homeUrl,
|
|
390
|
+
token: data.vendors[0]?.token,
|
|
391
|
+
};
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error(`${chalk.red("❌")} Failed to get order details:`, error.message);
|
|
394
|
+
// If getting details fails, use the appUrl of running vendor
|
|
395
|
+
return {
|
|
396
|
+
appUrl: runningVendors[0]?.appUrl || null,
|
|
397
|
+
dashboardUrl: runningVendors[0]?.dashboardUrl || null,
|
|
398
|
+
homeUrl: runningVendors[0]?.homeUrl || null,
|
|
399
|
+
token: runningVendors[0]?.token || null,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export default deploy;
|