sitevision-cli 1.0.0-beta.9 → 1.0.1

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 (100) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +194 -70
  4. package/dist/commands/TaskScreen.js +32 -0
  5. package/dist/commands/build.js +6 -185
  6. package/dist/commands/deploy.js +95 -212
  7. package/dist/commands/dev.js +87 -416
  8. package/dist/commands/info.js +1 -1
  9. package/dist/commands/setup-signing.js +50 -33
  10. package/dist/commands/sign.js +9 -86
  11. package/dist/commands/use-finish.js +17 -0
  12. package/dist/commands/watch.js +5 -31
  13. package/dist/components/AnimatedLogo.js +8 -2
  14. package/dist/components/AuthLoginScreen.js +33 -15
  15. package/dist/components/PasswordInput.js +2 -1
  16. package/dist/scaffold/inquirer-stub.js +119 -0
  17. package/dist/scaffold/register.js +15 -0
  18. package/dist/shell/AddonPicker.js +54 -0
  19. package/dist/shell/Changelog.js +50 -0
  20. package/dist/shell/CommandPalette.js +63 -0
  21. package/dist/shell/ConfigForm.js +703 -0
  22. package/dist/shell/Frame.js +138 -0
  23. package/dist/shell/Help.js +96 -0
  24. package/dist/shell/Popover.js +15 -0
  25. package/dist/shell/Settings.js +149 -0
  26. package/dist/shell/Shell.js +953 -0
  27. package/dist/shell/Tabs.js +96 -0
  28. package/dist/shell/actions.js +493 -0
  29. package/dist/utils/config.js +62 -2
  30. package/dist/utils/create-app.js +231 -0
  31. package/dist/utils/debug.js +32 -0
  32. package/dist/utils/environments.js +74 -0
  33. package/dist/utils/i18n.js +369 -0
  34. package/dist/utils/jsonc.js +2 -1
  35. package/dist/utils/keychain.js +54 -6
  36. package/dist/utils/oauth2-auth.js +146 -43
  37. package/dist/utils/password-prompt.js +37 -3
  38. package/dist/utils/process-runner.js +77 -38
  39. package/dist/utils/project-detection.js +457 -112
  40. package/dist/utils/session-cookie-auth.js +10 -6
  41. package/dist/utils/sitevision-api.js +134 -25
  42. package/dist/utils/sitevision-scripts-runner.js +50 -22
  43. package/dist/utils/tasks.js +510 -0
  44. package/dist/utils/version-check.js +3 -3
  45. package/dist/utils/webpack-runner.js +9 -5
  46. package/dist/utils/workspace.js +105 -0
  47. package/dist/utils/zip.js +18 -19
  48. package/package.json +29 -5
  49. package/readme.md +117 -141
  50. package/dist/app.d.ts +0 -6
  51. package/dist/app.js +0 -307
  52. package/dist/cli.d.ts +0 -2
  53. package/dist/commands/build.d.ts +0 -12
  54. package/dist/commands/deploy.d.ts +0 -16
  55. package/dist/commands/dev.d.ts +0 -16
  56. package/dist/commands/index.d.ts +0 -4
  57. package/dist/commands/info.d.ts +0 -2
  58. package/dist/commands/setup-signing.d.ts +0 -2
  59. package/dist/commands/sign.d.ts +0 -14
  60. package/dist/commands/types.d.ts +0 -18
  61. package/dist/commands/watch.d.ts +0 -2
  62. package/dist/components/AnimatedLogo.d.ts +0 -10
  63. package/dist/components/AuthLoginScreen.d.ts +0 -21
  64. package/dist/components/DevPropertiesForm.d.ts +0 -10
  65. package/dist/components/DevPropertiesForm.js +0 -210
  66. package/dist/components/InfoScreen.d.ts +0 -7
  67. package/dist/components/InfoScreen.js +0 -14
  68. package/dist/components/KeychainPasswordChoice.d.ts +0 -8
  69. package/dist/components/KeychainPasswordChoice.js +0 -30
  70. package/dist/components/MainMenu.d.ts +0 -7
  71. package/dist/components/MainMenu.js +0 -83
  72. package/dist/components/PasswordInput.d.ts +0 -10
  73. package/dist/components/ProcessOutput.d.ts +0 -6
  74. package/dist/components/ProcessOutput.js +0 -30
  75. package/dist/components/SetupFlow.d.ts +0 -8
  76. package/dist/components/SetupFlow.js +0 -233
  77. package/dist/components/SigningPropertiesForm.d.ts +0 -7
  78. package/dist/components/SigningPropertiesForm.js +0 -38
  79. package/dist/components/StatusIndicator.d.ts +0 -8
  80. package/dist/components/TextInput.d.ts +0 -10
  81. package/dist/components/TextInput.js +0 -29
  82. package/dist/components/WelcomeScreen.d.ts +0 -14
  83. package/dist/types/index.d.ts +0 -284
  84. package/dist/utils/branding.d.ts +0 -28
  85. package/dist/utils/config.d.ts +0 -17
  86. package/dist/utils/jsonc.d.ts +0 -19
  87. package/dist/utils/keychain.d.ts +0 -15
  88. package/dist/utils/oauth2-auth.d.ts +0 -27
  89. package/dist/utils/password-prompt.d.ts +0 -14
  90. package/dist/utils/process-runner.d.ts +0 -22
  91. package/dist/utils/project-detection.d.ts +0 -151
  92. package/dist/utils/session-cookie-auth.d.ts +0 -34
  93. package/dist/utils/signing-password.d.ts +0 -12
  94. package/dist/utils/signing-step.d.ts +0 -15
  95. package/dist/utils/signing-step.js +0 -16
  96. package/dist/utils/sitevision-api.d.ts +0 -112
  97. package/dist/utils/sitevision-scripts-runner.d.ts +0 -83
  98. package/dist/utils/version-check.d.ts +0 -9
  99. package/dist/utils/webpack-runner.d.ts +0 -76
  100. package/dist/utils/zip.d.ts +0 -71
