sitevision-cli 1.0.0-beta.31 → 1.0.0-beta.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +73 -34
  4. package/dist/commands/TaskScreen.js +32 -0
  5. package/dist/commands/build.js +6 -185
  6. package/dist/commands/deploy.js +85 -238
  7. package/dist/commands/dev.js +34 -37
  8. package/dist/commands/info.js +1 -1
  9. package/dist/commands/setup-signing.js +43 -28
  10. package/dist/commands/sign.js +9 -86
  11. package/dist/commands/use-finish.js +17 -0
  12. package/dist/commands/watch.js +0 -8
  13. package/dist/components/AuthLoginScreen.js +27 -12
  14. package/dist/scaffold/inquirer-stub.js +119 -0
  15. package/dist/scaffold/register.js +15 -0
  16. package/dist/shell/ConfigForm.js +174 -29
  17. package/dist/shell/Frame.js +4 -1
  18. package/dist/shell/Help.js +2 -1
  19. package/dist/shell/Settings.js +59 -6
  20. package/dist/shell/Shell.js +361 -43
  21. package/dist/shell/Tabs.js +8 -2
  22. package/dist/shell/actions.js +0 -0
  23. package/dist/utils/config.js +46 -3
  24. package/dist/utils/create-app.js +226 -0
  25. package/dist/utils/i18n.js +68 -13
  26. package/dist/utils/jsonc.js +2 -1
  27. package/dist/utils/keychain.js +44 -4
  28. package/dist/utils/oauth2-auth.js +35 -7
  29. package/dist/utils/password-prompt.js +23 -3
  30. package/dist/utils/process-runner.js +8 -3
  31. package/dist/utils/project-detection.js +172 -44
  32. package/dist/utils/session-cookie-auth.js +5 -4
  33. package/dist/utils/sitevision-api.js +21 -14
  34. package/dist/utils/sitevision-scripts-runner.js +32 -15
  35. package/dist/utils/tasks.js +132 -66
  36. package/dist/utils/version-check.js +3 -3
  37. package/dist/utils/webpack-runner.js +6 -5
  38. package/dist/utils/workspace.js +41 -3
  39. package/dist/utils/zip.js +18 -19
  40. package/package.json +24 -3
  41. package/readme.md +11 -4
  42. package/dist/app.d.ts +0 -6
  43. package/dist/app.js +0 -321
  44. package/dist/cli.d.ts +0 -2
  45. package/dist/commands/build.d.ts +0 -12
  46. package/dist/commands/deploy.d.ts +0 -17
  47. package/dist/commands/dev.d.ts +0 -14
  48. package/dist/commands/index.d.ts +0 -4
  49. package/dist/commands/info.d.ts +0 -2
  50. package/dist/commands/setup-signing.d.ts +0 -2
  51. package/dist/commands/sign.d.ts +0 -14
  52. package/dist/commands/types.d.ts +0 -18
  53. package/dist/commands/watch.d.ts +0 -2
  54. package/dist/components/AnimatedLogo.d.ts +0 -10
  55. package/dist/components/AuthLoginScreen.d.ts +0 -21
  56. package/dist/components/DevPropertiesForm.d.ts +0 -11
  57. package/dist/components/DevPropertiesForm.js +0 -246
  58. package/dist/components/InfoScreen.d.ts +0 -7
  59. package/dist/components/InfoScreen.js +0 -14
  60. package/dist/components/KeychainPasswordChoice.d.ts +0 -8
  61. package/dist/components/KeychainPasswordChoice.js +0 -30
  62. package/dist/components/MainMenu.d.ts +0 -7
  63. package/dist/components/MainMenu.js +0 -88
  64. package/dist/components/PasswordInput.d.ts +0 -10
  65. package/dist/components/ProcessOutput.d.ts +0 -6
  66. package/dist/components/ProcessOutput.js +0 -30
  67. package/dist/components/SetupFlow.d.ts +0 -8
  68. package/dist/components/SetupFlow.js +0 -233
  69. package/dist/components/SigningPropertiesForm.d.ts +0 -7
  70. package/dist/components/SigningPropertiesForm.js +0 -38
  71. package/dist/components/StatusIndicator.d.ts +0 -8
  72. package/dist/components/TextInput.d.ts +0 -10
  73. package/dist/components/TextInput.js +0 -29
  74. package/dist/components/WelcomeScreen.d.ts +0 -14
  75. package/dist/shell/AddonPicker.d.ts +0 -14
  76. package/dist/shell/Changelog.d.ts +0 -10
  77. package/dist/shell/CommandPalette.d.ts +0 -12
  78. package/dist/shell/ConfigForm.d.ts +0 -36
  79. package/dist/shell/Frame.d.ts +0 -59
  80. package/dist/shell/Help.d.ts +0 -9
  81. package/dist/shell/Popover.d.ts +0 -12
  82. package/dist/shell/Settings.d.ts +0 -6
  83. package/dist/shell/Shell.d.ts +0 -10
  84. package/dist/shell/Tabs.d.ts +0 -36
  85. package/dist/shell/actions.d.ts +0 -48
  86. package/dist/types/index.d.ts +0 -295
  87. package/dist/utils/branding.d.ts +0 -28
  88. package/dist/utils/config.d.ts +0 -27
  89. package/dist/utils/environments.d.ts +0 -20
  90. package/dist/utils/i18n.d.ts +0 -12
  91. package/dist/utils/jsonc.d.ts +0 -19
  92. package/dist/utils/keychain.d.ts +0 -15
  93. package/dist/utils/oauth2-auth.d.ts +0 -64
  94. package/dist/utils/password-prompt.d.ts +0 -14
  95. package/dist/utils/process-runner.d.ts +0 -31
  96. package/dist/utils/project-detection.d.ts +0 -202
  97. package/dist/utils/session-cookie-auth.d.ts +0 -35
  98. package/dist/utils/signing-password.d.ts +0 -12
  99. package/dist/utils/signing-step.d.ts +0 -15
  100. package/dist/utils/signing-step.js +0 -16
  101. package/dist/utils/sitevision-api.d.ts +0 -155
  102. package/dist/utils/sitevision-scripts-runner.d.ts +0 -84
  103. package/dist/utils/tasks.d.ts +0 -50
  104. package/dist/utils/version-check.d.ts +0 -9
  105. package/dist/utils/webpack-runner.d.ts +0 -76
  106. package/dist/utils/workspace.d.ts +0 -17
  107. package/dist/utils/zip.d.ts +0 -71
