sitevision-cli 1.0.0-beta.2 → 1.0.0-beta.21
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/dist/app.d.ts +1 -1
- package/dist/app.js +59 -8
- package/dist/cli.js +96 -39
- package/dist/commands/build.js +1 -1
- package/dist/commands/deploy.d.ts +2 -2
- package/dist/commands/deploy.js +135 -25
- package/dist/commands/dev.d.ts +8 -10
- package/dist/commands/dev.js +77 -366
- package/dist/commands/info.js +2 -2
- package/dist/commands/watch.js +5 -23
- package/dist/components/AnimatedLogo.js +8 -2
- package/dist/components/AuthLoginScreen.d.ts +21 -0
- package/dist/components/AuthLoginScreen.js +90 -0
- package/dist/components/DevPropertiesForm.d.ts +2 -1
- package/dist/components/DevPropertiesForm.js +198 -33
- package/dist/components/InfoScreen.js +2 -2
- package/dist/components/MainMenu.js +7 -2
- package/dist/components/PasswordInput.js +2 -1
- package/dist/components/SetupFlow.d.ts +2 -1
- package/dist/components/SetupFlow.js +100 -11
- package/dist/shell/AddonPicker.d.ts +14 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/CommandPalette.d.ts +8 -0
- package/dist/shell/CommandPalette.js +63 -0
- package/dist/shell/ConfigForm.d.ts +36 -0
- package/dist/shell/ConfigForm.js +558 -0
- package/dist/shell/Frame.d.ts +59 -0
- package/dist/shell/Frame.js +134 -0
- package/dist/shell/Settings.d.ts +6 -0
- package/dist/shell/Settings.js +96 -0
- package/dist/shell/Shell.d.ts +9 -0
- package/dist/shell/Shell.js +586 -0
- package/dist/shell/Tabs.d.ts +36 -0
- package/dist/shell/Tabs.js +90 -0
- package/dist/shell/actions.d.ts +45 -0
- package/dist/shell/actions.js +0 -0
- package/dist/types/index.d.ts +44 -5
- package/dist/utils/config.d.ts +10 -0
- package/dist/utils/config.js +14 -0
- package/dist/utils/environments.d.ts +20 -0
- package/dist/utils/environments.js +74 -0
- package/dist/utils/i18n.d.ts +12 -0
- package/dist/utils/i18n.js +279 -0
- package/dist/utils/jsonc.d.ts +19 -0
- package/dist/utils/jsonc.js +74 -0
- package/dist/utils/keychain.d.ts +9 -0
- package/dist/utils/keychain.js +54 -0
- package/dist/utils/oauth2-auth.d.ts +64 -0
- package/dist/utils/oauth2-auth.js +242 -0
- package/dist/utils/password-prompt.d.ts +5 -0
- package/dist/utils/password-prompt.js +28 -0
- package/dist/utils/project-detection.d.ts +105 -6
- package/dist/utils/project-detection.js +411 -54
- package/dist/utils/session-cookie-auth.d.ts +35 -0
- package/dist/utils/session-cookie-auth.js +99 -0
- package/dist/utils/sitevision-api.d.ts +64 -5
- package/dist/utils/sitevision-api.js +195 -33
- package/dist/utils/tasks.d.ts +48 -0
- package/dist/utils/tasks.js +371 -0
- package/dist/utils/workspace.d.ts +17 -0
- package/dist/utils/workspace.js +67 -0
- package/package.json +3 -1
- package/readme.md +102 -121
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { useSyncExternalStore } from 'react';
|
|
5
|
+
import { WebpackRunner, hasLocalWebpackConfig } from './webpack-runner.js';
|
|
6
|
+
import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from './sitevision-scripts-runner.js';
|
|
7
|
+
import { copyStaticToBuild, copySrcToBuild, cleanBuild, createBuildZip, zipExists, } from './zip.js';
|
|
8
|
+
import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, localizedText, } from './project-detection.js';
|
|
9
|
+
import { signApp, deployApp, deployProduction, activateApp, } from './sitevision-api.js';
|
|
10
|
+
import { ProcessRunner } from './process-runner.js';
|
|
11
|
+
const MAX_LINES = 2000;
|
|
12
|
+
const emitter = new EventEmitter();
|
|
13
|
+
let tasks = [];
|
|
14
|
+
let nextId = 1;
|
|
15
|
+
function notify() {
|
|
16
|
+
tasks = [...tasks];
|
|
17
|
+
emitter.emit('change');
|
|
18
|
+
}
|
|
19
|
+
export function getTasks() {
|
|
20
|
+
return tasks;
|
|
21
|
+
}
|
|
22
|
+
export function useTasks() {
|
|
23
|
+
return useSyncExternalStore(callback => {
|
|
24
|
+
emitter.on('change', callback);
|
|
25
|
+
return () => emitter.off('change', callback);
|
|
26
|
+
}, () => tasks);
|
|
27
|
+
}
|
|
28
|
+
export function runningTasks(appRoot) {
|
|
29
|
+
return tasks.filter(t => t.status === 'running' && (!appRoot || t.appRoot === appRoot));
|
|
30
|
+
}
|
|
31
|
+
export function clearFinished() {
|
|
32
|
+
tasks = tasks.filter(t => t.status === 'running');
|
|
33
|
+
notify();
|
|
34
|
+
}
|
|
35
|
+
function createTask(kind, project, label, stop = () => { }) {
|
|
36
|
+
const task = {
|
|
37
|
+
id: nextId++,
|
|
38
|
+
kind,
|
|
39
|
+
appRoot: project.root,
|
|
40
|
+
appName: localizedText(project.manifest.name) || project.manifest.id,
|
|
41
|
+
label,
|
|
42
|
+
status: 'running',
|
|
43
|
+
phase: 'starting',
|
|
44
|
+
startedAt: Date.now(),
|
|
45
|
+
lines: [],
|
|
46
|
+
stop,
|
|
47
|
+
};
|
|
48
|
+
tasks.push(task);
|
|
49
|
+
notify();
|
|
50
|
+
const log = (tag, text, level = 'info') => {
|
|
51
|
+
for (const line of text.trimEnd().split('\n')) {
|
|
52
|
+
task.lines.push({ time: Date.now(), tag, level, text: line });
|
|
53
|
+
}
|
|
54
|
+
if (task.lines.length > MAX_LINES) {
|
|
55
|
+
task.lines.splice(0, task.lines.length - MAX_LINES);
|
|
56
|
+
}
|
|
57
|
+
notify();
|
|
58
|
+
};
|
|
59
|
+
const finish = (status, error) => {
|
|
60
|
+
if (task.status !== 'running')
|
|
61
|
+
return;
|
|
62
|
+
task.status = status;
|
|
63
|
+
task.error = error;
|
|
64
|
+
task.endedAt = Date.now();
|
|
65
|
+
task.phase = status;
|
|
66
|
+
if (error)
|
|
67
|
+
log('svc', error, 'error');
|
|
68
|
+
notify();
|
|
69
|
+
};
|
|
70
|
+
return { task, log, finish };
|
|
71
|
+
}
|
|
72
|
+
function setPhase(task, phase) {
|
|
73
|
+
task.phase = phase;
|
|
74
|
+
notify();
|
|
75
|
+
}
|
|
76
|
+
const errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// One-shot steps shared by the tasks below.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/** Build the app to dist/<appId>.zip. Returns the zip path or throws. */
|
|
81
|
+
async function buildOnce(project, task, log, mode) {
|
|
82
|
+
const { root, manifest } = project;
|
|
83
|
+
cleanBuild(root);
|
|
84
|
+
if (isBundledApp(manifest) && !hasLocalWebpackConfig(root)) {
|
|
85
|
+
if (!hasSitevisionScripts(root)) {
|
|
86
|
+
throw new Error('No webpack.config.js found and @sitevision/sitevision-scripts is not installed. Run npm install.');
|
|
87
|
+
}
|
|
88
|
+
const { warning } = checkSitevisionScriptsCompatibility(root);
|
|
89
|
+
if (warning)
|
|
90
|
+
log('bld', warning, 'warn');
|
|
91
|
+
setPhase(task, 'building');
|
|
92
|
+
log('bld', 'building via sitevision-scripts');
|
|
93
|
+
const result = await runSitevisionScriptsBuild(root, chunk => log('bld', chunk));
|
|
94
|
+
if (!result.success)
|
|
95
|
+
throw new Error(result.error ?? 'Build failed');
|
|
96
|
+
const zipPath = getDelegatedZipPath(root, manifest.id);
|
|
97
|
+
if (!zipExists(zipPath)) {
|
|
98
|
+
throw new Error(`Build reported success but no zip was found at ${zipPath}.`);
|
|
99
|
+
}
|
|
100
|
+
return zipPath;
|
|
101
|
+
}
|
|
102
|
+
if (isBundledApp(manifest)) {
|
|
103
|
+
if (!WebpackRunner.isWebpackAvailable(root)) {
|
|
104
|
+
throw new Error('webpack not found. Run npm install.');
|
|
105
|
+
}
|
|
106
|
+
setPhase(task, 'building');
|
|
107
|
+
log('bld', 'compiling with webpack');
|
|
108
|
+
const runner = new WebpackRunner(root, {
|
|
109
|
+
mode,
|
|
110
|
+
cssPrefix: manifest.id,
|
|
111
|
+
restApp: getAppType(manifest) !== 'web' && getAppType(manifest) !== 'widget',
|
|
112
|
+
});
|
|
113
|
+
const result = await runner.run();
|
|
114
|
+
await runner.close();
|
|
115
|
+
reportBuild(result, log);
|
|
116
|
+
if (!result.success)
|
|
117
|
+
throw new Error(result.errors?.join('\n') || 'Build failed');
|
|
118
|
+
copyStaticToBuild(root);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
setPhase(task, 'copying');
|
|
122
|
+
log('bld', 'copying source files');
|
|
123
|
+
copySrcToBuild(root);
|
|
124
|
+
copyStaticToBuild(root);
|
|
125
|
+
}
|
|
126
|
+
setPhase(task, 'zipping');
|
|
127
|
+
return createBuildZip(root, getFullAppId(manifest.id));
|
|
128
|
+
}
|
|
129
|
+
function reportBuild(result, log) {
|
|
130
|
+
for (const warning of result.warnings ?? [])
|
|
131
|
+
log('bld', warning, 'warn');
|
|
132
|
+
for (const error of result.errors ?? [])
|
|
133
|
+
log('bld', error, 'error');
|
|
134
|
+
if (result.success) {
|
|
135
|
+
log('bld', `compiled in ${result.stats?.time ?? 0}ms${result.stats?.assets?.length ? ` · ${result.stats.assets.join(', ')}` : ''}`, 'ok');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function signOnce(project, task, log, zipPath, credentials) {
|
|
139
|
+
setPhase(task, 'signing');
|
|
140
|
+
log('sgn', `signing via developer.sitevision.se${credentials.certificateName ? ` · cert ${credentials.certificateName}` : ''}`);
|
|
141
|
+
const signedPath = getSignedZipPath(project.root, project.manifest);
|
|
142
|
+
const result = await signApp(zipPath, credentials, signedPath);
|
|
143
|
+
if (!result.success)
|
|
144
|
+
throw new Error(result.error ?? 'Signing failed');
|
|
145
|
+
log('sgn', `signed ${path.basename(signedPath)}`, 'ok');
|
|
146
|
+
return signedPath;
|
|
147
|
+
}
|
|
148
|
+
async function deployOnce(project, task, log, zipPath, config, options) {
|
|
149
|
+
setPhase(task, 'deploying');
|
|
150
|
+
const appType = getAppType(project.manifest);
|
|
151
|
+
log('dep', `POST multipart → ${options.production ? 'production' : 'dev'} import · ${config.addonName}`);
|
|
152
|
+
const result = options.production
|
|
153
|
+
? await deployProduction(zipPath, { ...config, activate: options.activate }, appType)
|
|
154
|
+
: await deployApp(zipPath, config, appType, options.force);
|
|
155
|
+
if (!result.success)
|
|
156
|
+
throw new Error(result.error ?? 'Deployment failed');
|
|
157
|
+
log('dep', `${result.message ?? 'deployed'}${result.executableId ? ` · exec ${result.executableId}` : ''}`, 'ok');
|
|
158
|
+
return result.executableId;
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Tasks
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
export function startBuild(project) {
|
|
164
|
+
const { task, log, finish } = createTask('build', project, 'build');
|
|
165
|
+
void (async () => {
|
|
166
|
+
try {
|
|
167
|
+
const zip = await buildOnce(project, task, log, 'production');
|
|
168
|
+
log('bld', `created ${zip}`, 'ok');
|
|
169
|
+
finish('success');
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
finish('error', errorText(error));
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
return task;
|
|
176
|
+
}
|
|
177
|
+
export function startSign(project, credentials) {
|
|
178
|
+
const { task, log, finish } = createTask('sign', project, 'sign');
|
|
179
|
+
void (async () => {
|
|
180
|
+
try {
|
|
181
|
+
const zipPath = getZipPath(project.root, project.manifest);
|
|
182
|
+
if (!zipExists(zipPath)) {
|
|
183
|
+
throw new Error(`Zip file not found: ${zipPath}. Run build first.`);
|
|
184
|
+
}
|
|
185
|
+
await signOnce(project, task, log, zipPath, credentials);
|
|
186
|
+
finish('success');
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
finish('error', errorText(error));
|
|
190
|
+
}
|
|
191
|
+
})();
|
|
192
|
+
return task;
|
|
193
|
+
}
|
|
194
|
+
export function startDeploy(project, config, options) {
|
|
195
|
+
const { task, log, finish } = createTask('deploy', project, options.production ? 'deploy production' : 'deploy');
|
|
196
|
+
void (async () => {
|
|
197
|
+
try {
|
|
198
|
+
const zipPath = options.production
|
|
199
|
+
? getSignedZipPath(project.root, project.manifest)
|
|
200
|
+
: getZipPath(project.root, project.manifest);
|
|
201
|
+
if (!zipExists(zipPath)) {
|
|
202
|
+
throw new Error(`${options.production ? 'Signed zip' : 'Zip'} not found: ${zipPath}. Run ${options.production ? 'sign' : 'build'} first.`);
|
|
203
|
+
}
|
|
204
|
+
await deployOnce(project, task, log, zipPath, config, options);
|
|
205
|
+
finish('success');
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
finish('error', errorText(error));
|
|
209
|
+
}
|
|
210
|
+
})();
|
|
211
|
+
return task;
|
|
212
|
+
}
|
|
213
|
+
export function startActivate(project, config, executableId, versionLabel) {
|
|
214
|
+
const { task, log, finish } = createTask('activate', project, `activate ${versionLabel}`);
|
|
215
|
+
void (async () => {
|
|
216
|
+
try {
|
|
217
|
+
setPhase(task, 'activating');
|
|
218
|
+
log('act', `PUT activateCustomModuleExecutable · ${versionLabel}`);
|
|
219
|
+
const result = await activateApp(executableId, config, getAppType(project.manifest));
|
|
220
|
+
if (!result.success)
|
|
221
|
+
throw new Error(result.error ?? 'Activation failed');
|
|
222
|
+
log('act', `${versionLabel} is now active`, 'ok');
|
|
223
|
+
finish('success');
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
finish('error', errorText(error));
|
|
227
|
+
}
|
|
228
|
+
})();
|
|
229
|
+
return task;
|
|
230
|
+
}
|
|
231
|
+
export function startInstall(project) {
|
|
232
|
+
const runner = new ProcessRunner('npm', ['install'], project.root);
|
|
233
|
+
const { task, log, finish } = createTask('install', project, 'npm install', () => runner.kill());
|
|
234
|
+
setPhase(task, 'installing');
|
|
235
|
+
runner.on('output', (output) => log('npm', output.data, output.type === 'stderr' ? 'warn' : 'info'));
|
|
236
|
+
runner
|
|
237
|
+
.run()
|
|
238
|
+
.then(result => finish(result.exitCode === 0 ? 'success' : 'error', result.exitCode === 0
|
|
239
|
+
? undefined
|
|
240
|
+
: `npm install exited with ${result.exitCode}`))
|
|
241
|
+
.catch(error => finish('error', errorText(error)));
|
|
242
|
+
return task;
|
|
243
|
+
}
|
|
244
|
+
const WATCH_TARGETS = [
|
|
245
|
+
'src',
|
|
246
|
+
'static',
|
|
247
|
+
'i18n',
|
|
248
|
+
'resource',
|
|
249
|
+
'config',
|
|
250
|
+
'manifest.json',
|
|
251
|
+
];
|
|
252
|
+
/**
|
|
253
|
+
* Dev / watch loop: build on every source change, then optionally sign and
|
|
254
|
+
* deploy. Runs until `task.stop()`; the task stays in the registry meanwhile.
|
|
255
|
+
*/
|
|
256
|
+
export function startDev(project, options) {
|
|
257
|
+
const { root, manifest } = project;
|
|
258
|
+
const watchers = [];
|
|
259
|
+
let webpack = null;
|
|
260
|
+
let debounce;
|
|
261
|
+
let building = false;
|
|
262
|
+
let pending = false;
|
|
263
|
+
const stop = () => {
|
|
264
|
+
clearTimeout(debounce);
|
|
265
|
+
for (const watcher of watchers)
|
|
266
|
+
watcher.close();
|
|
267
|
+
void webpack?.close().catch(() => { });
|
|
268
|
+
finish('stopped');
|
|
269
|
+
};
|
|
270
|
+
const { task, log, finish } = createTask(options.deploy ? 'dev' : 'watch', project, options.deploy ? 'dev' : 'watch', stop);
|
|
271
|
+
const afterBuild = async (zipPath) => {
|
|
272
|
+
let deployZip = zipPath;
|
|
273
|
+
if (options.signingCredentials) {
|
|
274
|
+
deployZip = await signOnce(project, task, log, zipPath, options.signingCredentials);
|
|
275
|
+
}
|
|
276
|
+
if (options.deploy && options.deployConfig) {
|
|
277
|
+
await deployOnce(project, task, log, deployZip, options.deployConfig, {
|
|
278
|
+
force: true,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
setPhase(task, 'watching');
|
|
282
|
+
log('svc', 'watching for changes');
|
|
283
|
+
};
|
|
284
|
+
const fail = (error) => {
|
|
285
|
+
log('svc', errorText(error), 'error');
|
|
286
|
+
setPhase(task, 'error');
|
|
287
|
+
};
|
|
288
|
+
const rebuild = async () => {
|
|
289
|
+
if (building) {
|
|
290
|
+
pending = true;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
building = true;
|
|
294
|
+
try {
|
|
295
|
+
do {
|
|
296
|
+
pending = false;
|
|
297
|
+
try {
|
|
298
|
+
// eslint-disable-next-line no-await-in-loop
|
|
299
|
+
await afterBuild(await buildOnce(project, task, log, 'development'));
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
fail(error);
|
|
303
|
+
}
|
|
304
|
+
} while (pending);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
building = false;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const onChange = (name, file) => {
|
|
311
|
+
log('fs', `changed ${file ?? name}`);
|
|
312
|
+
clearTimeout(debounce);
|
|
313
|
+
debounce = setTimeout(() => {
|
|
314
|
+
void rebuild();
|
|
315
|
+
}, 300);
|
|
316
|
+
};
|
|
317
|
+
const watchFiles = () => {
|
|
318
|
+
for (const name of WATCH_TARGETS) {
|
|
319
|
+
const target = path.join(root, name);
|
|
320
|
+
if (!fs.existsSync(target))
|
|
321
|
+
continue;
|
|
322
|
+
const isDir = fs.statSync(target).isDirectory();
|
|
323
|
+
watchers.push(fs.watch(target, { recursive: isDir }, (_event, file) => {
|
|
324
|
+
onChange(name, file);
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
log('svc', `watching ${WATCH_TARGETS.join(', ')}`);
|
|
328
|
+
};
|
|
329
|
+
void (async () => {
|
|
330
|
+
try {
|
|
331
|
+
cleanBuild(root);
|
|
332
|
+
if (isBundledApp(manifest) && hasLocalWebpackConfig(root)) {
|
|
333
|
+
// Project ships its own webpack config: incremental in-process watch.
|
|
334
|
+
if (!WebpackRunner.isWebpackAvailable(root)) {
|
|
335
|
+
throw new Error('webpack not found. Run npm install.');
|
|
336
|
+
}
|
|
337
|
+
setPhase(task, 'building');
|
|
338
|
+
webpack = new WebpackRunner(root, {
|
|
339
|
+
mode: 'development',
|
|
340
|
+
watch: true,
|
|
341
|
+
cssPrefix: manifest.id,
|
|
342
|
+
restApp: getAppType(manifest) !== 'web' && getAppType(manifest) !== 'widget',
|
|
343
|
+
});
|
|
344
|
+
await webpack.watch(result => {
|
|
345
|
+
void (async () => {
|
|
346
|
+
reportBuild(result, log);
|
|
347
|
+
if (!result.success) {
|
|
348
|
+
setPhase(task, 'error');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
copyStaticToBuild(root);
|
|
353
|
+
await afterBuild(await createBuildZip(root, getFullAppId(manifest.id)));
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
fail(error);
|
|
357
|
+
}
|
|
358
|
+
})();
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
watchFiles();
|
|
363
|
+
await rebuild();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
finish('error', errorText(error));
|
|
368
|
+
}
|
|
369
|
+
})();
|
|
370
|
+
return task;
|
|
371
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ProjectInfo } from './project-detection.js';
|
|
2
|
+
import type { DevProperties } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Find every Sitevision app below `root` (e.g. root/webapps/x, root/restapps/y).
|
|
5
|
+
* Depth-limited walk that skips dependency and output folders.
|
|
6
|
+
*/
|
|
7
|
+
export declare function discoverApps(root: string): ProjectInfo[];
|
|
8
|
+
/** Group label for an app: its parent folder relative to the workspace root. */
|
|
9
|
+
export declare function appGroup(root: string, appRoot: string): string;
|
|
10
|
+
/** Missing something nothing can deploy without. */
|
|
11
|
+
export declare function configIncomplete(dev?: Partial<DevProperties>): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* True when a workspace has apps but nothing usable to deploy with: neither the
|
|
14
|
+
* shared root config nor the apps themselves carry domain/site/username. The
|
|
15
|
+
* shell then opens on Workspace settings instead of the first app.
|
|
16
|
+
*/
|
|
17
|
+
export declare function needsOnboarding(root: string, apps: ProjectInfo[]): boolean;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { detectProject, readWorkspaceDevProperties, } from './project-detection.js';
|
|
4
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build']);
|
|
5
|
+
const MAX_DEPTH = 3;
|
|
6
|
+
/**
|
|
7
|
+
* Find every Sitevision app below `root` (e.g. root/webapps/x, root/restapps/y).
|
|
8
|
+
* Depth-limited walk that skips dependency and output folders.
|
|
9
|
+
*/
|
|
10
|
+
export function discoverApps(root) {
|
|
11
|
+
const found = [];
|
|
12
|
+
const walk = (dir, depth) => {
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name))
|
|
22
|
+
continue;
|
|
23
|
+
if (entry.name.startsWith('.'))
|
|
24
|
+
continue;
|
|
25
|
+
const full = path.join(dir, entry.name);
|
|
26
|
+
let project = null;
|
|
27
|
+
try {
|
|
28
|
+
project = detectProject(full);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Unparseable manifest: skip it; the app can still be opened directly.
|
|
32
|
+
}
|
|
33
|
+
if (project) {
|
|
34
|
+
found.push(project);
|
|
35
|
+
}
|
|
36
|
+
else if (depth < MAX_DEPTH) {
|
|
37
|
+
walk(full, depth + 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
walk(root, 1);
|
|
42
|
+
// Group first, then path: a deeper folder (webapps/nested) must not split
|
|
43
|
+
// its parent's run of apps, or the same group heading renders twice.
|
|
44
|
+
return found.toSorted((a, b) => path.dirname(a.root).localeCompare(path.dirname(b.root)) ||
|
|
45
|
+
a.root.localeCompare(b.root));
|
|
46
|
+
}
|
|
47
|
+
/** Group label for an app: its parent folder relative to the workspace root. */
|
|
48
|
+
export function appGroup(root, appRoot) {
|
|
49
|
+
const relative = path.relative(root, path.dirname(appRoot));
|
|
50
|
+
return relative === '' ? '.' : relative;
|
|
51
|
+
}
|
|
52
|
+
/** Missing something nothing can deploy without. */
|
|
53
|
+
export function configIncomplete(dev) {
|
|
54
|
+
if (!dev?.domain || !dev.siteName)
|
|
55
|
+
return true;
|
|
56
|
+
return (dev.authMethod ?? 'basic') === 'basic' && !dev.username;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* True when a workspace has apps but nothing usable to deploy with: neither the
|
|
60
|
+
* shared root config nor the apps themselves carry domain/site/username. The
|
|
61
|
+
* shell then opens on Workspace settings instead of the first app.
|
|
62
|
+
*/
|
|
63
|
+
export function needsOnboarding(root, apps) {
|
|
64
|
+
return (apps.length > 0 &&
|
|
65
|
+
configIncomplete(readWorkspaceDevProperties(root)) &&
|
|
66
|
+
apps.some(app => configIncomplete(app.devProperties)));
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sitevision-cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"svc": "dist/cli.js"
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"ink": "^7.1.0",
|
|
25
25
|
"ink-spinner": "^5.0.0",
|
|
26
26
|
"meow": "^14.1.0",
|
|
27
|
+
"open": "^11.0.3",
|
|
28
|
+
"puppeteer-core": "^25.10.0",
|
|
27
29
|
"react": "^19.2.7"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|