@nocobase/cli 2.1.0-beta.36 → 2.1.0-beta.37

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.
@@ -13,6 +13,7 @@ const API_BASE_URL_EXAMPLE = 'http://localhost:13000/api';
13
13
  const ENV_KEY_PATTERN = /^[A-Za-z0-9]+$/;
14
14
  const TCP_PORT_EXAMPLE = '13000';
15
15
  const API_BASE_URL_REQUEST_TIMEOUT_MS = 5_000;
16
+ const TCP_PORT_PROBE_HOSTS = ['127.0.0.1', '0.0.0.0', '::1'];
16
17
  function buildHealthCheckUrl(apiBaseUrl) {
17
18
  return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
18
19
  }
@@ -119,7 +120,7 @@ export function validateTcpPort(value) {
119
120
  }
120
121
  return undefined;
121
122
  }
122
- async function canListenOnTcpPort(port) {
123
+ async function canListenOnTcpPortHost(port, host) {
123
124
  return await new Promise((resolve) => {
124
125
  const server = net.createServer();
125
126
  const resolveAndCleanup = (result) => {
@@ -131,15 +132,28 @@ async function canListenOnTcpPort(port) {
131
132
  resolve(result);
132
133
  }
133
134
  };
134
- server.once('error', () => {
135
+ server.once('error', (error) => {
136
+ if (error.code === 'EADDRNOTAVAIL' || error.code === 'EAFNOSUPPORT') {
137
+ resolveAndCleanup(undefined);
138
+ return;
139
+ }
135
140
  resolveAndCleanup(false);
136
141
  });
137
142
  server.once('listening', () => {
138
143
  resolveAndCleanup(true);
139
144
  });
140
- server.listen(port, '127.0.0.1');
145
+ server.listen(port, host);
141
146
  });
142
147
  }
148
+ async function canListenOnTcpPort(port) {
149
+ for (const host of TCP_PORT_PROBE_HOSTS) {
150
+ const result = await canListenOnTcpPortHost(port, host);
151
+ if (result === false) {
152
+ return false;
153
+ }
154
+ }
155
+ return true;
156
+ }
143
157
  function parseDockerPublishedTcpPorts(output) {
144
158
  const ports = new Set();
145
159
  for (const line of output.split(/\r?\n/)) {
@@ -210,9 +224,10 @@ async function allocateAvailableTcpPort() {
210
224
  }
211
225
  export async function findAvailableTcpPort() {
212
226
  const dockerPorts = await getDockerPublishedTcpPorts();
213
- for (let attempt = 0; attempt < 10; attempt += 1) {
227
+ for (let attempt = 0; attempt < 20; attempt += 1) {
214
228
  const candidate = await allocateAvailableTcpPort();
215
- if (!dockerPorts.has(Number.parseInt(candidate, 10))) {
229
+ const port = Number.parseInt(candidate, 10);
230
+ if (!dockerPorts.has(port) && (await canListenOnTcpPort(port))) {
216
231
  return candidate;
217
232
  }
218
233
  }
@@ -228,6 +243,9 @@ export async function validateAvailableTcpPort(value) {
228
243
  return formatError;
229
244
  }
230
245
  const port = parseTcpPort(raw);
246
+ if (port === undefined) {
247
+ return translateCli('validators.tcpPort.invalid', { example: TCP_PORT_EXAMPLE });
248
+ }
231
249
  const dockerPorts = await getDockerPublishedTcpPorts();
232
250
  if (dockerPorts.has(port)) {
233
251
  return translateCli('validators.tcpPort.alreadyInUseByDocker', { port });
@@ -295,6 +295,10 @@ function computePwcWizardSteps(options, merged, locale) {
295
295
  const PWC_FORM_ITEM_EXPLAIN = '<div class="pwc-form-item-explain" data-pwc-explain="1" role="alert" hidden></div>';
296
296
  /** Suffix slot for status icon (e.g. fail) — filled by client script. */
297
297
  const PWC_FORM_ITEM_SUFFIX = '<span class="pwc-form-item-suffix" data-pwc-suffix="1" aria-hidden="true"></span>';
298
+ /** Password visibility toggle icon (`eye`). */
299
+ const PWC_FORM_EYE_ICON_SVG = `<svg class="pwc-form-toggle__svg" viewBox="0 0 14 14" width="14" height="14" focusable="false" aria-hidden="true"><path fill="currentColor" d="M7 3c-3.14 0-5.67 2.06-6.69 4c1.02 1.94 3.55 4 6.69 4s5.67-2.06 6.69-4C12.67 5.06 10.14 3 7 3zm0 6.8A2.8 2.8 0 1 1 7 4.2a2.8 2.8 0 0 1 0 5.6zm0-1.2A1.6 1.6 0 1 0 7 5.4a1.6 1.6 0 0 0 0 3.2z"/></svg>`;
300
+ /** Password visibility toggle icon (`eye-off`). */
301
+ const PWC_FORM_EYE_OFF_ICON_SVG = `<svg class="pwc-form-toggle__svg" viewBox="0 0 14 14" width="14" height="14" focusable="false" aria-hidden="true"><path fill="currentColor" d="M2.17 1.32l10.51 10.51-.85.85-2.04-2.04A7.37 7.37 0 0 1 7 11C3.86 11 1.33 8.94.31 7c.54-1.02 1.53-2.15 2.84-2.98L1.32 2.17l.85-.85zm1.88 3.58A6.26 6.26 0 0 0 1.69 7c1.02 1.63 3.12 2.8 5.31 2.8c.79 0 1.56-.15 2.25-.43l-1.1-1.1A2.8 2.8 0 0 1 4.88 5l-.83-.1zm5.87 2.48l-.9-.9a2 2 0 0 0-1.68-1.68l-.9-.9a2.8 2.8 0 0 1 3.48 3.48zM7 3c3.14 0 5.67 2.06 6.69 4a8.4 8.4 0 0 1-2.13 2.43l-.84-.84A6.98 6.98 0 0 0 12.31 7C11.29 5.37 9.19 4.2 7 4.2c-.39 0-.77.04-1.14.11l-.98-.98C5.56 3.12 6.27 3 7 3z"/></svg>`;
298
302
  function renderPwcRadioOptions(key, def, defaults, hidden, locale) {
299
303
  return def.options
300
304
  .map((o, index) => {
@@ -322,7 +326,7 @@ function renderPwcRadioOptions(key, def, defaults, hidden, locale) {
322
326
  })
323
327
  .join('');
324
328
  }
325
- function renderPwcFieldRow(key, def, defaults, show, locale) {
329
+ function renderPwcFieldRow(key, def, defaults, show, locale, uiText) {
326
330
  if (!isInputBlock(def)) {
327
331
  return '';
328
332
  }
@@ -330,13 +334,16 @@ function renderPwcFieldRow(key, def, defaults, show, locale) {
330
334
  const hidden = show[key] === false;
331
335
  const display = hidden ? 'none' : 'block';
332
336
  const itemOpen = (extraClass) => `<div class="pwc-form-item${extraClass ? ` ${extraClass}` : ''}" data-pwc-wrap="${escapeHtml(key)}" data-pwc-field="${escapeHtml(key)}" style="display:${display}">`;
337
+ const label = `<span class="pwc-form-item-label-text">${'required' in def && def.required
338
+ ? '<span class="pwc-form-item-required" aria-hidden="true">*</span>'
339
+ : ''}<span class="pwc-l">${escapeHtml(labelText)}</span></span>`;
333
340
  if (def.type === 'text') {
334
341
  const req = def.required ? ' required' : '';
335
342
  const disabled = hidden ? ' disabled' : '';
336
343
  const ph = def.placeholder ? ` placeholder="${escapeHtml(resolveUiText(def.placeholder, locale))}"` : '';
337
344
  const v = escapeHtml(String(defaults[key] ?? ''));
338
345
  return (itemOpen('') +
339
- `<div class="pwc-form-item-label"><span class="pwc-l">${escapeHtml(labelText)}</span></div>` +
346
+ `<div class="pwc-form-item-label">${label}</div>` +
340
347
  `<div class="pwc-form-item-control">` +
341
348
  `<div class="pwc-form-item-control-input">` +
342
349
  `<div class="pwc-input-affix-wrapper">` +
@@ -350,7 +357,7 @@ function renderPwcFieldRow(key, def, defaults, show, locale) {
350
357
  const checked = defaults[key] === true ? ' checked' : '';
351
358
  const disabled = hidden ? ' disabled' : '';
352
359
  return (itemOpen('pwc-form-item--bool') +
353
- `<div class="pwc-bool-wrap"><label><input class="pwc-bool-input" name="${escapeHtml(key)}" type="checkbox" value="1"${checked}${disabled} /> <span class="pwc-l">${escapeHtml(labelText)}</span></label></div>` +
360
+ `<div class="pwc-bool-wrap"><label><input class="pwc-bool-input" name="${escapeHtml(key)}" type="checkbox" value="1"${checked}${disabled} /> ${label}</label></div>` +
354
361
  PWC_FORM_ITEM_EXPLAIN +
355
362
  `</div>`);
356
363
  }
@@ -358,7 +365,7 @@ function renderPwcFieldRow(key, def, defaults, show, locale) {
358
365
  if (def.variant === 'radio') {
359
366
  const opts = renderPwcRadioOptions(key, def, defaults, hidden, locale);
360
367
  return (itemOpen('') +
361
- `<div class="pwc-form-item-label"><span class="pwc-l">${escapeHtml(labelText)}</span></div>` +
368
+ `<div class="pwc-form-item-label">${label}</div>` +
362
369
  `<div class="pwc-form-item-control">` +
363
370
  `<div class="pwc-form-item-control-input">` +
364
371
  `<div class="pwc-radio-group" role="radiogroup" aria-label="${escapeHtml(labelText)}">` +
@@ -379,7 +386,7 @@ function renderPwcFieldRow(key, def, defaults, show, locale) {
379
386
  const req = def.required ? ' required' : '';
380
387
  const disabled = hidden ? ' disabled' : '';
381
388
  return (itemOpen('') +
382
- `<div class="pwc-form-item-label"><span class="pwc-l">${escapeHtml(labelText)}</span></div>` +
389
+ `<div class="pwc-form-item-label">${label}</div>` +
383
390
  `<div class="pwc-form-item-control">` +
384
391
  `<div class="pwc-form-item-control-input">` +
385
392
  `<div class="pwc-input-affix-wrapper pwc-input-affix-wrapper--select">` +
@@ -393,13 +400,15 @@ function renderPwcFieldRow(key, def, defaults, show, locale) {
393
400
  const req = def.required ? ' required' : '';
394
401
  const disabled = hidden ? ' disabled' : '';
395
402
  const v = escapeHtml(String(defaults[key] ?? ''));
403
+ const showPassword = escapeHtml(uiText?.showPassword ?? 'Show password');
396
404
  return (itemOpen('') +
397
- `<div class="pwc-form-item-label"><span class="pwc-l">${escapeHtml(labelText)}</span></div>` +
405
+ `<div class="pwc-form-item-label">${label}</div>` +
398
406
  `<div class="pwc-form-item-control">` +
399
407
  `<div class="pwc-form-item-control-input">` +
400
- `<div class="pwc-input-affix-wrapper">` +
401
- `<input class="pwc-form-input" name="${escapeHtml(key)}" type="password" value="${v}"${req}${disabled} autocomplete="new-password" />` +
408
+ `<div class="pwc-input-affix-wrapper pwc-input-affix-wrapper--password" data-pwc-password-wrap="1">` +
409
+ `<input class="pwc-form-input" data-pwc-password-input="1" name="${escapeHtml(key)}" type="password" value="${v}"${req}${disabled} autocomplete="new-password" />` +
402
410
  PWC_FORM_ITEM_SUFFIX +
411
+ `<button class="pwc-password-toggle" type="button" data-pwc-password-toggle="1" aria-label="${showPassword}" title="${showPassword}" aria-pressed="false"${disabled}>${PWC_FORM_EYE_ICON_SVG}</button>` +
403
412
  `</div></div>` +
404
413
  PWC_FORM_ITEM_EXPLAIN +
405
414
  `</div></div>`);
@@ -410,7 +419,7 @@ function renderPwcFieldRow(key, def, defaults, show, locale) {
410
419
  const disabled = hidden ? ' disabled' : '';
411
420
  const v = String(defaults[key] ?? 0);
412
421
  return (itemOpen('') +
413
- `<div class="pwc-form-item-label"><span class="pwc-l">${escapeHtml(labelText)}</span></div>` +
422
+ `<div class="pwc-form-item-label">${label}</div>` +
414
423
  `<div class="pwc-form-item-control">` +
415
424
  `<div class="pwc-form-item-control-input">` +
416
425
  `<div class="pwc-input-affix-wrapper">` +
@@ -517,7 +526,7 @@ function buildPwcFormHtml(catalog, defaults, show, stepDefs, initialStepIndex, t
517
526
  for (const key of sd.keys) {
518
527
  const def = catalog[key];
519
528
  if (def) {
520
- out.push(renderPwcFieldRow(key, def, defaults, show, locale));
529
+ out.push(renderPwcFieldRow(key, def, defaults, show, locale, uiText));
521
530
  }
522
531
  }
523
532
  out.push('</section>');
@@ -707,6 +716,8 @@ function runPromptCatalogWebUIImpl(options) {
707
716
  back: t('promptCatalog.web.back'),
708
717
  next: t('promptCatalog.web.next'),
709
718
  submit: t('promptCatalog.web.submit'),
719
+ showPassword: t('promptCatalog.web.showPassword'),
720
+ hidePassword: t('promptCatalog.web.hidePassword'),
710
721
  checking: t('promptCatalog.web.checking'),
711
722
  sending: t('promptCatalog.web.sending'),
712
723
  successTitle: t('promptCatalog.web.successTitle'),
@@ -1213,6 +1224,8 @@ function runPromptCatalogWebUIImpl(options) {
1213
1224
  /* antd Form.Item: label + control + explain — @see https://ant.design/components/form */
1214
1225
  .pwc-form-item { margin: 0 0 24px 0; }
1215
1226
  .pwc-form-item-label { margin: 0; padding: 0; }
1227
+ .pwc-form-item-label-text { display: inline-flex; align-items: flex-start; gap: 4px; }
1228
+ .pwc-form-item-required { color: var(--pwc-form-color-error); font-weight: 600; line-height: 1; margin-top: 2px; }
1216
1229
  .pwc-form-item-control { width: 100%; }
1217
1230
  .pwc-form-item--bool { margin: 0 0 24px 0; }
1218
1231
  .pwc-form-item--bool .pwc-bool-wrap { margin: 0; }
@@ -1226,7 +1239,71 @@ function runPromptCatalogWebUIImpl(options) {
1226
1239
  pointer-events: none;
1227
1240
  }
1228
1241
  .pwc-input-affix-wrapper .pwc-form-input { padding-right: 32px; }
1229
- .pwc-input-affix-wrapper--select .pwc-form-input { padding-right: 32px; }
1242
+ .pwc-input-affix-wrapper--select::after {
1243
+ content: '';
1244
+ position: absolute;
1245
+ right: 14px;
1246
+ top: 50%;
1247
+ width: 8px;
1248
+ height: 8px;
1249
+ margin-top: -6px;
1250
+ border-right: 2px solid var(--pwc-ad-color-text-description);
1251
+ border-bottom: 2px solid var(--pwc-ad-color-text-description);
1252
+ transform: rotate(45deg);
1253
+ pointer-events: none;
1254
+ transition: border-color 0.2s;
1255
+ }
1256
+ .pwc-input-affix-wrapper--select:hover::after {
1257
+ border-color: var(--pwc-ad-color-text);
1258
+ }
1259
+ .pwc-input-affix-wrapper--select:focus-within::after {
1260
+ border-color: var(--pwc-ad-color-primary);
1261
+ }
1262
+ .pwc-input-affix-wrapper--select .pwc-form-input {
1263
+ padding-right: 56px;
1264
+ appearance: none;
1265
+ -webkit-appearance: none;
1266
+ -moz-appearance: none;
1267
+ background-image: none;
1268
+ }
1269
+ .pwc-input-affix-wrapper--select .pwc-form-item-suffix { right: 36px; }
1270
+ .pwc-input-affix-wrapper--password .pwc-form-input { padding-right: 68px; }
1271
+ .pwc-input-affix-wrapper--password .pwc-form-item-suffix { right: 39px; }
1272
+ .pwc-password-toggle {
1273
+ position: absolute;
1274
+ z-index: 1;
1275
+ right: 8px;
1276
+ top: 50%;
1277
+ width: 24px;
1278
+ height: 24px;
1279
+ margin: 0;
1280
+ padding: 0;
1281
+ display: inline-flex;
1282
+ align-items: center;
1283
+ justify-content: center;
1284
+ border: none;
1285
+ border-radius: 6px;
1286
+ background: transparent;
1287
+ color: var(--pwc-ad-color-text-description);
1288
+ cursor: pointer;
1289
+ transform: translateY(-50%);
1290
+ transition: color 0.2s, background 0.2s, box-shadow 0.2s;
1291
+ }
1292
+ .pwc-password-toggle:hover {
1293
+ color: var(--pwc-ad-color-text);
1294
+ background: color-mix(in srgb, var(--pwc-ad-color-text) 6%, transparent);
1295
+ }
1296
+ .pwc-password-toggle:focus-visible {
1297
+ outline: none;
1298
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--pwc-ad-color-primary) 16%, transparent);
1299
+ }
1300
+ .pwc-password-toggle:disabled {
1301
+ color: color-mix(in srgb, var(--pwc-ad-color-text-description) 60%, transparent);
1302
+ cursor: not-allowed;
1303
+ background: transparent;
1304
+ box-shadow: none;
1305
+ }
1306
+ .pwc-form-toggle__svg { display: block; }
1230
1307
  .pwc-form-item-explain { font-size: 14px; line-height: 1.5715; margin-top: 4px; clear: both; }
1231
1308
  .pwc-form-item-explain[hidden] { display: none !important; }
1232
1309
  .pwc-form-item-explain:not([hidden]) { color: var(--pwc-form-color-error); }
@@ -1257,6 +1334,9 @@ function runPromptCatalogWebUIImpl(options) {
1257
1334
  border-radius: 6px;
1258
1335
  transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
1259
1336
  }
1337
+ input.pwc-form-input, select.pwc-form-input {
1338
+ height: 40px;
1339
+ }
1260
1340
  input.pwc-form-input:hover, select.pwc-form-input:hover, textarea:hover {
1261
1341
  border-color: var(--pwc-ad-color-primary);
1262
1342
  }
@@ -1411,6 +1491,8 @@ function runPromptCatalogWebUIImpl(options) {
1411
1491
  var pwcErrIcon = ${JSON.stringify(PWC_AD_ALERT_ERROR_ICON_SVG)};
1412
1492
  var pwcOkIcon = ${JSON.stringify(PWC_AD_ALERT_SUCCESS_ICON_SVG)};
1413
1493
  var pwcFieldFail = ${JSON.stringify(PWC_FORM_FAIL_ICON_SVG)};
1494
+ var pwcEyeIcon = ${JSON.stringify(PWC_FORM_EYE_ICON_SVG)};
1495
+ var pwcEyeOffIcon = ${JSON.stringify(PWC_FORM_EYE_OFF_ICON_SVG)};
1414
1496
  var pwcCloseTimer = null;
1415
1497
  var pwcCloseProbeTimer = null;
1416
1498
  var pwcFieldReqSeq = {};
@@ -1468,7 +1550,7 @@ function runPromptCatalogWebUIImpl(options) {
1468
1550
  }
1469
1551
  function pwcSetFieldError(key, message) {
1470
1552
  if (!form || !key) { return; }
1471
- var wrap = form.querySelector('[data-pwc-field=\"' + String(key) + '\"]');
1553
+ var wrap = form.querySelector('[data-pwc-field="' + String(key) + '"]');
1472
1554
  if (!wrap) { return; }
1473
1555
  var ex = wrap.querySelector('[data-pwc-explain]');
1474
1556
  var suf = wrap.querySelector('[data-pwc-suffix]');
@@ -1506,9 +1588,50 @@ function runPromptCatalogWebUIImpl(options) {
1506
1588
  } catch (e) {}
1507
1589
  return { msg: msg, fk: fk };
1508
1590
  }
1591
+ function pwcSyncPasswordToggle(toggle, input) {
1592
+ if (!toggle || !input) { return; }
1593
+ var visible = input.getAttribute('type') === 'text';
1594
+ var label = visible ? uiText.hidePassword : uiText.showPassword;
1595
+ toggle.setAttribute('aria-label', String(label));
1596
+ toggle.setAttribute('title', String(label));
1597
+ toggle.setAttribute('aria-pressed', visible ? 'true' : 'false');
1598
+ toggle.innerHTML = visible ? pwcEyeOffIcon : pwcEyeIcon;
1599
+ }
1600
+ function pwcInitPasswordToggles() {
1601
+ if (!form) { return; }
1602
+ var toggles = form.querySelectorAll('[data-pwc-password-toggle]');
1603
+ for (var i = 0; i < toggles.length; i++) {
1604
+ (function () {
1605
+ var toggle = toggles[i];
1606
+ var wrap = toggle.parentElement;
1607
+ var input = wrap ? wrap.querySelector('[data-pwc-password-input]') : null;
1608
+ pwcSyncPasswordToggle(toggle, input);
1609
+ toggle.addEventListener('click', function () {
1610
+ if (!input || input.disabled) { return; }
1611
+ var nextType = input.getAttribute('type') === 'text' ? 'password' : 'text';
1612
+ var start = typeof input.selectionStart === 'number' ? input.selectionStart : null;
1613
+ var end = typeof input.selectionEnd === 'number' ? input.selectionEnd : null;
1614
+ input.setAttribute('type', nextType);
1615
+ pwcSyncPasswordToggle(toggle, input);
1616
+ if (typeof input.focus === 'function') {
1617
+ input.focus({ preventScroll: true });
1618
+ }
1619
+ if (
1620
+ start !== null &&
1621
+ end !== null &&
1622
+ typeof input.setSelectionRange === 'function'
1623
+ ) {
1624
+ try {
1625
+ input.setSelectionRange(start, end);
1626
+ } catch (e) {}
1627
+ }
1628
+ });
1629
+ })();
1630
+ }
1631
+ }
1509
1632
  function pwcSetFieldDirty(key, dirty) {
1510
1633
  if (!form || !key) { return; }
1511
- var wrap = form.querySelector('[data-pwc-field=\"' + String(key) + '\"]');
1634
+ var wrap = form.querySelector('[data-pwc-field="' + String(key) + '"]');
1512
1635
  if (!wrap) { return; }
1513
1636
  if (dirty) {
1514
1637
  wrap.setAttribute('data-pwc-dirty', '1');
@@ -1518,7 +1641,7 @@ function runPromptCatalogWebUIImpl(options) {
1518
1641
  }
1519
1642
  function pwcIsFieldDirty(key) {
1520
1643
  if (!form || !key) { return false; }
1521
- var wrap = form.querySelector('[data-pwc-field=\"' + String(key) + '\"]');
1644
+ var wrap = form.querySelector('[data-pwc-field="' + String(key) + '"]');
1522
1645
  return !!(wrap && wrap.getAttribute('data-pwc-dirty') === '1');
1523
1646
  }
1524
1647
  function getControl(formEl, k) {
@@ -1609,7 +1732,7 @@ function runPromptCatalogWebUIImpl(options) {
1609
1732
  var wasHidden = w.style.display === 'none';
1610
1733
  var hidden = k != null && sh[k] === false;
1611
1734
  w.style.display = hidden ? 'none' : 'block';
1612
- var ctrls = w.querySelectorAll('input, select, textarea');
1735
+ var ctrls = w.querySelectorAll('input, select, textarea, button[data-pwc-password-toggle]');
1613
1736
  for (var i = 0; i < ctrls.length; i++) {
1614
1737
  ctrls[i].disabled = hidden || ctrls[i].getAttribute('data-pwc-static-disabled') === '1';
1615
1738
  }
@@ -1633,7 +1756,7 @@ function runPromptCatalogWebUIImpl(options) {
1633
1756
  }
1634
1757
  for (var i = 0; i < st.keys.length; i++) {
1635
1758
  var key = st.keys[i];
1636
- var wrap = form.querySelector('[data-pwc-wrap=\"' + String(key) + '\"]');
1759
+ var wrap = form.querySelector('[data-pwc-wrap="' + String(key) + '"]');
1637
1760
  if (wrap && wrap.style.display !== 'none') {
1638
1761
  return true;
1639
1762
  }
@@ -1771,7 +1894,7 @@ function runPromptCatalogWebUIImpl(options) {
1771
1894
  }
1772
1895
  for (var j = 0; j < st.keys.length; j++) {
1773
1896
  var k = st.keys[j];
1774
- var w = form.querySelector('[data-pwc-wrap=\"' + k + '\"]');
1897
+ var w = form.querySelector('[data-pwc-wrap="' + k + '"]');
1775
1898
  if (!w || w.style.display === 'none') { continue; }
1776
1899
  var inp = getControl(form, k);
1777
1900
  if (inp == null) { continue; }
@@ -1796,7 +1919,7 @@ function runPromptCatalogWebUIImpl(options) {
1796
1919
  }
1797
1920
  function pwcValidateField(key) {
1798
1921
  if (!form || !key || pwcValField == null) { return Promise.resolve(); }
1799
- var wrap = form.querySelector('[data-pwc-wrap=\"' + String(key) + '\"]');
1922
+ var wrap = form.querySelector('[data-pwc-wrap="' + String(key) + '"]');
1800
1923
  if (!wrap || wrap.style.display === 'none') { return Promise.resolve(); }
1801
1924
  var reqSeq = (pwcFieldReqSeq[key] || 0) + 1;
1802
1925
  pwcFieldReqSeq[key] = reqSeq;
@@ -1898,6 +2021,7 @@ function runPromptCatalogWebUIImpl(options) {
1898
2021
  var pwcNext = document.getElementById('pwcNext');
1899
2022
  if (pwcNext) { pwcNext.addEventListener('click', function () { void pwcTryAdvanceNext(); }); }
1900
2023
  pwcRefreshVisibleSteps();
2024
+ pwcInitPasswordToggles();
1901
2025
  if (form) { setTimeout(function () { reflow(); }, 0); }
1902
2026
  if (form) {
1903
2027
  form.addEventListener('submit', function (e) {
@@ -1934,7 +2058,7 @@ function runPromptCatalogWebUIImpl(options) {
1934
2058
  });
1935
2059
  }
1936
2060
  })();
1937
- <\/script>
2061
+ </script>
1938
2062
  </body>
1939
2063
  </html>`;
1940
2064
  return page;
@@ -19,9 +19,39 @@ import fsp from 'node:fs/promises';
19
19
  import os from 'node:os';
20
20
  import path from 'node:path';
21
21
  import spawn from 'cross-spawn';
22
+ import { translateCli } from './cli-locale.js';
23
+ import { resolveConfiguredCommandName } from './cli-config.js';
22
24
  const FORWARDED_SIGNALS = ['SIGINT', 'SIGTERM'];
23
- function resolveCommandName(name) {
24
- return name;
25
+ const PROCESS_TIMEOUT_FORCE_KILL_DELAY_MS = 1000;
26
+ const MISSING_COMMAND_SPECS = {
27
+ docker: {
28
+ displayName: 'Docker',
29
+ configKey: 'bin.docker',
30
+ },
31
+ git: {
32
+ displayName: 'Git',
33
+ configKey: 'bin.git',
34
+ },
35
+ yarn: {
36
+ displayName: 'Yarn',
37
+ configKey: 'bin.yarn',
38
+ },
39
+ };
40
+ async function resolveCommandName(name) {
41
+ return await resolveConfiguredCommandName(name);
42
+ }
43
+ function createMissingCommandError(name, label, error) {
44
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
45
+ if (code !== 'ENOENT') {
46
+ return undefined;
47
+ }
48
+ if (!Object.prototype.hasOwnProperty.call(MISSING_COMMAND_SPECS, name)) {
49
+ return undefined;
50
+ }
51
+ const spec = MISSING_COMMAND_SPECS[name];
52
+ return new Error(translateCli('commands.shared.missingCommand', { action: label, displayName: spec.displayName, configKey: spec.configKey }, {
53
+ fallback: `Couldn't run \`${label}\` because the ${spec.displayName} executable could not be found. Install ${spec.displayName} or update \`nb config set ${spec.configKey} <path>\` and try again.`,
54
+ }));
25
55
  }
26
56
  function pathExists(candidate) {
27
57
  try {
@@ -40,8 +70,8 @@ function isDirectory(candidate) {
40
70
  }
41
71
  }
42
72
  function hasLocalNocoBaseBinary(candidate) {
43
- return (pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1'))
44
- || pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1.cmd')));
73
+ return (pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1')) ||
74
+ pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1.cmd')));
45
75
  }
46
76
  export function resolveCwd(cwd) {
47
77
  const next = cwd ?? process.cwd();
@@ -61,10 +91,7 @@ export function resolveProjectCwd(cwd) {
61
91
  throw new Error(`The specified --cwd is not a directory: ${fallback}`);
62
92
  }
63
93
  let current = hasExplicitInput ? fallback : process.cwd();
64
- while (true) {
65
- if (hasLocalNocoBaseBinary(current)) {
66
- return current;
67
- }
94
+ while (!hasLocalNocoBaseBinary(current)) {
68
95
  const parent = path.dirname(current);
69
96
  if (parent === current) {
70
97
  if (hasExplicitInput) {
@@ -74,12 +101,13 @@ export function resolveProjectCwd(cwd) {
74
101
  }
75
102
  current = parent;
76
103
  }
104
+ return current;
77
105
  }
78
- export function run(name, args, options) {
106
+ export async function run(name, args, options) {
79
107
  const cwd = resolveCwd(options?.cwd);
80
108
  const label = options?.errorName ?? name;
81
- const command = resolveCommandName(name);
82
- return new Promise((resolve, reject) => {
109
+ const command = await resolveCommandName(name);
110
+ return await new Promise((resolve, reject) => {
83
111
  const child = spawn(command, [...args], {
84
112
  stdio: options?.stdio ?? 'inherit',
85
113
  cwd,
@@ -104,12 +132,19 @@ export function run(name, args, options) {
104
132
  }
105
133
  }
106
134
  const cleanupSignalForwarding = forwardSignalsToChild(child);
135
+ const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
107
136
  child.once('error', (error) => {
137
+ timeoutController.cleanup();
108
138
  cleanupSignalForwarding();
109
- reject(error);
139
+ reject(createMissingCommandError(name, label, error) ?? error);
110
140
  });
111
141
  child.once('close', (code, signal) => {
142
+ timeoutController.cleanup();
112
143
  cleanupSignalForwarding();
144
+ if (timeoutController.didTimeout()) {
145
+ reject(new Error(`${label} timed out after ${options?.timeoutMs}ms`));
146
+ return;
147
+ }
113
148
  if (code === 0) {
114
149
  resolve();
115
150
  return;
@@ -122,6 +157,46 @@ export function run(name, args, options) {
122
157
  });
123
158
  });
124
159
  }
160
+ function attachProcessTimeout(child, timeoutMs) {
161
+ if (!timeoutMs || timeoutMs <= 0) {
162
+ return {
163
+ cleanup: () => undefined,
164
+ didTimeout: () => false,
165
+ };
166
+ }
167
+ let didTimeout = false;
168
+ let forceKillTimer;
169
+ const isChildRunning = () => child.exitCode === null && child.signalCode === null;
170
+ const terminateChild = (signal) => {
171
+ if (!isChildRunning()) {
172
+ return;
173
+ }
174
+ try {
175
+ child.kill(signal);
176
+ }
177
+ catch {
178
+ // Ignore kill errors here and let the child close/error handlers surface the failure.
179
+ }
180
+ };
181
+ const timeoutTimer = setTimeout(() => {
182
+ didTimeout = true;
183
+ terminateChild('SIGTERM');
184
+ forceKillTimer = setTimeout(() => {
185
+ terminateChild('SIGKILL');
186
+ }, PROCESS_TIMEOUT_FORCE_KILL_DELAY_MS);
187
+ forceKillTimer.unref?.();
188
+ }, timeoutMs);
189
+ timeoutTimer.unref?.();
190
+ return {
191
+ cleanup: () => {
192
+ clearTimeout(timeoutTimer);
193
+ if (forceKillTimer) {
194
+ clearTimeout(forceKillTimer);
195
+ }
196
+ },
197
+ didTimeout: () => didTimeout,
198
+ };
199
+ }
125
200
  function forwardSignalsToChild(child) {
126
201
  let forwardedSignalCount = 0;
127
202
  const listeners = new Map();
@@ -149,10 +224,11 @@ function forwardSignalsToChild(child) {
149
224
  }
150
225
  };
151
226
  }
152
- export function commandSucceeds(name, args, options) {
227
+ export async function commandSucceeds(name, args, options) {
153
228
  const cwd = resolveCwd(options?.cwd);
154
- const command = resolveCommandName(name);
155
- return new Promise((resolve) => {
229
+ const label = options?.errorName ?? name;
230
+ const command = await resolveCommandName(name);
231
+ return await new Promise((resolve, reject) => {
156
232
  const child = spawn(command, [...args], {
157
233
  cwd,
158
234
  env: {
@@ -162,15 +238,31 @@ export function commandSucceeds(name, args, options) {
162
238
  stdio: 'ignore',
163
239
  windowsHide: process.platform === 'win32',
164
240
  });
165
- child.once('error', () => resolve(false));
166
- child.once('close', (code) => resolve(code === 0));
241
+ const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
242
+ child.once('error', (error) => {
243
+ timeoutController.cleanup();
244
+ const missingCommandError = createMissingCommandError(name, label, error);
245
+ if (missingCommandError) {
246
+ reject(missingCommandError);
247
+ return;
248
+ }
249
+ resolve(false);
250
+ });
251
+ child.once('close', (code) => {
252
+ timeoutController.cleanup();
253
+ if (timeoutController.didTimeout()) {
254
+ resolve(false);
255
+ return;
256
+ }
257
+ resolve(code === 0);
258
+ });
167
259
  });
168
260
  }
169
- export function commandOutput(name, args, options) {
261
+ export async function commandOutput(name, args, options) {
170
262
  const cwd = resolveCwd(options?.cwd);
171
263
  const label = options?.errorName ?? name;
172
- const command = resolveCommandName(name);
173
- return new Promise((resolve, reject) => {
264
+ const command = await resolveCommandName(name);
265
+ return await new Promise((resolve, reject) => {
174
266
  const child = spawn(command, [...args], {
175
267
  cwd,
176
268
  env: {
@@ -190,8 +282,17 @@ export function commandOutput(name, args, options) {
190
282
  child.stderr.on('data', (chunk) => {
191
283
  stderr += chunk;
192
284
  });
193
- child.once('error', reject);
285
+ const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
286
+ child.once('error', (error) => {
287
+ timeoutController.cleanup();
288
+ reject(createMissingCommandError(name, label, error) ?? error);
289
+ });
194
290
  child.once('close', (code, signal) => {
291
+ timeoutController.cleanup();
292
+ if (timeoutController.didTimeout()) {
293
+ reject(new Error(`${label} timed out after ${options?.timeoutMs}ms`));
294
+ return;
295
+ }
195
296
  if (code === 0) {
196
297
  resolve(stdout.trim());
197
298
  return;
@@ -216,7 +317,7 @@ async function readCommandOutputFile(filePath) {
216
317
  export async function commandOutputViaFile(name, args, options) {
217
318
  const cwd = resolveCwd(options?.cwd);
218
319
  const label = options?.errorName ?? name;
219
- const command = resolveCommandName(name);
320
+ const command = await resolveCommandName(name);
220
321
  const captureDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-output-'));
221
322
  const stdoutPath = path.join(captureDir, 'stdout.log');
222
323
  const stderrPath = path.join(captureDir, 'stderr.log');
@@ -233,8 +334,17 @@ export async function commandOutputViaFile(name, args, options) {
233
334
  stdio: ['ignore', stdoutHandle.fd, stderrHandle.fd],
234
335
  windowsHide: process.platform === 'win32',
235
336
  });
236
- child.once('error', reject);
337
+ const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
338
+ child.once('error', (error) => {
339
+ timeoutController.cleanup();
340
+ reject(createMissingCommandError(name, label, error) ?? error);
341
+ });
237
342
  child.once('close', (code, signal) => {
343
+ timeoutController.cleanup();
344
+ if (timeoutController.didTimeout()) {
345
+ reject(new Error(`${label} timed out after ${options?.timeoutMs}ms`));
346
+ return;
347
+ }
238
348
  resolve({ code, signal });
239
349
  });
240
350
  });