@@ -0,0 +1,231 @@
1
+ /**
2
+ * New apps are scaffolded by Sitevision's own tool. svc runs it as a task and
3
+ * answers its questions through a socket (see source/scaffold); when the tool no
4
+ * longer asks through inquirer the run is reported as unmanaged, and the caller
5
+ * hands it the real terminal instead.
6
+ */
7
+ import fs from 'node:fs';
8
+ import net from 'node:net';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import readline from 'node:readline';
12
+ import { spawn } from 'node:child_process';
13
+ import { stripVTControlCharacters } from 'node:util';
14
+ import { t } from './i18n.js';
15
+ import { ProcessRunner } from './process-runner.js';
16
+ import { createTask, setPhase } from './tasks.js';
17
+ import { detectProject, findDevPropertiesPath, migrateLegacyPassword, readInheritedDevProperties, readManifest, updatePackageJson, writeManifestField, } from './project-detection.js';
18
+ import { debug } from './debug.js';
19
+ export const CREATE_COMMAND = [
20
+ 'npx',
21
+ '--yes',
22
+ '@sitevision/create-sitevision-app',
23
+ ];
24
+ /** A folder name that is also a usable package name and manifest id. */
25
+ export const validAppName = (name) => /^[\da-z][\w\-.]*$/i.test(name);
26
+ /** The closest conventional name: "Min nya app!" and "MinNyaApp" become "min-nya-app". */
27
+ export function suggestAppName(name) {
28
+ return name
29
+ .trim()
30
+ .normalize('NFKD')
31
+ .replaceAll(/[\u0300-\u036F]/g, '')
32
+ .replaceAll(/(?<=[\da-z])(?=[A-Z])/g, '-')
33
+ .toLowerCase()
34
+ .replaceAll(/\s+/g, '-')
35
+ .replaceAll(/[^\w\-.]/g, '')
36
+ .replace(/^[^\da-z]+/i, '');
37
+ }
38
+ /** What is wrong with a typed app name, said so the fix is obvious. */
39
+ export function appNameProblem(typed) {
40
+ const name = typed.trim();
41
+ if (!name)
42
+ return t('Type a name for the app, for example my-new-app.');
43
+ if (validAppName(name))
44
+ return undefined;
45
+ const suggestion = suggestAppName(name);
46
+ const tryThis = suggestion
47
+ ? ' ' + t('Try "{suggestion}".', { suggestion })
48
+ : '';
49
+ if (/\s/.test(name) && validAppName(name.replaceAll(/\s+/g, '-')))
50
+ return t("An app name can't have spaces.") + tryThis;
51
+ const bad = [...new Set(name.replaceAll(/[\s\w\-.]/g, ''))].join(' ');
52
+ if (bad) {
53
+ return (t("An app name can't contain {chars}: it becomes the folder, the package name and the app id.", {
54
+ chars: bad,
55
+ }) + tryThis);
56
+ }
57
+ return t('Start the name with a letter or a digit.') + tryThis;
58
+ }
59
+ /** A valid name that could be better; the user may keep it. */
60
+ export function appNameAdvice(typed) {
61
+ const name = typed.trim();
62
+ if (!validAppName(name) || name === name.toLowerCase())
63
+ return undefined;
64
+ return t('Lowercase is safer: npm expects it, and "{name}" and "{lower}" are one folder on macOS and Windows but two on Linux. Try "{suggestion}", or press Enter again to keep "{name}".', { name, lower: name.toLowerCase(), suggestion: suggestAppName(name) });
65
+ }
66
+ export const isEmptyOrMissing = (dir) => !fs.existsSync(dir) || fs.readdirSync(dir).length === 0;
67
+ /** Listen for the stub's questions. `null` from `ask` means the user cancelled. */
68
+ export async function servePrompts(ask, onCancel) {
69
+ const id = `svc-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
70
+ const socketPath = process.platform === 'win32'
71
+ ? `\\\\.\\pipe\\${id}`
72
+ : path.join(os.tmpdir(), `${id}.sock`);
73
+ let connected = false;
74
+ const server = net.createServer(socket => {
75
+ connected = true;
76
+ socket.on('error', () => { });
77
+ void (async () => {
78
+ for await (const line of readline.createInterface({ input: socket })) {
79
+ const reply = await ask(JSON.parse(line));
80
+ if (!reply) {
81
+ socket.destroy();
82
+ onCancel();
83
+ return;
84
+ }
85
+ socket.write(JSON.stringify(reply) + '\n');
86
+ }
87
+ })();
88
+ });
89
+ await new Promise((resolve, reject) => {
90
+ server.once('error', reject);
91
+ server.listen(socketPath, resolve);
92
+ });
93
+ return {
94
+ socketPath,
95
+ connected: () => connected,
96
+ close: () => server.close(),
97
+ };
98
+ }
99
+ /**
100
+ * Run the scaffolder for `name` inside `parentDir`. "unmanaged" means it never
101
+ * asked svc anything and left no app: the prompt bridge did not take.
102
+ */
103
+ export function startCreateApp(options) {
104
+ const { name, parentDir, ask, command = CREATE_COMMAND } = options;
105
+ const appDir = path.join(parentDir, name);
106
+ let runner;
107
+ let stopped = false;
108
+ const { task, log, finish } = createTask('create', { root: appDir, manifest: { name, id: name } }, 'create-sitevision-app', () => {
109
+ stopped = true;
110
+ runner?.kill();
111
+ finish('stopped');
112
+ });
113
+ const done = (async () => {
114
+ const prompts = await servePrompts(ask, () => task.stop());
115
+ try {
116
+ // .js from dist, .ts when svc itself runs from source.
117
+ const register = new URL(`../scaffold/register${path.extname(import.meta.url)}`, import.meta.url).href;
118
+ runner = new ProcessRunner(command[0], [...command.slice(1), name], parentDir, {
119
+ ...process.env,
120
+ NODE_OPTIONS: `${process.env['NODE_OPTIONS'] ?? ''} --import=${register}`.trim(),
121
+ SVC_PROMPT_SOCKET: prompts.socketPath,
122
+ });
123
+ runner.on('output', (output) => {
124
+ // The tool clears the screen between steps; that is noise in a log.
125
+ const text = stripVTControlCharacters(output.data);
126
+ if (text.trim())
127
+ log('create', text, output.type === 'stderr' ? 'warn' : 'info');
128
+ });
129
+ setPhase(task, 'scaffolding');
130
+ await runner.run();
131
+ if (stopped)
132
+ return 'stopped';
133
+ // The exit code is 0 even when the prompts crash, so look at the result.
134
+ if (readManifest(appDir)) {
135
+ finish('success');
136
+ return 'created';
137
+ }
138
+ if (prompts.connected()) {
139
+ finish('error', 'The scaffolder finished without creating an app');
140
+ return 'failed';
141
+ }
142
+ finish('stopped');
143
+ return 'unmanaged';
144
+ }
145
+ catch (error) {
146
+ finish('error', error instanceof Error ? error.message : String(error));
147
+ return 'failed';
148
+ }
149
+ finally {
150
+ prompts.close();
151
+ }
152
+ })();
153
+ return { task, done };
154
+ }
155
+ /** Run the scaffolder on the real terminal; resolves when it exits. */
156
+ export async function createAppInTerminal(name, parentDir) {
157
+ await new Promise(resolve => {
158
+ // `name` is validated by the caller; joined to avoid DEP0190.
159
+ debug('spawn', `${CREATE_COMMAND.join(' ')} ${name} (cwd ${parentDir})`);
160
+ const child = spawn([...CREATE_COMMAND, name].join(' '), {
161
+ cwd: parentDir,
162
+ stdio: 'inherit',
163
+ shell: true,
164
+ });
165
+ child.on('close', code => {
166
+ debug('spawn', `create-sitevision-app exited ${code}`);
167
+ resolve();
168
+ });
169
+ child.on('error', () => resolve());
170
+ });
171
+ try {
172
+ return Boolean(readManifest(path.join(parentDir, name)));
173
+ }
174
+ catch {
175
+ // A half-written manifest is a failed scaffold, not a crash.
176
+ return false;
177
+ }
178
+ }
179
+ /**
180
+ * Replace the template's placeholders with what is known, and fold the config
181
+ * the tool wrote into svc's layout: no plaintext password, and nothing in the
182
+ * app's .dev_properties.json that the workspace already provides.
183
+ */
184
+ export function seedNewApp(appDir, workspaceRoot) {
185
+ const found = readManifest(appDir);
186
+ if (!found)
187
+ return;
188
+ const { manifest, manifestPath } = found;
189
+ const seed = (key, value) => {
190
+ const localized = typeof manifest[key] === 'object';
191
+ writeManifestField(manifestPath, key, value, localized ? 'en' : undefined);
192
+ };
193
+ seed('name', path.basename(appDir));
194
+ if (workspaceRoot) {
195
+ let shared = {};
196
+ try {
197
+ shared = JSON.parse(fs.readFileSync(path.join(workspaceRoot, 'package.json'), 'utf8'));
198
+ }
199
+ catch { }
200
+ const author = typeof shared['author'] === 'object'
201
+ ? shared['author']?.name
202
+ : shared['author'];
203
+ if (typeof author === 'string' && author)
204
+ writeManifestField(manifestPath, 'author', author);
205
+ if (typeof shared['homepage'] === 'string' && shared['homepage'])
206
+ writeManifestField(manifestPath, 'helpUrl', shared['homepage']);
207
+ }
208
+ const project = detectProject(appDir);
209
+ if (project)
210
+ migrateLegacyPassword(project);
211
+ const file = findDevPropertiesPath(appDir);
212
+ if (!workspaceRoot || !file)
213
+ return;
214
+ const own = JSON.parse(fs.readFileSync(file, 'utf8'));
215
+ const inherited = readInheritedDevProperties(appDir);
216
+ if (typeof own['addonName'] === 'string' && own['addonName']) {
217
+ const addonName = own['addonName'];
218
+ updatePackageJson(appDir, packageJson => {
219
+ packageJson['addonName'] = addonName;
220
+ });
221
+ }
222
+ const rest = Object.fromEntries(Object.entries(own).filter(([key, value]) => key !== 'addonName' &&
223
+ value !== '' &&
224
+ value !== undefined &&
225
+ !(key === 'useHTTPForDevDeploy' && value === false) &&
226
+ JSON.stringify(value) !== JSON.stringify(inherited[key])));
227
+ if (Object.keys(rest).length === 0)
228
+ fs.rmSync(file);
229
+ else
230
+ fs.writeFileSync(file, JSON.stringify(rest, null, 2) + '\n');
231
+ }
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { stripVTControlCharacters } from 'node:util';
4
+ let logFile;
5
+ /** Start the debug log (one file per run) in the given directory. */
6
+ export function enableDebug(dir) {
7
+ if (logFile)
8
+ return logFile;
9
+ fs.mkdirSync(dir, { recursive: true });
10
+ logFile = path.join(dir, 'debug.log');
11
+ fs.writeFileSync(logFile, '');
12
+ return logFile;
13
+ }
14
+ export const debugFile = () => logFile;
15
+ /** Append one line; a multi-line message is indented under the first line. */
16
+ export function debug(scope, message) {
17
+ if (!logFile)
18
+ return;
19
+ const text = message.split('\n').join('\n' + ' '.repeat(34));
20
+ try {
21
+ fs.appendFileSync(logFile, `${new Date().toISOString()} ${scope.padEnd(8)} ${text}\n`);
22
+ }
23
+ catch {
24
+ // NOOP: A full or read-only disk must not take the CLI down.
25
+ }
26
+ }
27
+ /** Print a message for the user and keep a copy in the debug log. */
28
+ export function say(text) {
29
+ debug('out', stripVTControlCharacters(text).trim());
30
+ console.log(text);
31
+ }
32
+ export const errorText = (error) => error instanceof Error ? (error.stack ?? error.message) : String(error);
@@ -0,0 +1,74 @@
1
+ import { resolveRuntimeSecrets } from './project-detection.js';
2
+ /** Name of the environment the top-level dev properties describe. */
3
+ export function baseEnvironment(dev) {
4
+ return dev?.baseEnvironment?.trim() || 'dev';
5
+ }
6
+ export function environmentNames(dev) {
7
+ const base = baseEnvironment(dev);
8
+ return [
9
+ base,
10
+ ...Object.keys(dev?.environments ?? {}).filter(name => name !== base),
11
+ ];
12
+ }
13
+ export function isProductionEnvironment(name, dev) {
14
+ if (name === baseEnvironment(dev))
15
+ return dev?.production ?? false;
16
+ return dev?.environments?.[name]?.production ?? /prod/i.test(name);
17
+ }
18
+ /** Badge colour: dev green, production red, anything else yellow. */
19
+ export function environmentColor(name, dev) {
20
+ if (isProductionEnvironment(name, dev))
21
+ return 'red';
22
+ return name === baseEnvironment(dev) ? 'green' : 'yellow';
23
+ }
24
+ /**
25
+ * The effective dev properties for one environment: the base with that
26
+ * environment's overrides applied and credentials resolved for its
27
+ * domain/username. The base ("dev") is returned as-is.
28
+ */
29
+ export function resolveEnvironment(dev, name) {
30
+ const production = isProductionEnvironment(name, dev);
31
+ if (name === baseEnvironment(dev)) {
32
+ return { ...dev, environmentName: name, productionEnvironment: production };
33
+ }
34
+ const override = dev.environments?.[name] ?? {};
35
+ const { production: _production, ...fields } = override;
36
+ const resolved = {
37
+ ...dev,
38
+ ...fields,
39
+ password: undefined,
40
+ accessToken: undefined,
41
+ sessionCookie: undefined,
42
+ environmentName: name,
43
+ productionEnvironment: production,
44
+ };
45
+ return resolveRuntimeSecrets(resolved);
46
+ }
47
+ /** A project view whose dev properties are resolved for `name`. */
48
+ export function environmentProject(project, name) {
49
+ if (!project.devProperties)
50
+ return project;
51
+ return {
52
+ ...project,
53
+ devProperties: resolveEnvironment(project.devProperties, name),
54
+ };
55
+ }
56
+ /**
57
+ * Write one environment's override into the base: keep only the keys whose
58
+ * value differs from the base, so the file stays minimal.
59
+ */
60
+ export function withEnvironmentOverride(base, name, values) {
61
+ const current = { ...base.environments?.[name] };
62
+ const changed = Object.fromEntries(Object.entries(values).map(([key, value]) => {
63
+ const baseValue = base[key];
64
+ const same = value === undefined ||
65
+ value === '' ||
66
+ JSON.stringify(value) === JSON.stringify(baseValue);
67
+ return [key, same ? undefined : value];
68
+ }));
69
+ const override = Object.fromEntries(Object.entries({ ...current, ...changed }).filter(([, value]) => value !== undefined));
70
+ return {
71
+ ...base,
72
+ environments: { ...base.environments, [name]: override },
73
+ };
74
+ }