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,10 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { EventEmitter } from 'node:events';
4
+ import { stripVTControlCharacters } from 'node:util';
4
5
  import { useSyncExternalStore } from 'react';
5
6
  import { WebpackRunner, hasLocalWebpackConfig } from './webpack-runner.js';
6
7
  import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from './sitevision-scripts-runner.js';
7
- import { copyStaticToBuild, copySrcToBuild, cleanBuild, createBuildZip, zipExists, } from './zip.js';
8
+ import { copyStaticToBuild, copySrcToBuild, cleanBuild, createBuildZip, formatFileSize, getZipSize, zipExists, } from './zip.js';
9
+ import { deleteSessionCookie } from './keychain.js';
8
10
  import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, getDeployZipPath, localizedText, } from './project-detection.js';
9
11
  import { signApp, deployApp, deployProduction, activateApp, createAddon, listAddons, classifyAddon, } from './sitevision-api.js';
10
12
  import { ProcessRunner } from './process-runner.js';
@@ -28,11 +30,19 @@ export function useTasks() {
28
30
  export function runningTasks(appRoot) {
29
31
  return tasks.filter(t => t.status === 'running' && (!appRoot || t.appRoot === appRoot));
30
32
  }
33
+ /** A scaffold belongs to no listed app yet, so it counts for every one. */
34
+ export function scaffoldRunning() {
35
+ return runningTasks().some(t => t.kind === 'create');
36
+ }
37
+ /** What "stop" means for the selected app: its own tasks and any scaffold. */
38
+ export function stoppableTasks(appRoot) {
39
+ return runningTasks().filter(t => t.kind === 'create' || t.appRoot === appRoot);
40
+ }
31
41
  export function clearFinished() {
32
42
  tasks = tasks.filter(t => t.status === 'running');
33
43
  notify();
34
44
  }
35
- function createTask(kind, project, label, stop = () => { }) {
45
+ export function createTask(kind, project, label, stop = () => { }) {
36
46
  const task = {
37
47
  id: nextId++,
38
48
  kind,
@@ -48,7 +58,7 @@ function createTask(kind, project, label, stop = () => { }) {
48
58
  tasks.push(task);
49
59
  notify();
50
60
  const log = (tag, text, level = 'info') => {
51
- for (const line of text.trimEnd().split('\n')) {
61
+ for (const line of logLines(text)) {
52
62
  task.lines.push({ time: Date.now(), tag, level, text: line });
53
63
  }
54
64
  if (task.lines.length > MAX_LINES) {
@@ -69,7 +79,7 @@ function createTask(kind, project, label, stop = () => { }) {
69
79
  };
70
80
  return { task, log, finish };
71
81
  }
72
- function setPhase(task, phase) {
82
+ export function setPhase(task, phase) {
73
83
  task.phase = phase;
74
84
  notify();
75
85
  }
@@ -78,7 +88,9 @@ const errorText = (error) => error instanceof Error ? error.message : String(err
78
88
  // One-shot steps shared by the tasks below.
79
89
  // ---------------------------------------------------------------------------
80
90
  /** Build the app to dist/<appId>.zip. Returns the zip path or throws. */
81
- async function buildOnce(project, task, log, mode, signal) {
91
+ async function buildOnce(project, task, log, mode, signal,
92
+ // False (`svc build --no-zip`): leave build/ and no zip.
93
+ zip = true) {
82
94
  const { root, manifest } = project;
83
95
  cleanBuild(root);
84
96
  if (isBundledApp(manifest) && !hasLocalWebpackConfig(root)) {
@@ -97,7 +109,10 @@ async function buildOnce(project, task, log, mode, signal) {
97
109
  if (!zipExists(zipPath)) {
98
110
  throw new Error(`Build reported success but no zip was found at ${zipPath}.`);
99
111
  }
100
- return zipPath;
112
+ // sitevision-scripts always zips; without a zip means taking it away.
113
+ if (!zip)
114
+ fs.rmSync(zipPath);
115
+ return zip ? zipPath : undefined;
101
116
  }
102
117
  if (isBundledApp(manifest)) {
103
118
  if (!WebpackRunner.isWebpackAvailable(root)) {
@@ -123,6 +138,8 @@ async function buildOnce(project, task, log, mode, signal) {
123
138
  copySrcToBuild(root);
124
139
  copyStaticToBuild(root);
125
140
  }
141
+ if (!zip)
142
+ return undefined;
126
143
  setPhase(task, 'zipping');
127
144
  return createBuildZip(root, getFullAppId(manifest.id));
128
145
  }
@@ -170,15 +187,26 @@ async function deployOnce(project, task, log, zipPath, config, options) {
170
187
  }
171
188
  }
172
189
  }
190
+ // A stale session must not be replayed by the next run. Not when it came
191
+ // from SITEVISION_SESSION_COOKIE: that one would just be read again.
192
+ if (!result.success &&
193
+ result.authExpired &&
194
+ config.sessionCookie &&
195
+ !process.env['SITEVISION_SESSION_COOKIE']) {
196
+ deleteSessionCookie(config.domain, config.username);
197
+ }
173
198
  if (!result.success)
174
199
  throw new Error(result.error ?? 'Deployment failed');
200
+ if (options.production && options.activate && !result.activated) {
201
+ throw new Error(result.message ?? 'Deployed, but not activated');
202
+ }
175
203
  log('dep', `${result.message ?? 'deployed'}${result.executableId ? ` · exec ${result.executableId}` : ''}`, 'ok');
176
204
  return result.executableId;
177
205
  }
178
206
  // ---------------------------------------------------------------------------
179
207
  // Tasks
180
208
  // ---------------------------------------------------------------------------
181
- export function startBuild(project) {
209
+ export function startBuild(project, { zip = true } = {}) {
182
210
  const controller = new AbortController();
183
211
  const { task, log, finish } = createTask('build', project, 'build', () => {
184
212
  controller.abort();
@@ -186,8 +214,10 @@ export function startBuild(project) {
186
214
  });
187
215
  void (async () => {
188
216
  try {
189
- const zip = await buildOnce(project, task, log, 'production', controller.signal);
190
- log('bld', `created ${zip}`, 'ok');
217
+ const built = await buildOnce(project, task, log, 'production', controller.signal, zip);
218
+ log('bld', built
219
+ ? `created ${built} · ${formatFileSize(getZipSize(built))}`
220
+ : 'built, zip skipped', 'ok');
191
221
  finish('success');
192
222
  }
193
223
  catch (error) {
@@ -266,6 +296,7 @@ export function startInstall(project) {
266
296
  .catch(error => finish('error', errorText(error)));
267
297
  return task;
268
298
  }
299
+ const WEBPACK_UNSEEN_TARGETS = ['static', 'manifest.json'];
269
300
  const WATCH_TARGETS = [
270
301
  'src',
271
302
  'static',
@@ -283,8 +314,6 @@ export function startDev(project, options) {
283
314
  const watchers = [];
284
315
  let webpack = null;
285
316
  let debounce;
286
- let building = false;
287
- let pending = false;
288
317
  const controller = new AbortController();
289
318
  const stop = () => {
290
319
  controller.abort();
@@ -318,36 +347,24 @@ export function startDev(project, options) {
318
347
  log('svc', errorText(error), 'error');
319
348
  setPhase(task, 'error');
320
349
  };
321
- const rebuild = async () => {
322
- if (building) {
323
- pending = true;
350
+ const rebuild = serialized(async () => {
351
+ if (controller.signal.aborted)
324
352
  return;
325
- }
326
- building = true;
327
353
  try {
328
- do {
329
- pending = false;
330
- try {
331
- // eslint-disable-next-line no-await-in-loop
332
- const zip = await buildOnce(project, task, log, 'development', controller.signal);
333
- // eslint-disable-next-line no-await-in-loop
334
- await afterBuild(zip);
335
- }
336
- catch (error) {
337
- fail(error);
338
- }
339
- } while (pending && !controller.signal.aborted);
354
+ const zip = await buildOnce(project, task, log, 'development', controller.signal);
355
+ if (zip)
356
+ await afterBuild(zip);
340
357
  }
341
- finally {
342
- building = false;
358
+ catch (error) {
359
+ fail(error);
343
360
  }
344
- };
361
+ });
345
362
  // Size + mtime of every watched file. An event that leaves this unchanged
346
363
  // (editor metadata, xattrs, identical re-save) is noise and must not
347
364
  // rebuild, or a stray event during sign/deploy loops forever.
348
- const snapshot = () => {
365
+ const snapshot = (targets) => {
349
366
  const entries = [];
350
- for (const name of WATCH_TARGETS) {
367
+ for (const name of targets) {
351
368
  const target = path.join(root, name);
352
369
  const stat = fs.statSync(target, { throwIfNoEntry: false });
353
370
  if (!stat)
@@ -367,22 +384,22 @@ export function startDev(project, options) {
367
384
  return entries.toSorted().join('\n');
368
385
  };
369
386
  let fingerprint = '';
370
- const onChange = (name, event, file) => {
371
- const where = file ? path.join(name, file) : name;
372
- clearTimeout(debounce);
373
- debounce = setTimeout(() => {
374
- const next = snapshot();
375
- if (next === fingerprint) {
376
- log('fs', `${event} ${where} · no file changed, ignored`, 'warn');
377
- return;
378
- }
379
- fingerprint = next;
380
- log('fs', `${event} ${where}`);
381
- void rebuild();
382
- }, 300);
383
- };
384
- const watchFiles = () => {
385
- for (const name of WATCH_TARGETS) {
387
+ const watchFiles = (targets, changed) => {
388
+ const onChange = (name, event, file) => {
389
+ const where = file ? path.join(name, file) : name;
390
+ clearTimeout(debounce);
391
+ debounce = setTimeout(() => {
392
+ const next = snapshot(targets);
393
+ if (next === fingerprint) {
394
+ log('fs', `${event} ${where} · no file changed, ignored`, 'warn');
395
+ return;
396
+ }
397
+ fingerprint = next;
398
+ log('fs', `${event} ${where}`);
399
+ changed();
400
+ }, 300);
401
+ };
402
+ for (const name of targets) {
386
403
  const target = path.join(root, name);
387
404
  if (!fs.existsSync(target))
388
405
  continue;
@@ -391,8 +408,8 @@ export function startDev(project, options) {
391
408
  onChange(name, event, file);
392
409
  }));
393
410
  }
394
- fingerprint = snapshot();
395
- log('svc', `watching ${WATCH_TARGETS.join(', ')}`);
411
+ fingerprint = snapshot(targets);
412
+ log('svc', `watching ${targets.join(', ')}`);
396
413
  };
397
414
  void (async () => {
398
415
  try {
@@ -409,25 +426,37 @@ export function startDev(project, options) {
409
426
  cssPrefix: manifest.id,
410
427
  restApp: getAppType(manifest) !== 'web' && getAppType(manifest) !== 'widget',
411
428
  });
429
+ // Zip, sign and deploy what is in build/ now. Webpack compiles and
430
+ // static edits both end here, one at a time, the latest one last.
431
+ let compiled = false;
432
+ const repackage = serialized(async () => {
433
+ if (!compiled || controller.signal.aborted)
434
+ return;
435
+ try {
436
+ copyStaticToBuild(root);
437
+ await afterBuild(await createBuildZip(root, getFullAppId(manifest.id)));
438
+ }
439
+ catch (error) {
440
+ fail(error);
441
+ }
442
+ });
412
443
  await webpack.watch(result => {
413
- void (async () => {
414
- reportBuild(result, log);
415
- if (!result.success) {
416
- setPhase(task, 'error');
417
- return;
418
- }
419
- try {
420
- copyStaticToBuild(root);
421
- await afterBuild(await createBuildZip(root, getFullAppId(manifest.id)));
422
- }
423
- catch (error) {
424
- fail(error);
425
- }
426
- })();
444
+ reportBuild(result, log);
445
+ compiled = result.success;
446
+ if (result.success)
447
+ void repackage();
448
+ else
449
+ setPhase(task, 'error');
450
+ });
451
+ // Webpack watches what it bundles; what is copied in as-is it never sees.
452
+ watchFiles(WEBPACK_UNSEEN_TARGETS, () => {
453
+ void repackage();
427
454
  });
428
455
  }
429
456
  else {
430
- watchFiles();
457
+ watchFiles(WATCH_TARGETS, () => {
458
+ void rebuild();
459
+ });
431
460
  await rebuild();
432
461
  }
433
462
  }
@@ -437,3 +466,40 @@ export function startDev(project, options) {
437
466
  })();
438
467
  return task;
439
468
  }
469
+ /**
470
+ * Run `run` one call at a time. Calls made meanwhile collapse into one run with
471
+ * the latest value, so of several quick saves the last is what gets deployed
472
+ * last, never an older one that happened to upload slower.
473
+ */
474
+ export function serialized(run) {
475
+ let running = false;
476
+ let waiting;
477
+ return async (value) => {
478
+ waiting = { value };
479
+ if (running)
480
+ return;
481
+ running = true;
482
+ try {
483
+ while (waiting) {
484
+ const { value: current } = waiting;
485
+ waiting = undefined;
486
+ // eslint-disable-next-line no-await-in-loop
487
+ await run(current);
488
+ }
489
+ }
490
+ finally {
491
+ running = false;
492
+ }
493
+ };
494
+ }
495
+ /**
496
+ * Child output as log rows: split on LF or CRLF, no colour or cursor codes, and
497
+ * of a line redrawn with bare CRs (progress bars) only the final state. Any of
498
+ * those left in would move the cursor inside Ink's frame.
499
+ */
500
+ export function logLines(text) {
501
+ return stripVTControlCharacters(text)
502
+ .trimEnd()
503
+ .split(/\r?\n/)
504
+ .map(line => line.slice(line.lastIndexOf('\r') + 1));
505
+ }
@@ -1,9 +1,9 @@
1
1
  import https from 'node:https';
2
2
  /**
3
3
  * True if version `a` is strictly greater than `b`, comparing the numeric
4
- * MAJOR.MINOR.PATCH core. Pre-release suffixes (e.g. `-beta.1`) are ignored.
4
+ * MAJOR.MINOR.PATCH core. With equal cores, a stable `a` beats a pre-release `b`.
5
5
  */
6
- function isNewer(a, b) {
6
+ export function isNewer(a, b) {
7
7
  const core = (v) => (v.split('-')[0] ?? '').split('.');
8
8
  const pa = core(a);
9
9
  const pb = core(b);
@@ -15,7 +15,7 @@ function isNewer(a, b) {
15
15
  if (da < db)
16
16
  return false;
17
17
  }
18
- return false;
18
+ return !a.includes('-') && b.includes('-');
19
19
  }
20
20
  /**
21
21
  * Check the npm registry for a newer published version of `packageName`.
@@ -7,7 +7,9 @@
7
7
  import path from 'path';
8
8
  import fs from 'fs';
9
9
  import { createRequire } from 'module';
10
+ import { pathToFileURL } from 'url';
10
11
  import { copyChunksToResources } from './zip.js';
12
+ import { findInstalledPackage } from './sitevision-scripts-runner.js';
11
13
  // =============================================================================
12
14
  // LOCAL CONFIG DETECTION
13
15
  // =============================================================================
@@ -56,8 +58,7 @@ export class WebpackRunner {
56
58
  return;
57
59
  }
58
60
  // Load webpack from project's node_modules
59
- const webpackPath = path.join(this.projectRoot, 'node_modules', 'webpack');
60
- if (!fs.existsSync(webpackPath)) {
61
+ if (!WebpackRunner.isWebpackAvailable(this.projectRoot)) {
61
62
  throw new Error('webpack not found in project. Make sure webpack is installed: npm install webpack');
62
63
  }
63
64
  try {
@@ -83,7 +84,8 @@ export class WebpackRunner {
83
84
  throw new Error('webpack.config.js not found. Make sure your project has a webpack configuration.');
84
85
  }
85
86
  try {
86
- const configModule = await import(configPath);
87
+ // A bare Windows path (C:\\...) is not a valid import specifier.
88
+ const configModule = await import(pathToFileURL(configPath).href);
87
89
  const configFactory = configModule.default || configModule;
88
90
  // If it's a function, call it with options
89
91
  if (typeof configFactory === 'function') {
@@ -225,8 +227,7 @@ export class WebpackRunner {
225
227
  * Check if webpack is available in the project
226
228
  */
227
229
  static isWebpackAvailable(projectRoot) {
228
- const webpackPath = path.join(projectRoot, 'node_modules', 'webpack');
229
- return fs.existsSync(webpackPath);
230
+ return findInstalledPackage(projectRoot, 'webpack') !== null;
230
231
  }
231
232
  /**
232
233
  * Get the webpack version from the project
@@ -7,7 +7,9 @@ const MAX_DEPTH = 3;
7
7
  * Find every Sitevision app below `root` (e.g. root/webapps/x, root/restapps/y).
8
8
  * Depth-limited walk that skips dependency and output folders.
9
9
  */
10
- export function discoverApps(root) {
10
+ export function discoverApps(root,
11
+ // Filled with why each app that was left out is broken.
12
+ skipped = []) {
11
13
  const found = [];
12
14
  const walk = (dir, depth) => {
13
15
  let entries;
@@ -27,8 +29,10 @@ export function discoverApps(root) {
27
29
  try {
28
30
  project = detectProject(full);
29
31
  }
30
- catch {
31
- // Unparseable manifest: skip it; the app can still be opened directly.
32
+ catch (error) {
33
+ // A broken app is left out, said out loud, and not searched for more.
34
+ skipped.push(error instanceof Error ? error.message : String(error));
35
+ continue;
32
36
  }
33
37
  if (project) {
34
38
  found.push(project);
@@ -55,6 +59,40 @@ export function configIncomplete(dev) {
55
59
  return true;
56
60
  return (dev.authMethod ?? 'basic') === 'basic' && !dev.username;
57
61
  }
62
+ /**
63
+ * What a deploy is sent with, or which required value is missing. The one place
64
+ * dev properties become a DeployConfig, so nothing ever posts to "undefined".
65
+ */
66
+ export function toDeployConfig(dev,
67
+ // False for what talks to the site rather than an addon: listing its addons
68
+ // (how an empty addon name gets picked) and logging in.
69
+ { addon = true } = {}) {
70
+ const required = {
71
+ domain: dev?.domain,
72
+ siteName: dev?.siteName,
73
+ ...(addon && { addonName: dev?.addonName }),
74
+ // A token or cookie login has no use for a username.
75
+ ...((dev?.authMethod ?? 'basic') === 'basic' && { username: dev?.username }),
76
+ };
77
+ const missing = Object.entries(required).find(([, value]) => !value)?.[0];
78
+ if (!dev || missing) {
79
+ return {
80
+ error: `Deploy config is missing "${missing ?? 'domain'}". Run svc and press e to set it, or add it to .dev_properties.json.`,
81
+ };
82
+ }
83
+ return {
84
+ config: {
85
+ domain: dev.domain,
86
+ siteName: dev.siteName,
87
+ addonName: dev.addonName ?? '',
88
+ username: dev.username ?? '',
89
+ password: dev.password,
90
+ accessToken: dev.accessToken,
91
+ sessionCookie: dev.sessionCookie,
92
+ useHTTP: dev.useHTTPForDevDeploy,
93
+ },
94
+ };
95
+ }
58
96
  /**
59
97
  * True when a workspace has apps but nothing usable to deploy with: neither the
60
98
  * shared root config nor the apps themselves carry domain/site/username. The
package/dist/utils/zip.js CHANGED
@@ -114,7 +114,7 @@ async function buildZipBuffer(entries) {
114
114
  const localHeader = Buffer.alloc(30);
115
115
  localHeader.writeUInt32LE(0x04034b50, 0);
116
116
  localHeader.writeUInt16LE(20, 4); // version needed to extract
117
- localHeader.writeUInt16LE(0, 6); // general purpose flag
117
+ localHeader.writeUInt16LE(0x08_00, 6); // general purpose flag: UTF-8 names
118
118
  localHeader.writeUInt16LE(method, 8);
119
119
  localHeader.writeUInt16LE(dosTime, 10);
120
120
  localHeader.writeUInt16LE(dosDate, 12);
@@ -130,7 +130,7 @@ async function buildZipBuffer(entries) {
130
130
  centralHeader.writeUInt32LE(0x02014b50, 0);
131
131
  centralHeader.writeUInt16LE(20, 4); // version made by
132
132
  centralHeader.writeUInt16LE(20, 6); // version needed
133
- centralHeader.writeUInt16LE(0, 8); // general purpose flag
133
+ centralHeader.writeUInt16LE(0x08_00, 8); // general purpose flag: UTF-8 names
134
134
  centralHeader.writeUInt16LE(method, 10);
135
135
  centralHeader.writeUInt16LE(dosTime, 12);
136
136
  centralHeader.writeUInt16LE(dosDate, 14);
@@ -228,6 +228,9 @@ export async function createBuildZip(projectRoot, appId) {
228
228
  if (!fs.existsSync(buildDir)) {
229
229
  throw new Error(`Build directory not found: ${buildDir}. Run build first.`);
230
230
  }
231
+ if (!fs.existsSync(path.join(buildDir, 'manifest.json'))) {
232
+ throw new Error(`${buildDir} has no manifest.json, so the zip would not be an app. manifest.json belongs in static/ (or src/ for an app that is not bundled).`);
233
+ }
231
234
  return createZip(buildDir, zipPath);
232
235
  }
233
236
  // =============================================================================
@@ -268,15 +271,17 @@ export function copyChunksToResources(buildDir) {
268
271
  export function copyStaticToBuild(projectRoot) {
269
272
  const staticDir = path.join(projectRoot, 'static');
270
273
  const buildDir = path.join(projectRoot, 'build');
271
- if (!fs.existsSync(staticDir)) {
272
- return;
273
- }
274
- // Ensure build directory exists
275
- if (!fs.existsSync(buildDir)) {
276
- fs.mkdirSync(buildDir, { recursive: true });
274
+ fs.mkdirSync(buildDir, { recursive: true });
275
+ if (fs.existsSync(staticDir))
276
+ copyDirRecursive(staticDir, buildDir);
277
+ // Every build ends here. A manifest kept at the project root is detected,
278
+ // so it ships when neither src/ nor static/ brought one.
279
+ const manifestIn = (dir) => path.join(projectRoot, dir, 'manifest.json');
280
+ if (fs.existsSync(manifestIn('.')) &&
281
+ !fs.existsSync(manifestIn('src')) &&
282
+ !fs.existsSync(manifestIn('static'))) {
283
+ fs.copyFileSync(manifestIn('.'), path.join(buildDir, 'manifest.json'));
277
284
  }
278
- // Copy static directory contents to build
279
- copyDirRecursive(staticDir, buildDir);
280
285
  }
281
286
  /**
282
287
  * Copy source files to build directory (for non-bundled apps)
@@ -286,15 +291,9 @@ export function copyStaticToBuild(projectRoot) {
286
291
  export function copySrcToBuild(projectRoot) {
287
292
  const srcDir = path.join(projectRoot, 'src');
288
293
  const buildDir = path.join(projectRoot, 'build');
289
- if (!fs.existsSync(srcDir)) {
290
- return;
291
- }
292
- // Ensure build directory exists
293
- if (!fs.existsSync(buildDir)) {
294
- fs.mkdirSync(buildDir, { recursive: true });
295
- }
296
- // Copy src directory contents to build
297
- copyDirRecursive(srcDir, buildDir);
294
+ fs.mkdirSync(buildDir, { recursive: true });
295
+ if (fs.existsSync(srcDir))
296
+ copyDirRecursive(srcDir, buildDir);
298
297
  }
299
298
  /**
300
299
  * Recursively copy a directory
package/package.json CHANGED
@@ -1,7 +1,22 @@
1
1
  {
2
2
  "name": "sitevision-cli",
3
- "version": "1.0.0-beta.31",
3
+ "version": "1.0.0-beta.33",
4
+ "description": "Build, sign and deploy Sitevision apps from the terminal",
4
5
  "license": "MIT",
6
+ "author": "Rasmus Söderström",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/xrasod/SitevisionCli.git"
10
+ },
11
+ "homepage": "https://github.com/xrasod/SitevisionCli#readme",
12
+ "bugs": "https://github.com/xrasod/SitevisionCli/issues",
13
+ "keywords": [
14
+ "sitevision",
15
+ "cli",
16
+ "webapp",
17
+ "restapp",
18
+ "deploy"
19
+ ],
5
20
  "bin": {
6
21
  "svc": "dist/cli.js"
7
22
  },
@@ -10,9 +25,10 @@
10
25
  "node": ">=22"
11
26
  },
12
27
  "scripts": {
13
- "build": "tsc",
28
+ "build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc && node -e \"fs.chmodSync('dist/cli.js',0o755)\"",
14
29
  "dev": "tsc --watch",
15
30
  "test": "prettier --check . && xo && ava",
31
+ "prepublishOnly": "npm test && npm run build",
16
32
  "release": "./scripts/publish.sh",
17
33
  "release:beta": "./scripts/publish-beta.sh"
18
34
  },
@@ -52,7 +68,12 @@
52
68
  ],
53
69
  "nodeArguments": [
54
70
  "--import=tsx"
55
- ]
71
+ ],
72
+ "environmentVariables": {
73
+ "FORCE_COLOR": "0",
74
+ "SVC_NO_KEYCHAIN": "memory",
75
+ "XDG_CONFIG_HOME": "test/.xdg"
76
+ }
56
77
  },
57
78
  "prettier": "@vdemedes/prettier-config"
58
79
  }
package/readme.md CHANGED
@@ -60,7 +60,7 @@ and `p` to deploy and activate.
60
60
  | `1`–`4` | Overview · Config · Versions · Log |
61
61
  | `/` | Command palette |
62
62
  | `?` | Help: every key in one place |
63
- | `,` | Settings (language, intro animation) |
63
+ | `,` | Settings (language, update check, hints) |
64
64
  | `Tab` / `Esc` | Switch pane / back |
65
65
  | `q` | Quit |
66
66
 
@@ -72,17 +72,24 @@ that apply. `svc --minimal` gives a compact layout for small panes.
72
72
 
73
73
  ```bash
74
74
  svc # interactive shell
75
- svc build # build to dist/<id>.zip
75
+ svc build [--no-zip] # build to dist/<id>.zip
76
76
  svc sign # sign to dist/<id>-signed.zip
77
77
  svc deploy [--force] # deploy the zip
78
78
  svc deploy --production [--activate] # deploy the signed zip
79
79
  svc dev [--signed] # build + deploy on change
80
80
  svc watch [--signed] # build on change, no deploy
81
81
  svc info # project information
82
+ svc setup-signing [--global] # save the signing username and certificate
82
83
  ```
83
84
 
84
- Direct commands use the base environment. `--token` and `--cookie` pass an
85
- OAuth2 token or session cookie for one run.
85
+ Short flags: `-f` force, `-p` production, `-a` activate, `-s` signed. An unknown
86
+ flag is an error.
87
+
88
+ Direct commands use the base environment, run without a terminal, and exit
89
+ non-zero when a step fails, so `svc build && svc sign && svc deploy -p -a` is
90
+ safe in CI. `SITEVISION_DEPLOY_PASSWORD` and `SITEVISION_SIGNING_PASSWORD`
91
+ supply the passwords; `SITEVISION_ACCESS_TOKEN` and `SITEVISION_SESSION_COOKIE`
92
+ pass an OAuth2 token or session cookie for one run.
86
93
 
87
94
  ## Configuration
88
95
 
package/dist/app.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { type ProjectInfo } from './utils/project-detection.js';
2
- type Props = {
3
- project: ProjectInfo;
4
- };
5
- export default function App({ project: initialProject }: Props): import("react").JSX.Element | null;
6
- export {};