@@ -1,42 +1,44 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import readline from 'readline';
4
- function question(rl, prompt) {
5
- return new Promise(resolve => {
6
- rl.question(prompt, answer => {
7
- resolve(answer);
8
- });
9
- });
4
+ import { findDevPropertiesPath } from '../utils/project-detection.js';
5
+ import { configProblem, getGlobalSigning, setGlobalSigning, settingsFile, } from '../utils/config.js';
6
+ // rl.question drops lines that arrive before it is called, which is every line
7
+ // but the first when stdin is piped. The iterator buffers them.
8
+ function lineReader(rl) {
9
+ const lines = rl[Symbol.asyncIterator]();
10
+ return async (prompt) => {
11
+ process.stdout.write(prompt);
12
+ const { value, done } = await lines.next();
13
+ return done ? '' : String(value);
14
+ };
10
15
  }
11
16
  export const setupSigningCommand = {
12
17
  name: 'setup-signing',
13
18
  description: 'Configure signing credentials for developer.sitevision.se',
14
19
  requiresProject: true,
15
- async execute({ project }) {
20
+ async execute({ project, flags }) {
21
+ const global = Boolean(flags['global']);
16
22
  const rl = readline.createInterface({
17
23
  input: process.stdin,
18
24
  output: process.stdout,
19
25
  });
26
+ const question = lineReader(rl);
20
27
  console.log('\n\x1b[36m\x1b[1mSetup Signing Credentials\x1b[0m\n');
21
28
  console.log('Configure credentials for signing apps on developer.sitevision.se');
22
29
  console.log('(Password will be prompted when running signing commands)\n');
23
- // Find existing dev properties file
24
- const devPropertiesPaths = [
25
- path.join(project.root, '.dev_properties.json'),
26
- path.join(project.root, '.dev-properties.json'),
27
- ];
28
- let devPropertiesPath = devPropertiesPaths[0];
30
+ const devPropertiesPath = findDevPropertiesPath(project.root) ??
31
+ path.join(project.root, '.dev_properties.json');
29
32
  let existingProperties = {};
30
- for (const p of devPropertiesPaths) {
31
- if (fs.existsSync(p)) {
32
- devPropertiesPath = p;
33
- try {
34
- existingProperties = JSON.parse(fs.readFileSync(p, 'utf-8'));
35
- }
36
- catch {
37
- // Invalid file, start fresh
38
- }
39
- break;
33
+ if (global) {
34
+ existingProperties = { ...getGlobalSigning() };
35
+ }
36
+ else {
37
+ try {
38
+ existingProperties = JSON.parse(fs.readFileSync(devPropertiesPath, 'utf8'));
39
+ }
40
+ catch {
41
+ // Missing or invalid file, start fresh
40
42
  }
41
43
  }
42
44
  try {
@@ -45,13 +47,14 @@ export const setupSigningCommand = {
45
47
  const usernamePrompt = defaultUsername
46
48
  ? `Signing username [${defaultUsername}]: `
47
49
  : 'Signing username: ';
48
- let signingUsername = await question(rl, usernamePrompt);
50
+ let signingUsername = await question(usernamePrompt);
49
51
  if (!signingUsername && defaultUsername) {
50
52
  signingUsername = defaultUsername;
51
53
  }
52
54
  if (!signingUsername) {
53
55
  console.log('\x1b[31mError: Signing username is required\x1b[0m');
54
56
  rl.close();
57
+ process.exitCode = 1;
55
58
  return;
56
59
  }
57
60
  // Get certificate name (optional)
@@ -59,15 +62,26 @@ export const setupSigningCommand = {
59
62
  const certPrompt = defaultCertName
60
63
  ? `Certificate name (blank for default) [${defaultCertName}]: `
61
64
  : 'Certificate name (blank for default): ';
62
- let certificateName = await question(rl, certPrompt);
65
+ let certificateName = await question(certPrompt);
63
66
  if (!certificateName && defaultCertName) {
64
67
  certificateName = defaultCertName;
65
68
  }
66
69
  rl.close();
67
- // Update dev properties (strip any plaintext passwords — they live in the keychain)
68
- const { signingPassword: _signingRemoved, password: _passwordRemoved, ...cleanedProperties } = existingProperties;
70
+ if (global) {
71
+ if (configProblem()) {
72
+ console.log(`\x1b[31mError: ${settingsFile()} does not parse; fix it first\x1b[0m`);
73
+ process.exitCode = 1;
74
+ return;
75
+ }
76
+ setGlobalSigning({
77
+ signingUsername,
78
+ ...(certificateName && { certificateName }),
79
+ });
80
+ console.log(`\n\x1b[32mSigning credentials saved to ${settingsFile()}\x1b[0m\n`);
81
+ return;
82
+ }
69
83
  const updatedProperties = {
70
- ...cleanedProperties,
84
+ ...existingProperties,
71
85
  signingUsername,
72
86
  ...(certificateName && { certificateName }),
73
87
  };
@@ -77,6 +91,7 @@ export const setupSigningCommand = {
77
91
  catch (error) {
78
92
  rl.close();
79
93
  console.error('\x1b[31mError setting up signing credentials:\x1b[0m', error);
94
+ process.exitCode = 1;
80
95
  }
81
96
  },
82
97
  };
@@ -1,94 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { render, Box, Text, useInput } from 'ink';
4
- import { StatusIndicator } from '../components/StatusIndicator.js';
5
- import { signApp } from '../utils/sitevision-api.js';
6
- import { resolveSigningPassword } from '../utils/signing-password.js';
7
- import { getZipPath, getSignedZipPath } from '../utils/project-detection.js';
8
- import { formatFileSize, getZipSize, zipExists } from '../utils/zip.js';
9
- export function SignScreen({ projectRoot, manifest, devProperties, password, onBack, onRetryCredentials, }) {
10
- const [state, setState] = React.useState({
11
- status: 'signing',
12
- message: 'Signing app via developer.sitevision.se...',
13
- });
14
- useInput((input, key) => {
15
- if (state.status !== 'signing') {
16
- if (onBack && (key.escape || input === 'q')) {
17
- onBack();
18
- }
19
- if (onRetryCredentials && state.status === 'error' && input === 'r') {
20
- onRetryCredentials();
21
- }
22
- }
23
- });
24
- React.useEffect(() => {
25
- async function runSign() {
26
- try {
27
- const zipPath = getZipPath(projectRoot, manifest);
28
- const signedZipPath = getSignedZipPath(projectRoot, manifest);
29
- // Validate zip exists
30
- if (!zipExists(zipPath)) {
31
- setState({
32
- status: 'error',
33
- error: `Zip file not found: ${zipPath}\nRun 'build' first to create the zip file.`,
34
- });
35
- return;
36
- }
37
- // Sign the app
38
- const result = await signApp(zipPath, {
39
- username: devProperties.signingUsername,
40
- password,
41
- certificateName: devProperties.certificateName,
42
- }, signedZipPath);
43
- if (!result.success) {
44
- setState({
45
- status: 'error',
46
- error: result.error || 'Signing failed',
47
- });
48
- return;
49
- }
50
- const signedSize = getZipSize(signedZipPath);
51
- setState({
52
- status: 'success',
53
- message: 'App signed successfully',
54
- signedPath: signedZipPath,
55
- signedSize,
56
- });
57
- }
58
- catch (error) {
59
- setState({
60
- status: 'error',
61
- error: error instanceof Error ? error.message : String(error),
62
- });
63
- }
64
- }
65
- runSign();
66
- }, [projectRoot, manifest, devProperties, password]);
67
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: state.status === 'signing' ? 'running' : state.status, label: state.status === 'signing'
68
- ? 'Signing'
69
- : state.status === 'success'
70
- ? 'Signed'
71
- : 'Failed', message: state.message }) }), state.status === 'success' && state.signedPath && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "green", children: ["Created: ", state.signedPath] }), state.signedSize !== undefined && (_jsxs(Text, { dimColor: true, children: ["Size: ", formatFileSize(state.signedSize)] }))] })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), state.status !== 'signing' && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.status === 'error' && onRetryCredentials && (_jsx(Text, { dimColor: true, children: "Press r to retry with new credentials" })), onBack && _jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu" })] }))] }));
72
- }
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { TaskScreen } from './TaskScreen.js';
4
+ import { resolveSigningForCli } from './dev.js';
5
+ import { startSign } from '../utils/tasks.js';
73
6
  export const signCommand = {
74
7
  name: 'sign',
75
8
  description: 'Sign the app for production deployment',
76
9
  requiresProject: true,
77
10
  async execute({ project }) {
78
- // Check if signing credentials are configured
79
- if (!project.hasSigningProperties ||
80
- !project.devProperties?.signingUsername) {
81
- console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
82
- console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
11
+ const credentials = await resolveSigningForCli(project);
12
+ if (!credentials)
83
13
  return;
84
- }
85
- const signingUsername = project.devProperties.signingUsername;
86
- const password = await resolveSigningPassword(signingUsername);
87
- if (!password) {
88
- console.log('\x1b[31mError: Password is required\x1b[0m');
89
- return;
90
- }
91
- const { waitUntilExit } = render(_jsx(SignScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, password: password }));
92
- await waitUntilExit();
14
+ const task = startSign(project, credentials);
15
+ await render(_jsx(TaskScreen, { task: task })).waitUntilExit();
93
16
  },
