@blocklet/cli 1.17.12 → 1.17.13-beta-20260512-004004-69bacba8

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 CHANGED
@@ -28,7 +28,7 @@ Powered By
28
28
  / ___ \| | | (__| |_) | | (_) | (__| <
29
29
  /_/ \_\_| \___|____/|_|\___/ \___|_|\_\
30
30
 
31
- Blocklet CLI v1.17.11
31
+ Blocklet CLI v1.17.12
32
32
 
33
33
  Usage: blocklet server [options] [command]
34
34
 
@@ -52,7 +52,7 @@ const getSource = async (param, opts) => {
52
52
  const { url, name, version } = parseComponent(param);
53
53
  if (url) {
54
54
  const componentMeta = await getMeta(url);
55
- // bundleName 必须为 blocklet blocklet.yml 中的 name
55
+ // bundleName must match the 'name' field in the blocklet's blocklet.yml
56
56
  return { source: { url }, bundleName: componentMeta.name, bundleTitle: componentMeta.title || '' };
57
57
  }
58
58
 
@@ -68,7 +68,7 @@ const getSource = async (param, opts) => {
68
68
 
69
69
  const bundleMetaUrl = getMetaUrl(store, name, version);
70
70
  const componentMeta = await getMeta(bundleMetaUrl);
71
- // NOTICE: 支持传入 blocklet-did 来进行安装
71
+ // NOTICE: Installing by blocklet DID is also supported
72
72
  if (componentMeta.name !== name && componentMeta.did !== name) {
73
73
  printError(`Failed validate component meta: invalid name. Expected ${name}, Got: ${componentMeta.name}`);
74
74
  process.exit(1);
@@ -61,7 +61,7 @@ const createBlockletBundle = async ({
61
61
  compact = false,
62
62
  dependenciesDepth = 9,
63
63
  }) => {
64
- // 新增 meta.files 支持 glob 后,需要将 glob 匹配的文件也进行复制放入 bundle
64
+ // After adding glob support to meta.files, all glob-matched files must also be copied into the bundle
65
65
  const metaFileList = meta.files || [];
66
66
  const distDir = path.join(blockletDir, BLOCKLET_BUNDLE_FOLDER);
67
67
 
@@ -161,7 +161,7 @@ const createBlockletBundle = async ({
161
161
  };
162
162
 
163
163
  /**
164
- * 获取所有 extraFiles,理论上所有 .js 都需要查出来,才能进一步去找到依赖
164
+ * Get all extraFiles. In theory, all .js files must be found here in order to subsequently resolve their dependencies.
165
165
  * @param {Object} meta blocklet meta
166
166
  * @returns Array
167
167
  */
@@ -11,7 +11,7 @@ class BlockletMdBundler extends MarkdownBundler {
11
11
  * Creates an instance of BlockletMdBundler.
12
12
  * @param {{
13
13
  * blockletDir: string,
14
- * blockletMdFileName: string, // 约定: blocklet.md的文件名是小写的
14
+ * blockletMdFileName: string, // Convention: the blocklet.md filename must be lowercase
15
15
  * backupMdFileNames?: string[];
16
16
  * required?: boolean = false;
17
17
  * duplicateMdFileName?: string;
@@ -72,16 +72,16 @@ class BlockletMdBundler extends MarkdownBundler {
72
72
 
73
73
  _findByBackupFileNames() {
74
74
  for (const backupMdFileName of this.backupMdFileNames) {
75
- // 我们先来找找备份文件
75
+ // Try to find a backup file
76
76
  const foundByBackupMdFileName = fg.sync(backupMdFileName, this.fastGlobOptions);
77
77
 
78
78
  if (foundByBackupMdFileName.length === 1) {
79
- // 找到了1个备份的文件
79
+ // Exactly one backup file found
80
80
  return foundByBackupMdFileName;
81
81
  }
82
82
 
83
83
  if (foundByBackupMdFileName.length > 1) {
84
- // 备份文件太多了,我不知道选哪个好
84
+ // Multiple backup files found — ambiguous, cannot determine which one to use
85
85
  printError(
86
86
  `Only one ${chalk.red(backupMdFileName)}(not case sensitive) can exist in ${chalk.cyan(this.blockletDir)}`
87
87
  );
@@ -90,7 +90,7 @@ class BlockletMdBundler extends MarkdownBundler {
90
90
  }
91
91
 
92
92
  if (this.required) {
93
- // 连备份文件也没找到,但是文件还是必要的,不能没有啊,故中断当前任务
93
+ // No backup file found either, but this file is required — abort the current task
94
94
  printError(
95
95
  `Either ${chalk.red(this.blockletMdFileName)} or ${this.backupMdFileNames
96
96
  .map((backupMdFileName) => chalk.red(backupMdFileName))
@@ -106,7 +106,7 @@ class BlockletMdBundler extends MarkdownBundler {
106
106
  const foundByDuplicateMdFileName = fg.sync(this.duplicateMdFileName, this.fastGlobOptions);
107
107
 
108
108
  if (foundByDuplicateMdFileName.length) {
109
- // 我是故意这么换行的,不然warning信息和其他信息会显示在同一行,不太美观
109
+ // The blank line is intentional: without it, the warning would appear on the same line as other output
110
110
  print();
111
111
  printWarning(
112
112
  `File ${chalk.cyan(this.blockletMdFileName)} is currently in use, file ${chalk.yellow(
@@ -81,7 +81,7 @@ class ChangelogBundler {
81
81
  const ast = fromMarkdown(fs.readFileSync(changelogAbsolutePath));
82
82
  const children = ast?.children || [];
83
83
  children.forEach((element) => {
84
- // 只支持 h1~h4
84
+ // Only h1h4 headings are supported
85
85
  if (element.type === 'heading' && element.depth > 4) {
86
86
  const line = element.position?.start.line || 0;
87
87
  printError(`Just h1~h4 headings should be used in ${chalk.cyan(changelogAbsolutePath)} at line ${line}`);
@@ -48,7 +48,7 @@ class MarkdownBundler {
48
48
 
49
49
  /**
50
50
  *
51
- * @description 从本地所有的blocklet.md提取本地静态文件到当前目录的media文件夹下面
51
+ * @description Extract local static assets from all blocklet.md files into the bundle's media folder
52
52
  * @param {*} blockletDir
53
53
  */
54
54
  async _bundle() {
@@ -63,7 +63,7 @@ class MarkdownBundler {
63
63
  const referenceFile = astNode?.url || astNode?.attrs?.src || astNode?.attrs?.href;
64
64
  const localUrl = this._getLocalUrl(referenceFile);
65
65
 
66
- // FIXME: 后续jest mock做一个单测吧
66
+ // FIXME: Add a unit test for this using jest mock later
67
67
  if (!fs.existsSync(localUrl)) {
68
68
  printError(
69
69
  `Referenced file ${chalk.red(referenceFile)} not found when bundling ${chalk.cyan(
@@ -73,7 +73,7 @@ class MarkdownBundler {
73
73
  process.exit(1);
74
74
  }
75
75
 
76
- // 如果路径指向了一个文件夹,应该通过报错告知用户,文件夹不能存在于markdown文件当中
76
+ // If the referenced path is a directory, report an error — directories are not valid in markdown references
77
77
  if (fs.statSync(localUrl).isDirectory()) {
78
78
  printError(
79
79
  `Only files links are allowed in ${chalk.cyan(
@@ -109,12 +109,13 @@ class MarkdownBundler {
109
109
  // eslint-disable-next-line no-await-in-loop
110
110
  await this.writeFile(path.join(this.bundleDir, path.basename(markdownAbsolutePath)), toMarkdown(mdAstNode));
111
111
  } else {
112
- // 说明有些资源还没有计算好hash值和复制到bundle下,blocklet.md没处理完,当然也就不能直接修改blocklet.md里面的引用了
112
+ // Some assets have not yet had their hash computed or been copied into the bundle,
113
+ // so blocklet.md is not fully processed and its asset references cannot be rewritten yet.
113
114
  // eslint-disable-next-line no-await-in-loop
114
115
  await this._batchHash(cache);
115
116
  // eslint-disable-next-line no-await-in-loop
116
117
  await this._batchCopy(cache);
117
- // 还没处理完成,等到下一轮再处理即可
118
+ // Not finished yet — push back for reprocessing in the next iteration
118
119
  markdownAbsolutePaths.push(markdownAbsolutePath);
119
120
  }
120
121
  }
@@ -76,7 +76,7 @@ class ScreenshotsBundler {
76
76
  }
77
77
 
78
78
  /**
79
- * @description 清理 screenshots bundle 的文件夹
79
+ * @description Clean up the screenshots bundle directory
80
80
  * @memberof ScreenshotsBundler
81
81
  */
82
82
  async _clean() {
@@ -62,7 +62,7 @@ exports.run = async ({
62
62
  externals: inputExternals = [],
63
63
  dependenciesDepth = 9,
64
64
  }) => {
65
- // 合并所有外部依赖
65
+ // Merge all external dependencies (deduplicated)
66
66
  const externals = Array.from(new Set([...defaultExternals, ...inputExternals]));
67
67
 
68
68
  // eslint-disable-next-line no-param-reassign
@@ -17,7 +17,7 @@ const whiteList = ['store', 'accessToken', 'registry', 'developerDid', 'name', '
17
17
  function validate(key, value) {
18
18
  const validatorMap = {
19
19
  store: (v) => {
20
- // 校验是否为合法的url地址(必须是 http https 协议),例如:https://store.blocklet.dev/
20
+ // Validate that the value is a valid URL using http or https, e.g. https://store.blocklet.dev/
21
21
  if (/^((https?):\/\/)[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?$/.test(v)) {
22
22
  return true;
23
23
  }
@@ -12,7 +12,8 @@ const { wrapSpinner } = require('../../ui');
12
12
 
13
13
  /**
14
14
  *
15
- * @description 确认是否要覆盖之前填写的store url,回车或输入y直接覆盖,输入其他字符就不覆盖
15
+ * @description Prompt the user to confirm whether to overwrite the existing store URL.
16
+ * Pressing Enter or 'y' overwrites; any other input cancels.
16
17
  * @see https://www.npmjs.com/package/inquirer
17
18
  * @see https://github.com/SBoudrias/Inquirer.js/blob/0053e3f5694a4f75c4901512ab87e8906d1d7896/packages/inquirer/examples/pizza.js
18
19
  * @default false
@@ -32,7 +33,7 @@ async function confirmOverwriteStoreUrl() {
32
33
  }
33
34
 
34
35
  const run = async (store, { profile }) => {
35
- // 校验是否为合法的url地址(必须是 http https 协议),例如:https://store.blocklet.dev/
36
+ // Validate that the URL is a valid web URI (must use http or https)
36
37
  if (!validUrl.isWebUri(store)) {
37
38
  printError('Invalid store url:', store);
38
39
  process.exit(1);
@@ -41,14 +42,14 @@ const run = async (store, { profile }) => {
41
42
  try {
42
43
  const storeUrl = await getStoreUrl(store);
43
44
 
44
- // 读取配置
45
+ // Load configuration
45
46
  const config = new Config({
46
47
  configFile: process.env.ABT_NODE_CONFIG_FILE,
47
48
  section: profile === 'default' ? '' : profile,
48
49
  });
49
50
 
50
51
  const oldStoreUrl = config.get('store');
51
- // 配置过store url,前后两次配置不一样,且选择不覆盖配置时,退出程序
52
+ // If a store URL was already configured, it differs from the new one, and the user declined to overwrite, exit
52
53
  if (!isUndefined(oldStoreUrl) && oldStoreUrl !== storeUrl && !(await confirmOverwriteStoreUrl())) {
53
54
  process.exit(0);
54
55
  }
@@ -53,7 +53,7 @@ function getFilename(input) {
53
53
  // ignore invalid URL parsing
54
54
  }
55
55
 
56
- // 处理本地路径或文件名
56
+ // Handle local path or filename — extract the base filename
57
57
  return path.basename(input);
58
58
  }
59
59
 
@@ -115,7 +115,8 @@ const getAccessibleUrl = async (urls) => {
115
115
 
116
116
  const ping = async (url) => {
117
117
  try {
118
- // FIXME: 如果通过 ABT_NODE_HOST 指定了 Host IP 地址,这里通过 HTTP 服务检测有问题,所以暂时直接返回 true, 绕过检测
118
+ // FIXME: When a Host IP is specified via ABT_NODE_HOST, HTTP-based reachability checks fail,
119
+ // so temporarily return true to bypass the check when running inside Docker.
119
120
  if (isDocker()) {
120
121
  return true;
121
122
  }
@@ -136,7 +137,7 @@ const getAccessibleUrl = async (urls) => {
136
137
 
137
138
  let resultUrl = urls.find((url) => isDidDomain(url));
138
139
  if (!resultUrl) {
139
- [resultUrl] = urls; // 假设没有可访问的地址,直接返回第一个
140
+ [resultUrl] = urls; // No accessible URL found; fall back to the first one
140
141
  }
141
142
 
142
143
  print('');
@@ -192,7 +193,7 @@ const getBlockletMetaWithTimeout = (urls, options, timeout = 5000) => {
192
193
  };
193
194
 
194
195
  const findMountPointConflicts = async (meta, existedApp) => {
195
- // 收集待安装的组件,用于检测组件间 mountPoint 是否冲突
196
+ // Collect components to be installed, to detect mountPoint conflicts between them
196
197
  let components = meta.components || [];
197
198
  if (isEmpty(components)) {
198
199
  const { title, name, did } = meta;
@@ -202,7 +203,7 @@ const findMountPointConflicts = async (meta, existedApp) => {
202
203
  title: title || name || did,
203
204
  });
204
205
  }
205
- // 已安装的组件
206
+ // Already-installed components
206
207
  components.unshift(
207
208
  ...(existedApp.children?.map((v) => ({
208
209
  mountPoint: v.mountPoint,
@@ -210,9 +211,9 @@ const findMountPointConflicts = async (meta, existedApp) => {
210
211
  title: v.meta.title || v.meta.name || v.meta.did,
211
212
  })) || [])
212
213
  );
213
- // 忽略 mountPoint 缺失的组件
214
+ // Ignore components that have no mountPoint defined
214
215
  components = components.filter((x) => Boolean(x.mountPoint));
215
- // 查询待安装组件的 did
216
+ // Resolve the DID for each component that is pending installation
216
217
  await pMap(
217
218
  components.filter((v) => !v.did),
218
219
  async (config) => {
@@ -227,12 +228,12 @@ const findMountPointConflicts = async (meta, existedApp) => {
227
228
  }
228
229
  }
229
230
  );
230
- // 忽略 did 缺失的组件
231
+ // Ignore components whose DID could not be resolved
231
232
  components = components.filter((x) => Boolean(x.did));
232
233
 
233
234
  const conflictMsgs = [];
234
235
  components.reduce((acc, child) => {
235
- // 不同的组件挂载到了同一个 mountPoint
236
+ // Two different components are attempting to mount at the same mountPoint
236
237
  if (acc[child.mountPoint] && acc[child.mountPoint].did !== child.did) {
237
238
  conflictMsgs.push(
238
239
  `mountPoint conflict: '${child.title}' attempted to mount on '${child.mountPoint}', but it is already occupied by '${acc[child.mountPoint].title}'`
@@ -35,7 +35,7 @@ const run = async (script, { appId: inputAppId, timeout } = {}) => {
35
35
  appId = process.env.BLOCKLET_DEV_APP_DID;
36
36
  }
37
37
 
38
- // 确认 appId 是否有效
38
+ // Validate appId
39
39
  if (appId) {
40
40
  if (isValidDid(appId) === false) {
41
41
  printError(`${chalk.yellow('--app-id')} is not valid: ${appId}`);
@@ -48,7 +48,7 @@ const run = async (script, { appId: inputAppId, timeout } = {}) => {
48
48
 
49
49
  printInfo(`Try to run script for blocklet(${chalk.yellow(appId)}) (timeout: ${timeoutMs}ms)`);
50
50
 
51
- // 确认 script 是否存在
51
+ // Check that the script file exists
52
52
  const scriptPath = path.join(appDir, script);
53
53
  if (fs.existsSync(scriptPath) === false) {
54
54
  printError(`Script ${chalk.cyan(scriptPath)} does not exist`);
@@ -56,7 +56,7 @@ const run = async (script, { appId: inputAppId, timeout } = {}) => {
56
56
  }
57
57
 
58
58
  try {
59
- // 尝试从 blocklet.yml 中获取 meta 信息
59
+ // Try to read meta information from blocklet.yml
60
60
  if (fs.existsSync(path.join(appDir, 'blocklet.yml'))) {
61
61
  const meta = getBlockletMeta(appDir, { fix: false });
62
62
  componentDid = meta?.did;
@@ -92,7 +92,7 @@ const run = async (script, { appId: inputAppId, timeout } = {}) => {
92
92
  blocklet = await node.getBlocklet({ did: rootDid });
93
93
  const nodeEnvironments = await node.states.node.getEnvironments();
94
94
 
95
- // 如果 componentDid 存在,则运行 component 的脚本
95
+ // If componentDid is present, run the script for that component
96
96
  if (componentDid) {
97
97
  component = blocklet.children.find((x) => x.meta.did === componentDid);
98
98
 
@@ -0,0 +1,251 @@
1
+ const chalk = require('chalk');
2
+ const inquirer = require('inquirer');
3
+ const { fromRandom, fromSecretKey } = require('@ocap/wallet');
4
+ const { isValid: isValidDid } = require('@arcblock/did');
5
+ const { getBlockletInfo } = require('@blocklet/meta/lib/info');
6
+ const { getBlockletChainInfo } = require('@blocklet/meta/lib/util');
7
+ const { getChainClient } = require('@abtnode/util/lib/get-chain-client');
8
+ const { MAIN_CHAIN_ENDPOINT } = require('@abtnode/constant');
9
+
10
+ const { printError, printInfo, printSuccess, printWarning, getCLIBinaryName } = require('../../util');
11
+ const { getNode } = require('../../node');
12
+ const { checkRunning } = require('../../manager');
13
+
14
+ const sleep = (ms) =>
15
+ new Promise((r) => {
16
+ setTimeout(r, ms);
17
+ });
18
+
19
+ const checkNodeRunning = async () => {
20
+ const isRunning = await checkRunning();
21
+ if (!isRunning) {
22
+ const startCommand = chalk.cyan(`${getCLIBinaryName()} server start`);
23
+ printError('Blocklet Server is not running, can not execute anything!');
24
+ printInfo(`To start Blocklet Server, use ${startCommand}`);
25
+ process.exit(1);
26
+ }
27
+ };
28
+
29
+ const waitForTx = async (client, hash, timeoutSec) => {
30
+ const deadline = Date.now() + timeoutSec * 1000;
31
+ let lastErr;
32
+ // Sequential polling is the intent here — every iteration must observe the
33
+ // chain state produced by the previous one's sleep, so parallelizing is
34
+ // semantically wrong. Disable the lint rule that bans await-in-loop.
35
+ /* eslint-disable no-await-in-loop */
36
+ while (Date.now() < deadline) {
37
+ try {
38
+ const { info } = await client.getTx({ hash });
39
+ if (info && info.code === 'OK') return;
40
+ if (info && info.code && info.code !== 'OK') {
41
+ throw new Error(`tx ${hash} rejected with code ${info.code}`);
42
+ }
43
+ } catch (err) {
44
+ lastErr = err;
45
+ }
46
+ await sleep(1000);
47
+ }
48
+ /* eslint-enable no-await-in-loop */
49
+ throw new Error(`timed out waiting for tx ${hash} (${timeoutSec}s)${lastErr ? ` — ${lastErr.message}` : ''}`);
50
+ };
51
+
52
+ const run = async ({
53
+ appDid = '',
54
+ newSk = '',
55
+ genNew = false,
56
+ yes = false,
57
+ skipConfirm = false,
58
+ dryRun = false,
59
+ skipChainMigrate = false,
60
+ skipConfigUpdate = false,
61
+ confirmTimeout = '60',
62
+ } = {}) => {
63
+ // Either subcommand `--skip-confirm` or global `-y/--yes` suppresses the
64
+ // prompt. Both end up in the merged options bag via parseOptions.
65
+ const noPrompt = !!(skipConfirm || yes);
66
+ try {
67
+ await checkNodeRunning();
68
+
69
+ if (!appDid || isValidDid(appDid) === false) {
70
+ printError(`--app-did is not a valid DID: ${appDid}`);
71
+ process.exit(1);
72
+ return;
73
+ }
74
+
75
+ if (!newSk && !genNew) {
76
+ printError('Either --new-sk <hex> or --gen-new must be provided');
77
+ process.exit(1);
78
+ return;
79
+ }
80
+ if (newSk && genNew) {
81
+ printError('--new-sk and --gen-new are mutually exclusive');
82
+ process.exit(1);
83
+ return;
84
+ }
85
+ if (skipChainMigrate && skipConfigUpdate) {
86
+ printError('--skip-chain-migrate and --skip-config-update both set — nothing to do');
87
+ process.exit(1);
88
+ return;
89
+ }
90
+
91
+ const timeoutSec = Number(confirmTimeout);
92
+ if (!Number.isFinite(timeoutSec) || timeoutSec <= 0) {
93
+ printError(`--confirm-timeout must be a positive number, got ${confirmTimeout}`);
94
+ process.exit(1);
95
+ return;
96
+ }
97
+
98
+ const { node } = await getNode();
99
+ node.onReady(async () => {
100
+ try {
101
+ const blocklet = await node.getBlocklet({ did: appDid });
102
+ if (!blocklet) {
103
+ printError(`Blocklet ${appDid} not found on this server`);
104
+ process.exit(1);
105
+ return;
106
+ }
107
+
108
+ const nodeInfo = await node.getNodeInfo();
109
+ const { wallet: oldWallet } = getBlockletInfo(blocklet, nodeInfo.sk);
110
+ if (!oldWallet || !oldWallet.secretKey) {
111
+ printError('Cannot resolve old wallet — is BLOCKLET_APP_SK configured for this blocklet?');
112
+ process.exit(1);
113
+ return;
114
+ }
115
+
116
+ // Generate or accept new sk, keeping the old wallet's type so the
117
+ // address derivation stays in the same scheme (arcblock vs ethereum).
118
+ let resolvedNewSk = newSk;
119
+ if (genNew) {
120
+ const newW = fromRandom(oldWallet.type);
121
+ resolvedNewSk = newW.secretKey;
122
+ }
123
+ let newWallet;
124
+ try {
125
+ newWallet = fromSecretKey(resolvedNewSk, oldWallet.type);
126
+ } catch (err) {
127
+ printError(`Cannot derive new wallet from sk: ${err.message}`);
128
+ process.exit(1);
129
+ return;
130
+ }
131
+
132
+ if (newWallet.address === oldWallet.address) {
133
+ printError(`New sk derives the same address as the old one (${oldWallet.address}); refuse no-op`);
134
+ process.exit(1);
135
+ return;
136
+ }
137
+
138
+ // Resolve chain endpoint (same logic auth/lib/server.js#migrateAppDid uses).
139
+ let chainHost = getBlockletChainInfo(blocklet).host;
140
+ if (!chainHost || chainHost === 'none') chainHost = MAIN_CHAIN_ENDPOINT;
141
+
142
+ // Show plan.
143
+ printInfo(`Rotation plan for blocklet ${chalk.cyan(blocklet.meta?.title || appDid)}`);
144
+ printInfo(` appDid (current): ${oldWallet.address}`);
145
+ printInfo(` appPid (meta DID): ${blocklet.appPid}`);
146
+ printInfo(` new appDid: ${newWallet.address}`);
147
+ printInfo(` chain endpoint: ${chainHost}`);
148
+ printInfo(' steps:');
149
+ printInfo(` 1) chain migrate: ${skipChainMigrate ? chalk.gray('SKIP') : chalk.green('YES')}`);
150
+ printInfo(` 2) local config: ${skipConfigUpdate ? chalk.gray('SKIP') : chalk.green('YES')}`);
151
+ printInfo(` 3) restart blocklet: ${chalk.yellow('MANUAL after success')}`);
152
+
153
+ if (genNew) {
154
+ // Print the generated sk early — caller must save it before the
155
+ // command exits.
156
+ printWarning('Generated new sk (SAVE IT NOW; will not be shown again):');
157
+ printWarning(` ${chalk.yellow(resolvedNewSk)}`);
158
+ }
159
+
160
+ if (dryRun) {
161
+ printSuccess('dry run — no changes made');
162
+ process.exit(0);
163
+ return;
164
+ }
165
+
166
+ if (!noPrompt) {
167
+ const { confirmed } = await inquirer.prompt([
168
+ {
169
+ type: 'confirm',
170
+ name: 'confirmed',
171
+ message: 'This rotates the blocklet appSk irreversibly on chain. Proceed?',
172
+ default: false,
173
+ },
174
+ ]);
175
+ if (!confirmed) {
176
+ printInfo('aborted by user');
177
+ process.exit(0);
178
+ return;
179
+ }
180
+ }
181
+
182
+ // Step 1: on-chain AccountMigrateTx.
183
+ if (!skipChainMigrate) {
184
+ const client = getChainClient(chainHost);
185
+
186
+ const { state: newOnChain } = await client.getAccountState({
187
+ address: newWallet.address,
188
+ traceMigration: false,
189
+ });
190
+ if (newOnChain) {
191
+ printError(`new account ${newWallet.address} already exists on chain — abort`);
192
+ process.exit(1);
193
+ return;
194
+ }
195
+ const { state: oldOnChain } = await client.getAccountState({
196
+ address: oldWallet.address,
197
+ traceMigration: false,
198
+ });
199
+ if (oldOnChain && oldOnChain.migratedTo && oldOnChain.migratedTo.length > 0) {
200
+ printError(
201
+ `old account ${oldWallet.address} already migrated to ${JSON.stringify(
202
+ oldOnChain.migratedTo
203
+ )} — refuse to migrate again`
204
+ );
205
+ process.exit(1);
206
+ return;
207
+ }
208
+
209
+ printInfo('sending AccountMigrateTx ...');
210
+ const hash = await client.migrateAccount({ from: oldWallet, to: newWallet });
211
+ printInfo(` tx hash: ${chalk.cyan(hash)}`);
212
+ printInfo(` waiting up to ${timeoutSec}s for confirmation ...`);
213
+ await waitForTx(client, hash, timeoutSec);
214
+ printSuccess('chain migration confirmed');
215
+ }
216
+
217
+ // Step 2: write BLOCKLET_APP_SK back to blocklet config.
218
+ if (!skipConfigUpdate) {
219
+ printInfo('updating BLOCKLET_APP_SK in blocklet config ...');
220
+ // NOTE: must use blocklet.appPid (= meta DID), NOT appDid — when sk
221
+ // changes, the appDid moves into alsoKnownAs and lookups by old
222
+ // appDid would fail. See env-config-manager.js:110-112.
223
+ await node.configBlocklet({
224
+ did: blocklet.appPid,
225
+ configs: [{ key: 'BLOCKLET_APP_SK', value: resolvedNewSk, secure: true, shared: false }],
226
+ skipHook: true,
227
+ skipDidDocument: false,
228
+ });
229
+ printSuccess('BLOCKLET_APP_SK updated. Blocklet must be restarted to pick up the new key.');
230
+ }
231
+
232
+ printSuccess('Rotation complete.');
233
+ printWarning(
234
+ 'This command did NOT sync passport VC / owner.connectedAccount. ' +
235
+ 'If this blocklet has user-facing auth, follow up via the server UI ' +
236
+ '("Transfer App Owner" / "Rotate Key Pair") or run the corresponding ' +
237
+ 'node APIs manually (see receive-transfer-app-owner.js:317+).'
238
+ );
239
+ process.exit(0);
240
+ } catch (err) {
241
+ printError(`Rotation failed: ${err.message}`);
242
+ process.exit(1);
243
+ }
244
+ });
245
+ } catch (err) {
246
+ printError(err.message);
247
+ process.exit(1);
248
+ }
249
+ };
250
+
251
+ exports.run = run;
@@ -40,7 +40,7 @@ exports.run = async (originalMetaFile, { accessToken, profile }) => {
40
40
  realAccessToken = config.get('accessToken');
41
41
  debug('read access token from config');
42
42
  if (!realAccessToken) {
43
- // TODO: 添加 accessToken 不存在情况下自动去获取 accessToken 的逻辑
43
+ // TODO: Add logic to automatically obtain accessToken when it is missing
44
44
  printError('accessToken is required to upload a blocklet');
45
45
  process.exit(1);
46
46
  }
@@ -48,6 +48,7 @@ const create = require('./blocklet/create');
48
48
  const connect = require('./blocklet/connect');
49
49
  const exec = require('./blocklet/exec');
50
50
  const $debug = require('./blocklet/debug');
51
+ const rotate = require('./blocklet/rotate');
51
52
  const add = require('./blocklet/add');
52
53
  const remove = require('./blocklet/remove');
53
54
  const cleanup = require('./blocklet/cleanup');
@@ -261,6 +262,22 @@ program
261
262
  .description('Setting debug environment for blocklet')
262
263
  .action(parseArgsAndOptions($debug.run));
263
264
 
265
+ program
266
+ .command('rotate')
267
+ .requiredOption('--app-did <did>', 'The appDid of the blocklet to rotate')
268
+ .option('--new-sk <hex>', 'New secret key in hex (e.g. 0x... or 64-byte hex)')
269
+ .option('--gen-new', 'Auto-generate a new random sk; prints the value once', false)
270
+ .option('--skip-chain-migrate', 'Skip on-chain AccountMigrateTx (local config only)', false)
271
+ .option('--skip-config-update', 'Skip local BLOCKLET_APP_SK update (chain migrate only)', false)
272
+ .option('--confirm-timeout <sec>', 'Seconds to wait for chain confirmation', '60')
273
+ .option('--skip-confirm', 'Skip interactive confirmation prompt (also honored via global -y/--yes)', false)
274
+ .option('--dry-run', 'Print plan only, do not execute', false)
275
+ .description(
276
+ 'Rotate the BLOCKLET_APP_SK of a blocklet (on-chain AccountMigrate + local config update). ' +
277
+ 'Does NOT sync passport VC / connectedAccount — use server UI for user-facing apps.'
278
+ )
279
+ .action(parseOptions(rotate.run));
280
+
264
281
  program
265
282
  .command('connect <store-url>')
266
283
  .option('--profile <profile>', 'Your config profile', 'default')
@@ -34,7 +34,7 @@ module.exports = (parentCommand = '') => {
34
34
  async (...args) => {
35
35
  printVersionTip();
36
36
 
37
- // @note: 这里的 yes 我始终无法通过 commander.js 获取,所以做一下特殊处理
37
+ // @note: The --yes flag cannot be reliably read via commander.js, so it is parsed directly from process.argv
38
38
  const yes = process.argv.includes('--yes') || process.argv.includes('-y');
39
39
  Object.assign(args[0], { yes });
40
40
 
@@ -15,7 +15,7 @@ const getDockerStatusLog = require('../../util/docker-status-log');
15
15
  exports.run = ({ clipboard }) => {
16
16
  // Clipboard is not accessible when on a linux tty
17
17
  const copyToClipboard = process.platform === 'linux' && !process.env.DISPLAY ? false : clipboard;
18
- // 收集需要复制到剪贴板的 printInfo 信息
18
+ // Accumulate printInfo output that will be copied to the clipboard
19
19
  const toPlainText = (str) => stripAnsi(str);
20
20
  let clipboardBuffer = `${getVersionInfo()}\n`;
21
21
  const bufferedPrintInfo = (...args) => {
@@ -96,7 +96,6 @@ exports.run = ({ clipboard }) => {
96
96
  ? `${canUseFileSystemIsolation ? chalk.green('on(available)') : chalk.yellow('on(unavailable, version must be >= 21.6.0)')}`
97
97
  : chalk.red('off')
98
98
  );
99
- // 判断是否是mac
100
99
 
101
100
  getDockerStatusLog(bufferedPrintInfo, nodeInfo);
102
101
  bufferedPrintInfo('Server domain status:', correct ? chalk.green('correct') : chalk.red('mismatch'));
@@ -330,15 +330,15 @@ function ensureNginxServiceNotManaged() {
330
330
  }
331
331
 
332
332
  function setupNginxEnvironment() {
333
- // 确保 nginx 安装
333
+ // Ensure nginx is installed
334
334
  installNginxIfNeeded();
335
- // 确保 nginx stream 模块
335
+ // Ensure nginx has the stream module
336
336
  ensureNginxStreamModule();
337
- // 确保 nginx 有低端口权限
337
+ // Ensure nginx has permission to bind low-numbered ports
338
338
  configureNginxPortCapabilities();
339
- // 确保 nginx 不被系统服务管理
339
+ // Ensure nginx is not managed by the system service manager
340
340
  ensureNginxServiceNotManaged();
341
- // 确保 nginx 端口配置正确
341
+ // Ensure nginx port configuration is correct
342
342
  checkBlockletServerConfig();
343
343
 
344
344
  print();
@@ -85,7 +85,7 @@ const ABT_NODE_BINARY_NAME = getCLIBinaryName();
85
85
  const ABT_NODE_COMMAND_NAME = getCLICommandName();
86
86
 
87
87
  const parseMemoryLimit = () => {
88
- // 默认读取 30% 的内存,最小限制在 800,最大 4096 MB
88
+ // Default to 30% of total memory; minimum 800 MB, maximum 4096 MB
89
89
  const totalMemInMB = os.totalmem() / (1024 * 1024);
90
90
  const memoryPercent = totalMemInMB * 0.3;
91
91
  const calculatedLimit = Math.floor(Math.max(Math.min(memoryPercent, 4096), DAEMON_MAX_MEM_LIMIT_IN_MB));
@@ -425,7 +425,7 @@ const prepareInitialData = async (node, config, dataDir) => {
425
425
  await fs.remove(dataDirBak);
426
426
  } catch (error) {
427
427
  await fs.move(dataDirBak, dataDir, { overwrite: true });
428
- // FIXME: 这里可能会因错误导致节点无法启动, 进而导致节点在 ec2 上被无限重启
428
+ // FIXME: An error here can prevent the node from starting, which may cause it to restart infinitely on EC2
429
429
  throw error;
430
430
  }
431
431
  };
@@ -457,8 +457,8 @@ const exec = async ({ workingDir, config, dataDir, mode, updateDb, forceIntranet
457
457
 
458
458
  const blockletMaxMemoryLimit = get(config, 'node.runtimeConfig.blockletMaxMemoryLimit', BLOCKLET_MAX_MEM_LIMIT_IN_MB);
459
459
 
460
- // 如果日志目录不存在,pm2 会启动可能会失败, 测试发现 docker 里的 Ubuntu 18.04.4 LTS 系统会失败,
461
- // MAC OS 下正常,但是没搞明白为什么
460
+ // Ensure the log directory exists before starting pm2; without it pm2 may fail to start.
461
+ // Observed failure on Ubuntu 18.04.4 LTS inside Docker; macOS is unaffected — root cause unknown.
462
462
  const logDir = process.env.ABT_NODE_LOG_DIR || getDaemonLogDir(dataDir);
463
463
  fs.ensureDirSync(logDir);
464
464
 
@@ -792,7 +792,7 @@ const createStartLock = ({ lockFile, pid }) => fs.writeFileSync(lockFile, pid.to
792
792
 
793
793
  const clearStartLock = ({ pid, lockFile, enforceLock }) => {
794
794
  try {
795
- // 如果使用了 autoInit, 只要存在 lock 文件则直接删除改文件
795
+ // If autoInit is enabled, delete the lock file whenever it exists
796
796
  if (!enforceLock && fs.existsSync(lockFile)) {
797
797
  fs.removeSync(lockFile);
798
798
  return;
package/lib/node.js CHANGED
@@ -54,7 +54,7 @@ const readNodeConfig = async (dir) => {
54
54
  }
55
55
  } else {
56
56
  printConfigError();
57
- process.exit(1); // TODO: 太讨厌这种提前 exit 的函数了,非常不好调试, polunzh
57
+ process.exit(1); // TODO: Hate early-exit functions like this — extremely hard to debug
58
58
  }
59
59
 
60
60
  const configFile = await getConfigFile(dir);
@@ -102,11 +102,11 @@ node.onReady(async () => {
102
102
  try {
103
103
  const initialized = await node.isInitialized();
104
104
  if (initialized) {
105
- // 无论之前是否 crash, 当启动 server (非热重启) 时,永远重启之前运行中的组件
105
+ // Regardless of whether the server previously crashed, always restart previously running components on server start (non-hot-reload)
106
106
  await node.updateNodeStatus(SERVER_STATUS.RUNNING);
107
107
  if (runningBefore) {
108
108
  logger.info('> Blocklet Server is restarted from running before');
109
- // 如果之前是运行中, 直接跳过快速重启检查
109
+ // If the server was running before, skip the rapid-restart check
110
110
  ensureBlockletRunning.whenCycleCheck = true;
111
111
  } else {
112
112
  logger.info('> Blocklet Server is stopped from running before');
@@ -37,7 +37,7 @@ class Config {
37
37
  }
38
38
 
39
39
  get(key, defaultValue) {
40
- // TODO: 是否需要保护一些关键信息(例如 secretKey
40
+ // TODO: Consider whether sensitive fields (e.g. secretKey) should be protected/masked here
41
41
  return this._get(key, defaultValue);
42
42
  }
43
43
 
@@ -6,7 +6,7 @@ const { stopDockerRedis } = require('@abtnode/core/lib/util/docker/ensure-docker
6
6
  const { CONFIG_FOLDER_NAME, CONFIG_FOLDER_NAME_OLD } = require('@abtnode/constant');
7
7
 
8
8
  const clearAllCache = async (printSuccess, printError) => {
9
- // 停止 docker 中的 redis 容器
9
+ // Stop the Redis container in Docker
10
10
  await stopDockerRedis();
11
11
  try {
12
12
  let nowConfigFolder = '';
@@ -27,7 +27,7 @@ const clearAllCache = async (printSuccess, printError) => {
27
27
  forceType: 'sqlite',
28
28
  }));
29
29
 
30
- // 清理 sqlite adapter 的缓存
30
+ // Flush the SQLite adapter cache
31
31
  await cache.flushAll();
32
32
  }
33
33
  } catch (error) {
package/lib/util/index.js CHANGED
@@ -237,7 +237,8 @@ const startEventHub = async (logDir, maxMemoryRestart = BLOCKLET_MAX_MEM_LIMIT_I
237
237
  try {
238
238
  await wrapSpinner('Starting event hub...', async () => {
239
239
  await pm2.startAsync({
240
- // 因为 pm2 bug, 子进程会继承 namespace 和众多参数, 所以这里的 namespace 会给 blocklets
240
+ // Due to a pm2 bug, child processes inherit the namespace and many other parameters,
241
+ // so the namespace here is intentionally set to 'blocklets' to be inherited by blocklet processes.
241
242
  namespace: 'blocklets',
242
243
  name: PROCESS_NAME_EVENT_HUB,
243
244
  script: './lib/process/event-hub.js',
@@ -442,7 +443,7 @@ const isDaemonIpAccessible = async ({ ip, port, adminPath }) => {
442
443
  }
443
444
  };
444
445
 
445
- // 重试 2 次,总共 ping 3 次
446
+ // Retry 2 times, 3 total ping attempts
446
447
  await pRetry(ping, { retries: 2 });
447
448
  return true;
448
449
  } catch (error) {
@@ -459,7 +460,7 @@ const getAccessibleIps = async (info, forceIntranet = false) => {
459
460
  const port = getDaemonPort(nodeHttpPort, DEFAULT_HTTP_PORT);
460
461
  const result = { internal: ips.internal };
461
462
 
462
- // 云环境下下完全相信获取到的 IP 信息
463
+ // In a cloud environment, fully trust the retrieved IP information
463
464
  if ((await cloud.isInCloud()) === true) {
464
465
  result.external = ips.external;
465
466
  return result;
@@ -546,7 +547,7 @@ const getDaemonAccessUrls = async ({ info, wallet, getBaseUrls, forceIntranet })
546
547
  );
547
548
 
548
549
  if (ips.external) {
549
- accessibleIps.push(ips.external); // 如果公网 IP 可访问,那么它是 accessibleIps 的第一个元素
550
+ accessibleIps.push(ips.external); // If public IP is accessible, it is the first element of accessibleIps
550
551
  }
551
552
 
552
553
  if (ips.internal) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@blocklet/cli",
3
- "version": "1.17.12",
3
+ "version": "1.17.13-beta-20260512-004004-69bacba8",
4
4
  "description": "Command line tools to manage Blocklet Server",
5
- "homepage": "https://www.arcblock.io/docs/blocklet-cli",
5
+ "homepage": "https://github.com/ArcBlock/blocklet-server/tree/main/core/cli#readme",
6
6
  "bin": {
7
7
  "blocklet": "bin/blocklet.js"
8
8
  },
@@ -20,9 +20,9 @@
20
20
  "bin"
21
21
  ],
22
22
  "author": {
23
- "name": "wangshijun",
24
- "email": "shijun@arcblock.io",
25
- "url": "https://github.com/wangshijun"
23
+ "name": "ArcBlock",
24
+ "email": "blocklet@arcblock.io",
25
+ "url": "https://github.com/ArcBlock"
26
26
  },
27
27
  "license": "Apache-2.0",
28
28
  "repository": {
@@ -33,28 +33,28 @@
33
33
  "url": "https://github.com/ArcBlock/blocklet-server/issues"
34
34
  },
35
35
  "dependencies": {
36
- "@abtnode/blocklet-services": "1.17.12",
37
- "@abtnode/constant": "1.17.12",
38
- "@abtnode/core": "1.17.12",
39
- "@abtnode/db-cache": "1.17.12",
40
- "@abtnode/logger": "1.17.12",
41
- "@abtnode/models": "1.17.12",
42
- "@abtnode/router-provider": "1.17.12",
43
- "@abtnode/util": "1.17.12",
44
- "@abtnode/webapp": "1.17.12",
36
+ "@abtnode/blocklet-services": "1.17.13-beta-20260512-004004-69bacba8",
37
+ "@abtnode/constant": "1.17.13-beta-20260512-004004-69bacba8",
38
+ "@abtnode/core": "1.17.13-beta-20260512-004004-69bacba8",
39
+ "@abtnode/db-cache": "1.17.13-beta-20260512-004004-69bacba8",
40
+ "@abtnode/logger": "1.17.13-beta-20260512-004004-69bacba8",
41
+ "@abtnode/models": "1.17.13-beta-20260512-004004-69bacba8",
42
+ "@abtnode/router-provider": "1.17.13-beta-20260512-004004-69bacba8",
43
+ "@abtnode/util": "1.17.13-beta-20260512-004004-69bacba8",
44
+ "@abtnode/webapp": "1.17.13-beta-20260512-004004-69bacba8",
45
45
  "@arcblock/did": "1.29.27",
46
46
  "@arcblock/event-hub": "1.29.27",
47
47
  "@arcblock/ipfs-only-hash": "^0.0.2",
48
48
  "@arcblock/jwt": "1.29.27",
49
49
  "@arcblock/ws": "1.29.27",
50
- "@blocklet/constant": "1.17.12",
50
+ "@blocklet/constant": "1.17.13-beta-20260512-004004-69bacba8",
51
51
  "@blocklet/error": "^0.3.5",
52
52
  "@blocklet/form-collector": "^0.1.8",
53
- "@blocklet/images": "1.17.12",
54
- "@blocklet/meta": "1.17.12",
55
- "@blocklet/resolver": "1.17.12",
56
- "@blocklet/server-js": "1.17.12",
57
- "@blocklet/store": "1.17.12",
53
+ "@blocklet/images": "1.17.13-beta-20260512-004004-69bacba8",
54
+ "@blocklet/meta": "1.17.13-beta-20260512-004004-69bacba8",
55
+ "@blocklet/resolver": "1.17.13-beta-20260512-004004-69bacba8",
56
+ "@blocklet/server-js": "1.17.13-beta-20260512-004004-69bacba8",
57
+ "@blocklet/store": "1.17.13-beta-20260512-004004-69bacba8",
58
58
  "@blocklet/theme-builder": "^0.4.8",
59
59
  "@ocap/client": "1.29.27",
60
60
  "@ocap/mcrypto": "1.29.27",
@@ -136,7 +136,7 @@
136
136
  "sqlite3": "^5.1.7",
137
137
  "ssri": "^8.0.1",
138
138
  "strip-ansi": "6.0.1",
139
- "tar": "^6.1.11",
139
+ "tar": "^7.5.13",
140
140
  "terminal-link": "^2.1.1",
141
141
  "tweetnacl": "^1.0.3",
142
142
  "tweetnacl-sealedbox-js": "^1.2.0",
@@ -151,9 +151,9 @@
151
151
  "colors": "1.4.0"
152
152
  },
153
153
  "engines": {
154
- "node": ">=14"
154
+ "node": ">=22"
155
155
  },
156
- "gitHead": "a851c46d35a66372b7aa8fb5d7eed7d26980da8a",
156
+ "gitHead": "289f06c2f5a9390aa03cb942b29da0fc5bfdc745",
157
157
  "devDependencies": {
158
158
  "@types/fs-extra": "^11.0.4"
159
159
  }