sitevision-cli 1.0.0-beta.9 → 1.0.0

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 +138 -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 +44 -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 +27 -4
  49. package/readme.md +113 -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
package/CHANGELOG.md ADDED
@@ -0,0 +1,138 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ First stable release. Everything from the betas is in, see the entries below.
6
+
7
+ - `svc --debug` (or `SVC_DEBUG=1`) writes a log of everything svc does during
8
+ the run: each action, task and its output, request to Sitevision, child
9
+ process, config change, prompt and keychain lookup. The path is printed on
10
+ exit. Passwords, tokens and cookies are never written; the site, username
11
+ and file paths are, so read it before attaching it to a bug report.
12
+ - Bug reports and feature requests have forms on GitHub.
13
+
14
+ ## 1.0.0-beta.33
15
+
16
+ - **Breaking:** `--token` and `--cookie` are removed, since credentials on the
17
+ command line end up in shell history. Use `SITEVISION_ACCESS_TOKEN` or
18
+ `SITEVISION_SESSION_COOKIE` instead.
19
+ - **Breaking:** an unknown flag is now an error (exit code 2), so a misspelt
20
+ `--production` never turns into a dev deploy.
21
+ - `svc build`, `svc sign` and `svc deploy` work without a terminal. They print
22
+ their log line by line and exit on their own with a proper exit code: 0 on
23
+ success, 1 on failure, 130 when a prompt is cancelled. `svc dev` and
24
+ `svc watch` no longer crash when stdin is a pipe.
25
+ - New flags: `--no-zip` for `build`, and the short forms `-s`, `-f`, `-p` and
26
+ `-a`.
27
+ - Global settings in `~/.config/sitevision-cli/config.json`: a default signing
28
+ user and certificate for every project that does not set its own, and
29
+ switches for the update check and the addon name warning. Edit them with `,`
30
+ in the shell, which now shows a description of each setting and opens from
31
+ anywhere, or run `svc setup-signing --global`. A file that does not parse is
32
+ reported and left alone.
33
+ - The Config tab marks the addon name with `≠ manifest` when it matches none
34
+ of the manifest's names. Nothing is changed for you; the two are different
35
+ things and `svc` only points the difference out.
36
+ - The manifest's name and description always have a Swedish row in the Config
37
+ tab. Filling it in on a plain name keeps the old text as the English one.
38
+ - In a workspace, an environment's addon name is stored per app. Setting it
39
+ for one app no longer leaks into the others.
40
+ - `svc` names the missing setting when the deploy config is incomplete,
41
+ instead of posting to "undefined".
42
+ - A production deploy with `--activate` fails if the activation did not
43
+ happen, and the confirmation names the addon it deploys to.
44
+ - OAuth2 requires https and a token endpoint on the site's own domain. A
45
+ refresh token is only thrown away when the site rejects it, not on a network
46
+ error. An expired session cookie is removed so the next deploy asks for a
47
+ new login.
48
+ - A plaintext password in `.dev_properties.json` is moved to the keychain when
49
+ the file is saved, and `svc` says so when the keychain cannot store a secret.
50
+ `SVC_NO_KEYCHAIN=1` turns the keychain off for CI.
51
+ - `svc dev` with a project's own webpack config no longer deploys a half-built
52
+ zip when several files change at once.
53
+ - Builds work when dependencies are hoisted to the workspace root, a
54
+ `manifest.json` in the project root is picked up, file names with å, ä and ö
55
+ survive in the zip, and `SITEVISION_APP_ID_PREFIX`/`_SUFFIX` apply to build,
56
+ sign and deploy alike.
57
+ - A broken app in a workspace is skipped with a message instead of stopping
58
+ the shell. The shell asks before quitting during app creation, the PROD
59
+ badge survives a narrow terminal, Esc cancels a confirm, and Ctrl-modified
60
+ keys never fire a shortcut.
61
+ - The update check is skipped without a terminal and when `CI` is set.
62
+ - Updated user guides in English and Swedish.
63
+
64
+ ## 1.0.0-beta.32
65
+
66
+ - Sitevision CLI can now create new apps. **New app** in the command palette
67
+ runs Sitevision's own `create-sitevision-app` as a managed experience and
68
+ guides you through the setup: pick a name and a folder, then answer the
69
+ tool's questions right in `svc`. What your workspace already knows (domain,
70
+ site name, username) is filled in for you, and passwords never end up in a
71
+ file.
72
+ - When the app is created, `svc` fills in its manifest (the name, plus author
73
+ and help URL from the workspace's `package.json`) and offers to create the
74
+ addon on the site, so the app is ready for its first deploy.
75
+ - If the tool cannot be run this way, `svc` hands it the terminal and picks up
76
+ again when it is done.
77
+ - The Config tab has a MANIFEST section for editing `manifest.json`: id,
78
+ version, name, description, author and help URL, one row per language for
79
+ localized fields. Comments and formatting in the file are kept.
80
+ - `y` (sync `package.json`) now also mirrors the manifest's `version`,
81
+ `description` and `author` into the app's `package.json`. An app whose
82
+ `package.json` lacks them shows as out of sync until `y` is pressed.
83
+
84
+ ## 1.0.0-beta.31
85
+
86
+ - The help panel (`?`) now starts with a short guide to the pane you are in:
87
+ what it shows, how the values relate, and which keys apply. The Config guide
88
+ explains how local, shared and workspace settings layer on top of each
89
+ other. The panel scrolls when it does not fit.
90
+ - Updated dependencies.
91
+
92
+ ## 1.0.0-beta.30
93
+
94
+ - Dev and watch no longer rebuild on file events that change nothing, such as
95
+ editor metadata, swap files or `.DS_Store`. Ignored events are logged as
96
+ warnings so a stray trigger can be traced.
97
+ - The log names the event and path of the file that started a rebuild, for
98
+ example `change src/index.tsx`.
99
+
100
+ ## 1.0.0-beta.29
101
+
102
+ - After an update, the shell opens What's new with the changes since the
103
+ version you last ran.
104
+
105
+ ## 1.0.0-beta.28
106
+
107
+ ## 1.0.0-beta.27
108
+
109
+ - Popups float over the screen instead of replacing the content pane: the
110
+ command palette, settings, addon picker, confirmations and prompts.
111
+ - `?` opens a help panel with every key, plus the keys for the current tab.
112
+ - What's new in the command palette shows this changelog.
113
+
114
+ ## 1.0.0-beta.26
115
+
116
+ - No functional changes.
117
+
118
+ ## 1.0.0-beta.25
119
+
120
+ - Deploying to an addon that does not exist yet now offers to create it and
121
+ deploy again. If the addon list cannot be read, the error suggests logging in
122
+ again instead, since an expired session fails the same way.
123
+ - A Create addon action appears in the command palette when a deploy has found
124
+ the addon missing.
125
+ - Creating a RESTApp or MCPServer addon no longer sends a category, which only
126
+ WebApp and widget addons use.
127
+
128
+ ## 1.0.0-beta.24
129
+
130
+ - Deploying to a non-production environment uploads the signed zip when it is at
131
+ least as new as the build. The log names the zip that was uploaded.
132
+ - `x` in the Log tab wraps long lines immediately, and wrapped lines no longer
133
+ push the newest line out of view.
134
+ - Keys: switching environment moved from `E` to `v`, and refreshing versions
135
+ from `R` to `r`. Tab in the Config form moves between fields without leaving
136
+ the form.
137
+ - The bottom bar now shows force deploy (`P`), stop, config, login and settings.
138
+ - Running commands are stopped when `svc` exits.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rasmus Söderström
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/cli.js CHANGED
@@ -1,22 +1,29 @@
1
1
  #!/usr/bin/env node
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
3
  import { render } from 'ink';
4
4
  import { Text, Box } from 'ink';
5
5
  import meow from 'meow';
6
6
  import { readFileSync } from 'node:fs';
7
- import App from './app.js';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import { runningTasks } from './utils/tasks.js';
10
+ import { killAllChildren } from './utils/process-runner.js';
11
+ import { Shell } from './shell/Shell.js';
8
12
  import { getCommand } from './commands/index.js';
9
- import { requireProject, migrateLegacyPassword, } from './utils/project-detection.js';
13
+ import { requireProject, detectProject, migrateLegacyPassword, } from './utils/project-detection.js';
14
+ import { discoverApps } from './utils/workspace.js';
10
15
  import { promptYesNo } from './utils/password-prompt.js';
11
16
  import { checkForUpdate } from './utils/version-check.js';
12
- import { isFirstRun, markFirstRunComplete, getLastSeenVersion, setLastSeenVersion, } from './utils/config.js';
17
+ import { isFirstRun, markFirstRunComplete, getLastSeenVersion, setLastSeenVersion, getSettings, configProblem, settingsFile, } from './utils/config.js';
18
+ import { setLanguage } from './utils/i18n.js';
19
+ import { debug, enableDebug, errorText, say } from './utils/debug.js';
13
20
  import { WelcomeScreen } from './components/WelcomeScreen.js';
14
21
  import { AnimatedLogo } from './components/AnimatedLogo.js';
15
22
  import { printBranding, BIG_LOGO, BIG_LOGO_WIDTH, SMALL_LOGO, SMALL_LOGO_WIDTH, } from './utils/branding.js';
16
23
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
17
24
  const cli = meow(`
18
25
  Usage
19
- $ svc Start interactive menu
26
+ $ svc Open the interactive shell (app or workspace)
20
27
  $ svc <command> [options]
21
28
 
22
29
  Commands
@@ -26,13 +33,26 @@ const cli = meow(`
26
33
  sign Sign the app for production deployment
27
34
  deploy Deploy the application
28
35
  info Show project information
36
+ setup-signing Store the signing username and certificate
29
37
 
30
38
  Options
31
- --help Show this help message
32
- --version Show version number
39
+ --signed, -s dev/watch: sign after each build
40
+ --force, -f deploy: overwrite the existing addon version
41
+ --production, -p deploy: upload the signed zip to production
42
+ --activate, -a deploy: activate after a production deploy
43
+ --no-zip build: skip the zip archive
44
+ --global setup-signing: save for every project on this machine
45
+ --minimal Shell: compact layout for small terminals
46
+ --debug Write a log of every action to debug.log (or SVC_DEBUG=1)
47
+ --help Show this help message
48
+ --version Show version number
49
+
50
+ Credentials for CI come from the environment: SITEVISION_DEPLOY_PASSWORD,
51
+ SITEVISION_ACCESS_TOKEN or SITEVISION_SESSION_COOKIE.
33
52
 
34
53
  Examples
35
- $ svc # Interactive menu
54
+ $ svc # Shell: run inside an app, or at the repo root
55
+ $ svc --minimal # Shell without the sidebar, for a small pane
36
56
  $ svc dev
37
57
  $ svc dev --signed
38
58
  $ svc watch
@@ -43,9 +63,20 @@ const cli = meow(`
43
63
  $ svc info
44
64
  `, {
45
65
  importMeta: import.meta,
66
+ allowUnknownFlags: false,
46
67
  flags: {
47
68
  signed: {
48
69
  type: 'boolean',
70
+ shortFlag: 's',
71
+ default: false,
72
+ },
73
+ zip: {
74
+ type: 'boolean',
75
+ default: true,
76
+ },
77
+ activate: {
78
+ type: 'boolean',
79
+ shortFlag: 'a',
49
80
  default: false,
50
81
  },
51
82
  force: {
@@ -58,15 +89,34 @@ const cli = meow(`
58
89
  shortFlag: 'p',
59
90
  default: false,
60
91
  },
61
- token: {
62
- type: 'string',
92
+ minimal: {
93
+ type: 'boolean',
94
+ default: false,
95
+ },
96
+ global: {
97
+ type: 'boolean',
98
+ default: false,
63
99
  },
64
- cookie: {
65
- type: 'string',
100
+ debug: {
101
+ type: 'boolean',
102
+ default: false,
66
103
  },
67
104
  },
68
105
  });
69
106
  const [commandName, ...args] = cli.input;
107
+ const settings = getSettings();
108
+ if (cli.flags.debug || process.env['SVC_DEBUG']) {
109
+ const file = enableDebug(path.dirname(settingsFile()));
110
+ process.on('exit', code => {
111
+ debug('exit', `code ${code}`);
112
+ process.stderr.write(`\x1b[2mDebug log: ${file}\x1b[0m\n`);
113
+ });
114
+ const on = Object.entries(cli.flags)
115
+ .filter(([, v]) => v === true)
116
+ .map(([k]) => `--${k}`);
117
+ debug('start', `svc ${pkg.version} node ${process.version} ${process.platform} ${process.arch} ${[commandName ?? '(shell)', ...args, ...on].join(' ')}`);
118
+ }
119
+ setLanguage(settings.language);
70
120
  const CYAN = '\x1b[36m';
71
121
  const BOLD = '\x1b[1m';
72
122
  const DIM = '\x1b[2m';
@@ -95,8 +145,27 @@ function printMasthead(version) {
95
145
  `${spaces(gap)}${DIM}${right}${RESET}${spaces(padding)}${CYAN}│${RESET}`);
96
146
  console.log(`${CYAN}╰${border}╯${RESET}`);
97
147
  }
148
+ /**
149
+ * Stop every running task and kill leftover child processes. In raw mode Ink
150
+ * reads Ctrl+C as a keypress, so no SIGINT reaches anything else. Exits when
151
+ * there was work to stop (or on a signal); otherwise the process ends on its
152
+ * own, so a command's last output is never cut off.
153
+ */
154
+ function shutdown(code) {
155
+ const busy = runningTasks();
156
+ for (const task of busy)
157
+ task.stop();
158
+ const killed = killAllChildren();
159
+ if (busy.length > 0 || killed > 0 || code !== undefined)
160
+ process.exit(code);
161
+ }
162
+ function fail(message, hint) {
163
+ debug('out', `Error: ${message}`);
164
+ render(_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", message] }), _jsx(Text, { dimColor: true, children: hint })] }));
165
+ process.exit(1);
166
+ }
98
167
  // Pick the widest wordmark that fits the terminal, or undefined if even the
99
- // compact one would wrap (caller then falls back to the static masthead).
168
+ // compact one would wrap.
100
169
  function pickIntroArt(columns) {
101
170
  if (columns >= BIG_LOGO_WIDTH)
102
171
  return BIG_LOGO;
@@ -111,7 +180,64 @@ async function playIntro(art) {
111
180
  app.waitUntilExit().then(() => resolve(), () => resolve());
112
181
  });
113
182
  }
183
+ // Run the full-screen shell on the alternate screen buffer so the scrollback
184
+ // is untouched, and restore it on exit. The animated wordmark plays first,
185
+ // inside the same buffer, when the terminal is wide enough for it.
186
+ async function runShell(apps, workspaceRoot, updatedFrom, skipped = []) {
187
+ process.stdout.write('\x1b[?1049h\x1b[H');
188
+ // Also leave the alternate screen when a signal exits past the finally.
189
+ process.prependOnceListener('exit', () => process.stdout.write('\x1b[?1049l'));
190
+ // A crash outside React would print into the alternate screen and vanish
191
+ // with it: leave it first, then report.
192
+ const crash = (error) => {
193
+ process.stdout.write('\x1b[?1049l');
194
+ debug('crash', errorText(error));
195
+ console.error('svc crashed:', error);
196
+ shutdown(1);
197
+ };
198
+ process.once('uncaughtException', crash);
199
+ process.once('unhandledRejection', crash);
200
+ try {
201
+ const art = process.stdin.isTTY && settings.introAnimation && !cli.flags.minimal
202
+ ? pickIntroArt(process.stdout.columns ?? 0)
203
+ : undefined;
204
+ if (art) {
205
+ await playIntro(art);
206
+ process.stdout.write('\x1b[2J\x1b[H');
207
+ }
208
+ // A handover unmounts the shell, runs its job on the normal screen with
209
+ // the terminal to itself, and starts the shell again.
210
+ for (;;) {
211
+ let job;
212
+ const app = render(_jsx(Shell, { apps: apps, workspaceRoot: workspaceRoot, version: pkg.version, minimal: cli.flags.minimal, updatedFrom: updatedFrom, skipped: skipped, handover: next => {
213
+ job = next;
214
+ app.unmount();
215
+ } }));
216
+ // eslint-disable-next-line no-await-in-loop
217
+ await app.waitUntilExit();
218
+ if (!job)
219
+ break;
220
+ process.stdout.write('\x1b[?1049l');
221
+ // eslint-disable-next-line no-await-in-loop
222
+ await job();
223
+ if (workspaceRoot) {
224
+ skipped = [];
225
+ apps = discoverApps(workspaceRoot, skipped);
226
+ }
227
+ updatedFrom = undefined;
228
+ process.stdout.write('\x1b[?1049h\x1b[2J\x1b[H');
229
+ }
230
+ }
231
+ finally {
232
+ process.stdout.write('\x1b[?1049l');
233
+ }
234
+ }
114
235
  async function main() {
236
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
237
+ process.on(signal, () => {
238
+ shutdown(128 + os.constants.signals[signal]);
239
+ });
240
+ }
115
241
  // On the very first run we show a dedicated welcome screen instead of the
116
242
  // masthead, so the branding is the moment. Only when stdin is a TTY — the
117
243
  // welcome is interactive and would hang in CI / piped input.
@@ -122,106 +248,104 @@ async function main() {
122
248
  // silently rather than claiming an update happened.
123
249
  const lastSeen = getLastSeenVersion();
124
250
  const isUpdate = !firstRun && lastSeen !== undefined && lastSeen !== pkg.version;
125
- // On the plain interactive `svc` (no command), play the animated wordmark
126
- // instead of the static masthead — sized to the terminal. Only on a TTY so it
127
- // doesn't run in CI / piped input.
128
- const introEligible = !firstRun && !isUpdate && !commandName && Boolean(process.stdin.isTTY);
129
- const introArt = introEligible
130
- ? pickIntroArt(process.stdout.columns ?? 0)
131
- : undefined;
132
251
  if (!firstRun) {
133
- if (isUpdate) {
252
+ // The shell shows the changelog itself; only direct commands get a banner.
253
+ if (isUpdate && commandName) {
134
254
  printBranding();
135
- console.log(`\x1b[32m\n ✨ Updated to v${pkg.version}\x1b[0m \x1b[2m(from v${lastSeen})\x1b[0m\n`);
255
+ say(`\x1b[32m\n ✨ Updated to v${pkg.version}\x1b[0m \x1b[2m(from v${lastSeen})\x1b[0m\n`);
136
256
  }
137
- else if (!introArt) {
257
+ else if (commandName) {
138
258
  printMasthead(pkg.version);
139
259
  }
140
260
  // Record the current version so the banner shows once per upgrade.
141
261
  setLastSeenVersion(pkg.version);
262
+ if (configProblem()) {
263
+ say(`\x1b[33m ${settingsFile()} does not parse and is ignored: ${configProblem()}\x1b[0m`);
264
+ }
142
265
  // Check npm for a newer published release.
143
- const latestVersion = await checkForUpdate(pkg.name, pkg.version);
266
+ const latestVersion = settings.updateCheck && process.stdout.isTTY && !process.env['CI']
267
+ ? await checkForUpdate(pkg.name, pkg.version)
268
+ : null;
144
269
  if (latestVersion) {
145
- console.log(`\x1b[33m ↑ update available: ${pkg.version} → ${latestVersion} (run: npm i -g ${pkg.name})\x1b[0m`);
270
+ say(`\x1b[33m ↑ update available: ${pkg.version} → ${latestVersion} (run: npm i -g ${pkg.name})\x1b[0m`);
146
271
  }
147
272
  }
148
- // Check if we're in a Sitevision project
149
- const project = (() => {
273
+ // No command: the shell. Inside an app it is single-app mode; anywhere
274
+ // above one or more apps it is workspace mode.
275
+ if (!commandName) {
276
+ let project = null;
150
277
  try {
151
- return requireProject();
278
+ project = detectProject();
152
279
  }
153
280
  catch (error) {
154
- render(_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", error.message] }), _jsx(Text, { dimColor: true, children: "Make sure you're in a Sitevision project directory" })] }));
155
- process.exit(1);
281
+ fail(error.message, 'Fix the manifest and try again');
156
282
  }
157
- })();
158
- // --token / --cookie override the resolved bearer token / session cookie for
159
- // this run (manual / CI path, alongside SITEVISION_ACCESS_TOKEN and
160
- // SITEVISION_SESSION_COOKIE).
161
- if (cli.flags.token || cli.flags.cookie) {
162
- if (project.devProperties) {
163
- if (cli.flags.token) {
164
- project.devProperties.accessToken = cli.flags.token;
165
- }
166
- if (cli.flags.cookie) {
167
- project.devProperties.sessionCookie = cli.flags.cookie;
283
+ if (!project) {
284
+ const skipped = [];
285
+ const apps = discoverApps(process.cwd(), skipped);
286
+ if (apps.length === 0) {
287
+ fail('No Sitevision apps found here.', skipped[0] ??
288
+ 'Run svc inside an app directory (manifest.json) or at the root of a repo that contains apps.');
168
289
  }
290
+ await runShell(apps, process.cwd(), isUpdate ? lastSeen : undefined, skipped);
291
+ shutdown();
292
+ return;
169
293
  }
170
- else {
171
- console.log('\x1b[33m--token/--cookie needs a .dev_properties.json (domain, site, addon) to deploy against.\x1b[0m');
294
+ if (firstRun) {
295
+ await new Promise(resolve => {
296
+ const app = render(_jsx(WelcomeScreen, { project: project, onComplete: () => {
297
+ markFirstRunComplete();
298
+ setLastSeenVersion(pkg.version);
299
+ app.unmount();
300
+ } }));
301
+ app.waitUntilExit().then(() => resolve(), () => resolve());
302
+ });
172
303
  }
173
- }
174
- // First run: show the welcome (branding + optional signing-password save),
175
- // then continue to the normal flow once the user dismisses it.
176
- if (firstRun) {
177
- await new Promise(resolve => {
178
- const app = render(_jsx(WelcomeScreen, { project: project, onComplete: () => {
179
- markFirstRunComplete();
180
- setLastSeenVersion(pkg.version);
181
- app.unmount();
182
- } }));
183
- app.waitUntilExit().then(() => resolve(), () => resolve());
184
- });
185
- }
186
- // If no command, show interactive menu (with the animated intro first when
187
- // the terminal can fit it).
188
- if (!commandName) {
189
- if (introArt) {
190
- await playIntro(introArt);
191
- }
192
- render(_jsx(App, { project: project }));
304
+ await runShell([project], undefined, isUpdate ? lastSeen : undefined);
305
+ shutdown();
193
306
  return;
194
307
  }
308
+ // Check if we're in a Sitevision project
309
+ const project = (() => {
310
+ try {
311
+ return requireProject();
312
+ }
313
+ catch (error) {
314
+ return fail(error.message, "Make sure you're in a Sitevision project directory");
315
+ }
316
+ })();
195
317
  // Get the command
196
318
  const command = getCommand(commandName);
197
319
  if (!command) {
320
+ debug('out', `Unknown command: ${commandName}`);
198
321
  render(_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", children: ["Unknown command: ", commandName] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { bold: true, children: "svc --help" }), " to see available commands"] })] }));
199
322
  process.exit(1);
200
323
  }
201
324
  // Offer to migrate a legacy plaintext password into the OS keychain.
202
- // Interactive `svc` handles this in SetupFlow; this covers direct commands
203
- // (svc deploy/dev/sign/…). Skip on non-TTY stdin (e.g. CI) where prompting
204
- // would fail — the plaintext password is still used for this run.
325
+ // Skip on non-TTY stdin (e.g. CI) where prompting would fail — the
326
+ // plaintext password is still used for this run.
205
327
  if (project.hasLegacyPassword && process.stdin.isTTY) {
206
- console.log('\n\x1b[33m⚠ Plaintext password found in .dev_properties.json\x1b[0m');
328
+ say('\n\x1b[33m⚠ Plaintext password found in .dev_properties.json\x1b[0m');
207
329
  const move = await promptYesNo('Move it to the OS keychain and remove it from the file? (y/N): ');
208
330
  if (move) {
209
331
  if (migrateLegacyPassword(project)) {
210
- console.log('\x1b[32m✓ Password moved to keychain.\x1b[0m\n');
332
+ say('\x1b[32m✓ Password moved to keychain.\x1b[0m\n');
211
333
  }
212
334
  else {
213
- console.log('\x1b[31mCould not access keychain; leaving the file unchanged.\x1b[0m\n');
335
+ say('\x1b[31mCould not move the password; the file still holds it.\x1b[0m\n');
214
336
  }
215
337
  }
216
338
  }
217
- // Execute the command
339
+ debug('command', commandName);
218
340
  await command.execute({
219
341
  project,
220
342
  flags: cli.flags,
221
343
  args,
222
344
  });
345
+ shutdown();
223
346
  }
224
347
  main().catch(error => {
348
+ debug('crash', errorText(error));
225
349
  console.error('Fatal error:', error);
226
350
  process.exit(1);
227
351
  });
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { styleText } from 'node:util';
4
+ import { Box, useStdout } from 'ink';
5
+ import { StatusIndicator } from '../components/StatusIndicator.js';
6
+ import { useTasks } from '../utils/tasks.js';
7
+ import { useFinish } from './use-finish.js';
8
+ const LEVEL_STYLE = { info: 'reset', ok: 'green', warn: 'yellow', error: 'red' };
9
+ /**
10
+ * A one-shot command: the task's log printed above the status as it comes (a
11
+ * terminal keeps it in scrollback, CI keeps all of it), then exit with its result.
12
+ */
13
+ export function TaskScreen({ task, stay = false, children }) {
14
+ const { write } = useStdout();
15
+ const live = useTasks().find(t => t.id === task.id) ?? task;
16
+ const result = live.status === 'running'
17
+ ? undefined
18
+ : live.status === 'success'
19
+ ? 'success'
20
+ : 'error';
21
+ // ponytail: counts lines, so past the task's 2000-line cap (where old lines
22
+ // are dropped) some would be skipped. No one-shot command logs that much.
23
+ const printed = useRef(0);
24
+ useEffect(() => {
25
+ for (const line of live.lines.slice(printed.current)) {
26
+ write(`${styleText('dim', line.tag.padEnd(3))} ${styleText(LEVEL_STYLE[line.level], line.text)}\n`);
27
+ }
28
+ printed.current = live.lines.length;
29
+ });
30
+ useFinish(result, stay && result === 'error');
31
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(StatusIndicator, { status: result ?? 'running', label: live.phase, message: `${live.label} ${live.appName}` }), children] }));
32
+ }