94
17
  };
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { useApp } from 'ink';
3
+ /**
4
+ * Standalone commands end themselves: once `result` is set the Ink app exits,
5
+ * and a failure becomes a non-zero exit code. `stay` keeps the screen up (for a
6
+ * retry key) while still recording the failure.
7
+ */
8
+ export function useFinish(result, stay = false) {
9
+ const { exit } = useApp();
10
+ React.useEffect(() => {
11
+ if (!result)
12
+ return;
13
+ process.exitCode = result === 'error' ? 1 : 0;
14
+ if (!stay)
15
+ exit();
16
+ }, [result, stay, exit]);
17
+ }
@@ -5,14 +5,6 @@ export const watchCommand = {
5
5
  name: 'watch',
6
6
  description: 'Watch and rebuild (optionally signing) without deploying',
7
7
  requiresProject: true,
8
- flags: {
9
- signed: {
10
- type: 'boolean',
11
- description: 'Sign after each build',
12
- alias: 's',
13
- default: false,
14
- },
15
- },
16
8
  async execute({ project, flags }) {
17
9
  let signingCredentials;
18
10
  if (flags['signed']) {
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
5
  import { t } from '../utils/i18n.js';
6
- import { beginOAuth2Login, openBrowser } from '../utils/oauth2-auth.js';
6
+ import { beginOAuth2Login, oauth2ConfigProblem, openBrowser, } from '../utils/oauth2-auth.js';
7
7
  import { beginCookieLogin, } from '../utils/session-cookie-auth.js';
8
8
  /**
9
9
  * Ink-native interactive login for OAuth2 and session-cookie auth. Drives the
@@ -19,11 +19,13 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
19
19
  const cookieRef = React.useRef(null);
20
20
  const cancelOAuthRef = React.useRef(null);
21
21
  React.useEffect(() => {
22
+ // Set by the cleanup: the screen went away while a login was starting.
23
+ let gone = false;
22
24
  void (async () => {
23
25
  if (method === 'oauth2') {
24
26
  const session = beginOAuth2Login(devProperties);
25
27
  if (!session) {
26
- onError('OAuth2 is not fully configured (authorization/token endpoint or client ID missing).');
28
+ onError(oauth2ConfigProblem(devProperties) ?? 'Could not start the login.');
27
29
  return;
28
30
  }
29
31
  cancelOAuthRef.current = session.cancel;
@@ -32,6 +34,8 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
32
34
  setPhase('awaiting');
33
35
  const { token, error } = await session.complete();
34
36
  cancelOAuthRef.current = null;
37
+ if (gone)
38
+ return;
35
39
  if (token) {
36
40
  onComplete({ accessToken: token });
37
41
  }
@@ -41,8 +45,12 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
41
45
  }
42
46
  else {
43
47
  const session = await beginCookieLogin(devProperties);
48
+ if (gone) {
49
+ void session?.close();
50
+ return;
51
+ }
44
52
  if (!session) {
45
- onError('Could not open a login browser (is Chrome installed?). Set SITEVISION_SESSION_COOKIE or pass --cookie with a cookie copied from your browser.');
53
+ onError('Could not open a login browser (is Chrome installed?). Set SITEVISION_SESSION_COOKIE to a cookie copied from your browser.');
46
54
  return;
47
55
  }
48
56
  cookieRef.current = session;
@@ -53,6 +61,7 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
53
61
  // Release resources if the screen unmounts before completing: close the
54
62
  // browser (cookie) and the loopback server (oauth2, frees the port).
55
63
  return () => {
64
+ gone = true;
56
65
  void cookieRef.current?.close();
57
66
  cookieRef.current = null;
58
67
  cancelOAuthRef.current?.();
@@ -72,16 +81,22 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
72
81
  return;
73
82
  setPhase('capturing');
74
83
  void (async () => {
75
- const result = await session.capture();
76
- if (result.cookie) {
77
- cookieRef.current = null;
78
- await session.close();
79
- onComplete({ sessionCookie: result.cookie });
84
+ try {
85
+ const result = await session.capture();
86
+ if (result.cookie) {
87
+ cookieRef.current = null;
88
+ await session.close();
89
+ onComplete({ sessionCookie: result.cookie });
90
+ }
91
+ else {
92
+ // Keep the browser open so the user can navigate and retry.
93
+ setNote(result.error ?? 'No session cookie found.');
94
+ setPhase('awaiting');
95
+ }
80
96
  }
81
- else {
82
- // Keep the browser open so the user can navigate and retry.
83
- setNote(result.error ?? 'No session cookie found.');
84
- setPhase('awaiting');
97
+ catch (error) {
98
+ // The browser window was closed: nothing left to capture from.
99
+ onError(`Could not read the session from the browser: ${error instanceof Error ? error.message : String(error)}`);
85
100
  }
86
101
  })();
87
102
  }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Stands in for inquirer inside the scaffolder: every question goes to svc over
3
+ * the socket in SVC_PROMPT_SOCKET and the reply comes back as the answer. `when`,
4
+ * `filter`, `validate` and function-valued fields run here, where they live.
5
+ * Self-contained on purpose: it is loaded by the tool's node, not by svc.
6
+ */
7
+ import net from 'node:net';
8
+ import process from 'node:process';
9
+ import readline from 'node:readline';
10
+ const CHOICE_TYPES = new Set(['list', 'rawlist', 'expand', 'select', 'search']);
11
+ const evaluate = async (field, ...args) => typeof field === 'function' ? (await field(...args)) : field;
12
+ const text = (value) => typeof value === 'string' ? value : JSON.stringify(value);
13
+ function normalizeChoices(raw) {
14
+ if (!Array.isArray(raw))
15
+ return [];
16
+ const choices = [];
17
+ for (const entry of raw) {
18
+ if (entry === null || typeof entry !== 'object') {
19
+ choices.push({ name: text(entry), value: entry, checked: false });
20
+ continue;
21
+ }
22
+ const choice = entry;
23
+ if (choice['type'] === 'separator' || choice['disabled'])
24
+ continue;
25
+ const value = 'value' in choice ? choice['value'] : choice['name'];
26
+ choices.push({
27
+ name: text(choice['name'] ?? value),
28
+ value,
29
+ checked: Boolean(choice['checked']),
30
+ });
31
+ }
32
+ return choices;
33
+ }
34
+ /** Ask svc until the tool's own validation accepts; undefined when skipped. */
35
+ async function askOne(question, answers, exchange) {
36
+ const type = text(question['type'] ?? 'input');
37
+ const choices = normalizeChoices(await evaluate(question['choices'], answers));
38
+ const fallback = await evaluate(question['default'], answers);
39
+ const values = choices.map(choice => choice.value);
40
+ const message = text(await evaluate(question['message'], answers));
41
+ let initial = fallback;
42
+ if (type === 'checkbox') {
43
+ const preset = Array.isArray(fallback) ? fallback : [];
44
+ initial = choices.flatMap((choice, i) => choice.checked || preset.includes(choice.value) ? [i] : []);
45
+ }
46
+ else if (CHOICE_TYPES.has(type)) {
47
+ // Inquirer takes a default as either the value or its index.
48
+ const at = values.indexOf(fallback);
49
+ initial = at === -1 && typeof fallback === 'number' ? fallback : at;
50
+ initial = Math.max(0, initial);
51
+ }
52
+ let error;
53
+ for (;;) {
54
+ // eslint-disable-next-line no-await-in-loop
55
+ const reply = await exchange({
56
+ name: question.name,
57
+ type,
58
+ message,
59
+ choices: choices.map(choice => choice.name),
60
+ default: initial,
61
+ error,
62
+ });
63
+ if (reply.skip)
64
+ return undefined;
65
+ let { value } = reply;
66
+ if (type === 'checkbox')
67
+ value = value.map(i => values[i]);
68
+ else if (CHOICE_TYPES.has(type))
69
+ value = values[value];
70
+ else if (type === 'number')
71
+ value = Number(value);
72
+ // eslint-disable-next-line no-await-in-loop
73
+ value = await evaluate(question['filter'] ?? value, value, answers);
74
+ // eslint-disable-next-line no-await-in-loop
75
+ const valid = await evaluate(question['validate'] ?? true, value, answers);
76
+ if (valid === true)
77
+ return value;
78
+ error = typeof valid === 'string' ? valid : 'Invalid value';
79
+ }
80
+ }
81
+ async function prompt(questions, initial = {}) {
82
+ const list = Array.isArray(questions)
83
+ ? questions
84
+ : typeof questions.name === 'string'
85
+ ? [questions]
86
+ : Object.entries(questions).map(([name, question]) => ({ ...question, name }));
87
+ const socket = net.connect(process.env['SVC_PROMPT_SOCKET']);
88
+ const lines = readline.createInterface({ input: socket });
89
+ const replies = lines[Symbol.asyncIterator]();
90
+ const exchange = async (message) => {
91
+ socket.write(JSON.stringify(message) + '\n');
92
+ const next = await replies.next();
93
+ if (next.done)
94
+ throw new Error('svc closed the prompt');
95
+ return JSON.parse(next.value);
96
+ };
97
+ // ponytail: dotted names ("a.b") stay flat keys; nest them if a question ever uses one.
98
+ const answers = { ...initial };
99
+ try {
100
+ for (const question of list) {
101
+ const wanted = question['when'] === undefined ||
102
+ // eslint-disable-next-line no-await-in-loop
103
+ Boolean(await evaluate(question['when'], answers));
104
+ const answered = answers[question.name] !== undefined && !question['askAnswered'];
105
+ if (!wanted || answered)
106
+ continue;
107
+ // eslint-disable-next-line no-await-in-loop
108
+ const value = await askOne(question, answers, exchange);
109
+ if (value !== undefined)
110
+ answers[question.name] = value;
111
+ }
112
+ }
113
+ finally {
114
+ socket.end();
115
+ }
116
+ return answers;
117
+ }
118
+ const inquirer = { prompt };
119
+ export default inquirer;
@@ -0,0 +1,15 @@
1
+ // Loaded through NODE_OPTIONS into every process the scaffolder starts: hands
2
+ // sitevision-scripts svc's prompt bridge in place of inquirer.
3
+ import module from 'node:module';
4
+ import path from 'node:path';
5
+ const stub = new URL(`inquirer-stub${path.extname(import.meta.url)}`, import.meta.url).href;
6
+ // Missing before Node 22.15; svc then finds the run unmanaged and falls back.
7
+ module.registerHooks?.({
8
+ resolve(specifier, context, nextResolve) {
9
+ if (specifier === 'inquirer' &&
10
+ context.parentURL?.includes('sitevision-scripts')) {
11
+ return { url: stub, shortCircuit: true };
12
+ }
13
+ return nextResolve(specifier, context);
14
+ },
15
+ });