sitevision-cli 1.0.0-beta.13 → 1.0.0-beta.15
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/cli.js +70 -41
- package/dist/commands/build.js +1 -1
- package/dist/commands/dev.d.ts +8 -10
- package/dist/commands/dev.js +75 -390
- package/dist/commands/watch.js +5 -23
- package/dist/components/AuthLoginScreen.js +2 -1
- package/dist/components/DevPropertiesForm.js +6 -3
- package/dist/components/PasswordInput.js +2 -1
- 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 +35 -0
- package/dist/shell/ConfigForm.js +472 -0
- package/dist/shell/Frame.d.ts +52 -0
- package/dist/shell/Frame.js +98 -0
- package/dist/shell/Settings.d.ts +6 -0
- package/dist/shell/Settings.js +96 -0
- package/dist/shell/Shell.d.ts +8 -0
- package/dist/shell/Shell.js +520 -0
- package/dist/shell/Tabs.d.ts +36 -0
- package/dist/shell/Tabs.js +85 -0
- package/dist/shell/actions.d.ts +45 -0
- package/dist/shell/actions.js +0 -0
- package/dist/types/index.d.ts +12 -2
- 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 +263 -0
- package/dist/utils/oauth2-auth.d.ts +1 -0
- package/dist/utils/oauth2-auth.js +9 -12
- package/dist/utils/project-detection.d.ts +23 -1
- package/dist/utils/project-detection.js +124 -51
- package/dist/utils/sitevision-api.d.ts +35 -0
- package/dist/utils/sitevision-api.js +74 -1
- package/dist/utils/tasks.d.ts +48 -0
- package/dist/utils/tasks.js +371 -0
- package/dist/utils/workspace.d.ts +8 -0
- package/dist/utils/workspace.js +48 -0
- package/package.json +2 -1
- package/readme.md +76 -24
|
@@ -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,8 @@
|
|
|
1
|
+
import { type ProjectInfo } from './project-detection.js';
|
|
2
|
+
/**
|
|
3
|
+
* Find every Sitevision app below `root` (e.g. root/webapps/x, root/restapps/y).
|
|
4
|
+
* Depth-limited walk that skips dependency and output folders.
|
|
5
|
+
*/
|
|
6
|
+
export declare function discoverApps(root: string): ProjectInfo[];
|
|
7
|
+
/** Group label for an app: its parent folder relative to the workspace root. */
|
|
8
|
+
export declare function appGroup(root: string, appRoot: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { detectProject } 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
|
+
return found.toSorted((a, b) => a.root.localeCompare(b.root));
|
|
43
|
+
}
|
|
44
|
+
/** Group label for an app: its parent folder relative to the workspace root. */
|
|
45
|
+
export function appGroup(root, appRoot) {
|
|
46
|
+
const relative = path.relative(root, path.dirname(appRoot));
|
|
47
|
+
return relative === '' ? '.' : relative;
|
|
48
|
+
}
|
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.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"svc": "dist/cli.js"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"ink": "^7.1.0",
|
|
25
25
|
"ink-spinner": "^5.0.0",
|
|
26
26
|
"meow": "^14.1.0",
|
|
27
|
+
"open": "^11.0.3",
|
|
27
28
|
"puppeteer-core": "^25.10.0",
|
|
28
29
|
"react": "^19.2.7"
|
|
29
30
|
},
|
package/readme.md
CHANGED
|
@@ -25,33 +25,85 @@ npm install --global sitevision-cli
|
|
|
25
25
|
|
|
26
26
|
The CLI must be run inside a Sitevision project directory (containing a `manifest.json`).
|
|
27
27
|
|
|
28
|
-
### Interactive
|
|
28
|
+
### Interactive shell
|
|
29
|
+
|
|
30
|
+
Run `svc` with no arguments to open the full-screen shell. It works in two
|
|
31
|
+
places:
|
|
32
|
+
|
|
33
|
+
- **Inside an app** (a directory with `manifest.json`): single-app mode.
|
|
34
|
+
- **At the root of a repo** that contains apps in subfolders such as
|
|
35
|
+
`webapps/*`, `restapps/*` or `widgets/*`: workspace mode, with every app in
|
|
36
|
+
the left navigator and per-app status dots (dependencies, config,
|
|
37
|
+
package.json sync, signing).
|
|
38
|
+
|
|
39
|
+
The right pane has four tabs: **Overview**, **Config** (the whole
|
|
40
|
+
`.dev_properties.json` as one form, plus signing and keychain secrets; `Tab`
|
|
41
|
+
moves between fields, `Enter` saves, `Ctrl+O` on the addon field picks an
|
|
42
|
+
addon from the site's Addon Repository), **Versions** (the versions uploaded
|
|
43
|
+
to the site, `a` activates one) and **Log** (streaming build and deploy
|
|
44
|
+
output). Dev and watch keep running in the background while you navigate
|
|
45
|
+
between apps.
|
|
46
|
+
|
|
47
|
+
Single-letter keys drive everything; the bottom bar shows the ones that apply.
|
|
48
|
+
`/` opens the command palette with every action, `Tab` switches between the
|
|
49
|
+
navigator and the content pane, `1`–`4` pick a tab, `q` quits.
|
|
50
|
+
|
|
51
|
+
| Key | Action |
|
|
52
|
+
| --------------- | ----------------------------------------------------------------- |
|
|
53
|
+
| `d` / `w` | Dev (build, sign, deploy on change) / Watch (build and sign only) |
|
|
54
|
+
| `b` / `s` | Build / Sign |
|
|
55
|
+
| `p` / `P` | Deploy to dev / force deploy |
|
|
56
|
+
| `a` | Versions tab: list and activate remote versions |
|
|
57
|
+
| `e` / `y` / `l` | Edit dev properties / apply package.json sync / log in |
|
|
58
|
+
| `K` | Stop the running task for the selected app |
|
|
59
|
+
|
|
60
|
+
### Settings
|
|
61
|
+
|
|
62
|
+
`,` (or "Settings" in the palette) opens the global preferences, stored in
|
|
63
|
+
`~/.config/sitevision-cli/config.json`: the UI language (English or Swedish,
|
|
64
|
+
which also picks the manifest name language) and whether the intro animation
|
|
65
|
+
plays. In workspace mode the same screen has a row that jumps to the shared
|
|
66
|
+
workspace config.
|
|
67
|
+
|
|
68
|
+
### Environments
|
|
69
|
+
|
|
70
|
+
The top-level fields of `.dev_properties.json` are one environment, called
|
|
71
|
+
**dev** unless `baseEnvironment` says otherwise (a repo that only has a
|
|
72
|
+
production site can set `"baseEnvironment": "prod"`). Add more under
|
|
73
|
+
`environments`, overriding only what differs:
|
|
29
74
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"domain": "acme-use.sitevision-cloud.se",
|
|
78
|
+
"siteName": "Intranet",
|
|
79
|
+
"username": "me@acme.se",
|
|
80
|
+
"environments": {
|
|
81
|
+
"test": {"domain": "acme-tse.sitevision-cloud.se"},
|
|
82
|
+
"prod": {"domain": "acme.sitevision-cloud.se", "authMethod": "oauth2"}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
34
85
|
```
|
|
35
86
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
87
|
+
`E` cycles the active environment (also "Switch environment" and "Add
|
|
88
|
+
environment" in the palette); the choice is remembered in `.svcconfig`. The
|
|
89
|
+
top bar shows a badge, green for dev, yellow for others, red for production.
|
|
90
|
+
Versions, deploy, login state and the Config tab all follow the active
|
|
91
|
+
environment; on a non-dev environment the Config tab edits that environment's
|
|
92
|
+
overrides. Override names containing `prod`, or any environment with `"production":
|
|
93
|
+
true`, are production: deploy needs the signed zip, confirms, and activates,
|
|
94
|
+
and dev or watch refuse to run against them. The base environment is never
|
|
95
|
+
production by name, only by the flag, so a prod-only repo keeps its dev loop.
|
|
96
|
+
Both settings have rows in the Config tab under ENVIRONMENT.
|
|
97
|
+
|
|
98
|
+
### Shared configuration in a workspace
|
|
99
|
+
|
|
100
|
+
`.dev_properties.json` is resolved by merging every ancestor directory's file
|
|
101
|
+
(up to the repo root) under the app's own file, nearest wins. Put the shared
|
|
102
|
+
fields (`domain`, `siteName`, `username`, `authMethod`, `oauth2`,
|
|
103
|
+
`signingUsername`, ...) once at the repo root and keep only `addonName` in each
|
|
104
|
+
app. The Config tab marks inherited values with `↑ root`, and saving an app's
|
|
105
|
+
config never copies inherited values into the app file. Keychain entries are
|
|
106
|
+
keyed by domain and username, so one login covers every app on the site.
|
|
55
107
|
|
|
56
108
|
### Direct Commands
|
|
57
109
|
|