sitevision-cli 1.0.0-beta.3 → 1.0.0-beta.31

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 (72) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/app.d.ts +1 -1
  3. package/dist/app.js +59 -8
  4. package/dist/cli.js +125 -40
  5. package/dist/commands/build.js +1 -1
  6. package/dist/commands/deploy.d.ts +2 -2
  7. package/dist/commands/deploy.js +137 -28
  8. package/dist/commands/dev.d.ts +8 -10
  9. package/dist/commands/dev.js +70 -371
  10. package/dist/commands/watch.js +5 -23
  11. package/dist/components/AnimatedLogo.js +8 -2
  12. package/dist/components/AuthLoginScreen.d.ts +21 -0
  13. package/dist/components/AuthLoginScreen.js +90 -0
  14. package/dist/components/DevPropertiesForm.d.ts +2 -1
  15. package/dist/components/DevPropertiesForm.js +198 -33
  16. package/dist/components/MainMenu.js +5 -0
  17. package/dist/components/PasswordInput.js +2 -1
  18. package/dist/components/SetupFlow.d.ts +2 -1
  19. package/dist/components/SetupFlow.js +99 -10
  20. package/dist/shell/AddonPicker.d.ts +14 -0
  21. package/dist/shell/AddonPicker.js +54 -0
  22. package/dist/shell/Changelog.d.ts +10 -0
  23. package/dist/shell/Changelog.js +44 -0
  24. package/dist/shell/CommandPalette.d.ts +12 -0
  25. package/dist/shell/CommandPalette.js +63 -0
  26. package/dist/shell/ConfigForm.d.ts +36 -0
  27. package/dist/shell/ConfigForm.js +558 -0
  28. package/dist/shell/Frame.d.ts +59 -0
  29. package/dist/shell/Frame.js +135 -0
  30. package/dist/shell/Help.d.ts +9 -0
  31. package/dist/shell/Help.js +95 -0
  32. package/dist/shell/Popover.d.ts +12 -0
  33. package/dist/shell/Popover.js +15 -0
  34. package/dist/shell/Settings.d.ts +6 -0
  35. package/dist/shell/Settings.js +96 -0
  36. package/dist/shell/Shell.d.ts +10 -0
  37. package/dist/shell/Shell.js +622 -0
  38. package/dist/shell/Tabs.d.ts +36 -0
  39. package/dist/shell/Tabs.js +90 -0
  40. package/dist/shell/actions.d.ts +48 -0
  41. package/dist/shell/actions.js +0 -0
  42. package/dist/types/index.d.ts +37 -3
  43. package/dist/utils/config.d.ts +10 -0
  44. package/dist/utils/config.js +14 -0
  45. package/dist/utils/environments.d.ts +20 -0
  46. package/dist/utils/environments.js +74 -0
  47. package/dist/utils/i18n.d.ts +12 -0
  48. package/dist/utils/i18n.js +314 -0
  49. package/dist/utils/jsonc.d.ts +19 -0
  50. package/dist/utils/jsonc.js +74 -0
  51. package/dist/utils/keychain.d.ts +9 -0
  52. package/dist/utils/keychain.js +54 -0
  53. package/dist/utils/oauth2-auth.d.ts +64 -0
  54. package/dist/utils/oauth2-auth.js +242 -0
  55. package/dist/utils/password-prompt.d.ts +5 -0
  56. package/dist/utils/password-prompt.js +28 -0
  57. package/dist/utils/process-runner.d.ts +13 -4
  58. package/dist/utils/process-runner.js +63 -37
  59. package/dist/utils/project-detection.d.ts +90 -4
  60. package/dist/utils/project-detection.js +389 -63
  61. package/dist/utils/session-cookie-auth.d.ts +35 -0
  62. package/dist/utils/session-cookie-auth.js +99 -0
  63. package/dist/utils/sitevision-api.d.ts +72 -5
  64. package/dist/utils/sitevision-api.js +220 -38
  65. package/dist/utils/sitevision-scripts-runner.d.ts +2 -1
  66. package/dist/utils/sitevision-scripts-runner.js +18 -7
  67. package/dist/utils/tasks.d.ts +50 -0
  68. package/dist/utils/tasks.js +439 -0
  69. package/dist/utils/workspace.d.ts +17 -0
  70. package/dist/utils/workspace.js +67 -0
  71. package/package.json +5 -2
  72. package/readme.md +103 -121
@@ -1,359 +1,54 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import fs from 'fs';
3
- import path from 'path';
4
2
  import React from 'react';
5
- import { render, Box, Text, useApp, useInput } from 'ink';
3
+ import { render, Box, Text, useApp, useInput, useStdout } from 'ink';
6
4
  import { StatusIndicator } from '../components/StatusIndicator.js';
7
- import { WebpackRunner, hasLocalWebpackConfig } from '../utils/webpack-runner.js';
8
- import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from '../utils/sitevision-scripts-runner.js';
9
5
  import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
10
- import { signApp, deployApp } from '../utils/sitevision-api.js';
11
6
  import { setDeployPassword } from '../utils/keychain.js';
12
7
  import { resolveSigningPassword } from '../utils/signing-password.js';
13
- import { copyStaticToBuild, createBuildZip, cleanBuild } from '../utils/zip.js';
14
- import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, localizedText, } from '../utils/project-detection.js';
15
- export function DevScreen({ projectRoot, manifest, devProperties, signed, deploy = true, signingCredentials, onBack, onRetryCredentials, }) {
8
+ import { startDev, useTasks } from '../utils/tasks.js';
9
+ import { Log } from '../shell/Tabs.js';
10
+ /** Standalone `svc dev` / `svc watch`: one task, its log, Ctrl+C or q to stop. */
11
+ export function DevScreen({ project, options }) {
16
12
  const { exit } = useApp();
17
- const [state, setState] = React.useState({
18
- status: 'initializing',
19
- message: 'Starting webpack watch...',
20
- buildCount: 0,
21
- webpackReady: false,
22
- });
13
+ const { stdout } = useStdout();
14
+ const tasks = useTasks();
15
+ // eslint-disable-next-line react-hooks/exhaustive-deps
16
+ const task = React.useMemo(() => startDev(project, options), []);
17
+ const live = tasks.find(t => t.id === task.id) ?? task;
23
18
  useInput((input, key) => {
24
- if (onBack && (key.escape || input === 'q')) {
25
- onBack();
26
- }
27
- if (onRetryCredentials && state.status === 'error' && input === 'r') {
28
- onRetryCredentials();
29
- }
30
- });
31
- const webpackRunnerRef = React.useRef(null);
32
- const watchersRef = React.useRef([]);
33
- const debounceTimerRef = React.useRef(null);
34
- const isBuildingRef = React.useRef(false);
35
- const pendingRebuildRef = React.useRef(false);
36
- // Sign (if needed) and deploy an already-built zip, updating UI state.
37
- const signAndDeploy = React.useCallback(async (zipPath, buildTime) => {
38
- try {
39
- let deployZipPath = zipPath;
40
- // Sign if needed
41
- if (signed && signingCredentials) {
42
- setState(prev => ({
43
- ...prev,
44
- status: 'signing',
45
- message: 'Signing app...',
46
- }));
47
- const signedZipPath = getSignedZipPath(projectRoot, manifest);
48
- const signResult = await signApp(zipPath, signingCredentials, signedZipPath);
49
- if (!signResult.success) {
50
- setState(prev => ({
51
- ...prev,
52
- status: 'error',
53
- message: `Signing failed: ${signResult.error}`,
54
- error: signResult.error,
55
- }));
56
- return;
57
- }
58
- deployZipPath = signedZipPath;
59
- }
60
- // Watch/build-only mode: stop after building (and signing).
61
- if (!deploy || !devProperties) {
62
- setState(prev => ({
63
- ...prev,
64
- status: 'ready',
65
- message: signed
66
- ? 'Signed. Watching for changes...'
67
- : 'Built. Watching for changes...',
68
- buildCount: prev.buildCount + 1,
69
- lastBuildTime: buildTime,
70
- error: undefined,
71
- }));
72
- return;
73
- }
74
- // Deploy
75
- setState(prev => ({
76
- ...prev,
77
- status: 'deploying',
78
- message: 'Deploying to dev...',
79
- }));
80
- const appType = getAppType(manifest);
81
- const deployResult = await deployApp(deployZipPath, {
82
- domain: devProperties.domain,
83
- siteName: devProperties.siteName,
84
- addonName: devProperties.addonName,
85
- username: devProperties.username,
86
- password: devProperties.password,
87
- useHTTP: devProperties.useHTTPForDevDeploy,
88
- }, appType, true);
89
- if (!deployResult.success) {
90
- setState(prev => ({
91
- ...prev,
92
- status: 'error',
93
- message: `Deploy failed: ${deployResult.error}`,
94
- error: deployResult.error,
95
- }));
96
- return;
97
- }
98
- // Success - back to watching
99
- setState(prev => ({
100
- ...prev,
101
- status: 'ready',
102
- message: 'Deployed. Watching for changes...',
103
- buildCount: prev.buildCount + 1,
104
- lastBuildTime: buildTime,
105
- error: undefined,
106
- }));
107
- }
108
- catch (error) {
109
- setState(prev => ({
110
- ...prev,
111
- status: 'error',
112
- message: error instanceof Error ? error.message : String(error),
113
- error: error instanceof Error ? error.message : String(error),
114
- }));
115
- }
116
- }, [projectRoot, manifest, devProperties, signed, deploy, signingCredentials]);
117
- // In-house webpack path: copy static, zip, then sign + deploy.
118
- const handleBuildComplete = React.useCallback(async (result) => {
119
- if (!result.success) {
120
- setState(prev => ({
121
- ...prev,
122
- status: 'error',
123
- message: result.errors?.join('\n') || 'Build failed',
124
- error: result.errors?.join('\n'),
125
- }));
126
- return;
127
- }
128
- try {
129
- copyStaticToBuild(projectRoot);
130
- const appId = getFullAppId(manifest.id);
131
- await createBuildZip(projectRoot, appId);
132
- await signAndDeploy(getZipPath(projectRoot, manifest), result.stats?.time);
133
- }
134
- catch (error) {
135
- setState(prev => ({
136
- ...prev,
137
- status: 'error',
138
- message: error instanceof Error ? error.message : String(error),
139
- error: error instanceof Error ? error.message : String(error),
140
- }));
141
- }
142
- }, [projectRoot, manifest, signAndDeploy]);
143
- // Delegated path: full `sitevision-scripts build` then sign + deploy.
144
- // Coalesces overlapping triggers so a save mid-build queues one rebuild.
145
- const runDelegatedBuild = React.useCallback(async () => {
146
- if (isBuildingRef.current) {
147
- pendingRebuildRef.current = true;
148
- return;
149
- }
150
- isBuildingRef.current = true;
151
- const buildOnce = async () => {
152
- pendingRebuildRef.current = false;
153
- setState(prev => ({
154
- ...prev,
155
- status: 'building',
156
- message: 'Building via sitevision-scripts...',
157
- }));
158
- const result = await runSitevisionScriptsBuild(projectRoot);
159
- if (result.success) {
160
- await signAndDeploy(getDelegatedZipPath(projectRoot, manifest.id));
161
- }
162
- else {
163
- setState(prev => ({
164
- ...prev,
165
- status: 'error',
166
- message: result.error ?? 'Build failed',
167
- error: `${result.error}\n${result.output.slice(-1000)}`,
168
- }));
169
- }
170
- if (pendingRebuildRef.current) {
171
- await buildOnce();
172
- }
173
- };
174
- try {
175
- await buildOnce();
176
- }
177
- finally {
178
- isBuildingRef.current = false;
179
- }
180
- }, [projectRoot, manifest, signAndDeploy]);
181
- // Watch source files and trigger a delegated rebuild (debounced).
182
- const startFileWatcher = React.useCallback(() => {
183
- const targets = [
184
- 'src',
185
- 'static',
186
- 'i18n',
187
- 'resource',
188
- 'config',
189
- 'manifest.json',
190
- ]
191
- .map(name => path.join(projectRoot, name))
192
- .filter(target => fs.existsSync(target));
193
- for (const target of targets) {
194
- const isDir = fs.statSync(target).isDirectory();
195
- const watcher = fs.watch(target, { recursive: isDir }, () => {
196
- if (debounceTimerRef.current) {
197
- clearTimeout(debounceTimerRef.current);
198
- }
199
- debounceTimerRef.current = setTimeout(() => {
200
- void runDelegatedBuild();
201
- }, 300);
202
- });
203
- watchersRef.current.push(watcher);
204
- }
205
- }, [projectRoot, runDelegatedBuild]);
206
- React.useEffect(() => {
207
- const isBundled = isBundledApp(manifest);
208
- async function startWatch() {
209
- try {
210
- // Clean build directory
211
- cleanBuild(projectRoot);
212
- if (isBundled && !hasLocalWebpackConfig(projectRoot)) {
213
- // No local webpack config: delegate each build to
214
- // sitevision-scripts (full rebuild) and watch source files
215
- // ourselves, keeping the CLI's own sign + deploy flow.
216
- if (!hasSitevisionScripts(projectRoot)) {
217
- setState({
218
- status: 'error',
219
- message: 'No webpack.config.js found and @sitevision/sitevision-scripts is not installed. Run npm install.',
220
- buildCount: 0,
221
- webpackReady: false,
222
- error: 'No build pipeline available',
223
- });
224
- return;
225
- }
226
- const warning = checkSitevisionScriptsCompatibility(projectRoot).warning;
227
- setState(prev => ({ ...prev, webpackReady: true, warning }));
228
- startFileWatcher();
229
- await runDelegatedBuild();
230
- }
231
- else if (isBundled) {
232
- // Project ships its own webpack config: incremental in-process watch.
233
- if (!WebpackRunner.isWebpackAvailable(projectRoot)) {
234
- setState({
235
- status: 'error',
236
- message: 'webpack not found. Run npm install.',
237
- buildCount: 0,
238
- webpackReady: false,
239
- error: 'webpack not found',
240
- });
241
- return;
242
- }
243
- setState(prev => ({
244
- ...prev,
245
- status: 'building',
246
- message: 'Starting initial build...',
247
- }));
248
- const appType = getAppType(manifest);
249
- const runner = new WebpackRunner(projectRoot, {
250
- mode: 'development',
251
- watch: true,
252
- cssPrefix: manifest.id,
253
- restApp: appType === 'rest',
254
- });
255
- webpackRunnerRef.current = runner;
256
- await runner.watch(handleBuildComplete);
257
- setState(prev => ({
258
- ...prev,
259
- status: 'watching',
260
- message: 'Building...',
261
- webpackReady: true,
262
- }));
263
- }
264
- else {
265
- // Non-bundled app: just copy and deploy
266
- setState(prev => ({
267
- ...prev,
268
- status: 'building',
269
- message: 'Copying files...',
270
- }));
271
- await handleBuildComplete({
272
- success: true,
273
- stats: { time: 0, hash: '', assets: [] },
274
- });
275
- }
276
- }
277
- catch (error) {
278
- setState({
279
- status: 'error',
280
- message: error instanceof Error ? error.message : String(error),
281
- buildCount: 0,
282
- webpackReady: false,
283
- error: error instanceof Error ? error.message : String(error),
284
- });
285
- }
286
- }
287
- startWatch();
288
- // Cleanup
289
- return () => {
290
- if (webpackRunnerRef.current) {
291
- webpackRunnerRef.current.close().catch(() => { });
292
- }
293
- if (debounceTimerRef.current) {
294
- clearTimeout(debounceTimerRef.current);
295
- }
296
- for (const watcher of watchersRef.current) {
297
- watcher.close();
298
- }
299
- watchersRef.current = [];
300
- };
301
- }, [
302
- projectRoot,
303
- manifest,
304
- handleBuildComplete,
305
- runDelegatedBuild,
306
- startFileWatcher,
307
- ]);
308
- // Handle Ctrl+C
309
- React.useEffect(() => {
310
- const handleExit = () => {
311
- if (webpackRunnerRef.current) {
312
- webpackRunnerRef.current.close().catch(() => { });
313
- }
314
- for (const watcher of watchersRef.current) {
315
- watcher.close();
316
- }
19
+ if (key.escape || input === 'q') {
20
+ live.stop();
317
21
  exit();
318
- };
319
- process.on('SIGINT', handleExit);
320
- process.on('SIGTERM', handleExit);
321
- return () => {
322
- process.off('SIGINT', handleExit);
323
- process.off('SIGTERM', handleExit);
324
- };
325
- }, [exit]);
326
- const getStatusType = () => {
327
- switch (state.status) {
328
- case 'error':
329
- return 'error';
330
- case 'ready':
331
- return 'success';
332
- default:
333
- return 'running';
334
- }
335
- };
336
- const getStatusLabel = () => {
337
- switch (state.status) {
338
- case 'initializing':
339
- return 'Initializing';
340
- case 'watching':
341
- return 'Watching';
342
- case 'building':
343
- return 'Building';
344
- case 'signing':
345
- return 'Signing';
346
- case 'deploying':
347
- return 'Deploying';
348
- case 'ready':
349
- return 'Ready';
350
- case 'error':
351
- return 'Error';
352
22
  }
23
+ });
24
+ const status = live.status === 'running'
25
+ ? live.phase === 'error'
26
+ ? 'error'
27
+ : 'running'
28
+ : live.status === 'success'
29
+ ? 'success'
30
+ : 'error';
31
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(StatusIndicator, { status: status, label: live.phase, message: `${live.label} ${live.appName}` }), _jsx(Box, { marginTop: 1, children: _jsx(Log, { task: live, height: (stdout.rows || 24) - 6, scroll: 0, wrap: true }) }), _jsx(Text, { dimColor: true, children: "Press q, Esc or Ctrl+C to stop" })] }));
32
+ }
33
+ /** Resolve signing credentials for signed dev/watch, prompting if needed. */
34
+ export async function resolveSigningForCli(project) {
35
+ if (!project.hasSigningProperties ||
36
+ !project.devProperties?.signingUsername) {
37
+ console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
38
+ console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
39
+ return undefined;
40
+ }
41
+ const signingUsername = project.devProperties.signingUsername;
42
+ const password = await resolveSigningPassword(signingUsername);
43
+ if (!password) {
44
+ console.log('\x1b[31mError: Password is required for signed mode\x1b[0m');
45
+ return undefined;
46
+ }
47
+ return {
48
+ username: signingUsername,
49
+ password,
50
+ certificateName: project.devProperties.certificateName,
353
51
  };
354
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(StatusIndicator, { status: getStatusType(), label: getStatusLabel(), message: state.message }) }), state.warning && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "yellow", children: ["\u26A0 ", state.warning] }) })), state.buildCount > 0 && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Builds: ", state.buildCount, state.lastBuildTime
355
- ? ` | Last build: ${state.lastBuildTime}ms`
356
- : '', signed ? ' | Signed mode' : ''] }) })), _jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [localizedText(manifest.name), " v", manifest.version] }) }), deploy && devProperties && (_jsx(Box, { marginLeft: 2, marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Target: ", devProperties.domain, "/", devProperties.siteName, "/", devProperties.addonName] }) })), state.status === 'error' && state.error && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: "red", children: state.error }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [state.status === 'error' && onRetryCredentials && (_jsx(Text, { dimColor: true, children: "Press r to retry with new credentials" })), onBack ? (_jsx(Text, { dimColor: true, children: "Press q or Esc to return to menu (Ctrl+C to stop process)" })) : (_jsx(Text, { dimColor: true, children: "Press Ctrl+C to stop" }))] })] }));
357
52
  }
358
53
  export const devCommand = {
359
54
  name: 'dev',
@@ -368,15 +63,22 @@ export const devCommand = {
368
63
  },
369
64
  },
370
65
  async execute({ project, flags }) {
371
- // Check if dev properties are configured
372
- if (!project.hasDevProperties || !project.devProperties) {
66
+ const dev = project.devProperties;
67
+ if (!project.hasDevProperties || !dev) {
373
68
  console.log('\n\x1b[33mDeployment credentials not configured.\x1b[0m');
374
69
  console.log('Create a .dev_properties.json file with domain, siteName, addonName, and username, then run setup.\n');
375
70
  return;
376
71
  }
377
- // Resolve deploy password (already loaded from keychain/env in detectProject — prompt if missing)
378
- if (!project.devProperties.password) {
379
- const { domain, username } = project.devProperties;
72
+ // The standalone command only prompts for a basic password; OAuth2/cookie
73
+ // logins are interactive and live in the shell (`svc`) or `svc deploy`.
74
+ if ((dev.authMethod ?? 'basic') !== 'basic' &&
75
+ !dev.accessToken &&
76
+ !dev.sessionCookie) {
77
+ console.log('\n\x1b[33mNo token/cookie available. Run `svc` and use Dev from the shell, or set SITEVISION_ACCESS_TOKEN / SITEVISION_SESSION_COOKIE.\x1b[0m\n');
78
+ return;
79
+ }
80
+ if ((dev.authMethod ?? 'basic') === 'basic' && !dev.password) {
81
+ const { domain, username } = dev;
380
82
  console.log('');
381
83
  const pw = await promptPassword(`Deploy password for ${username}@${domain}: `);
382
84
  if (!pw) {
@@ -387,31 +89,28 @@ export const devCommand = {
387
89
  if (remember && domain && username) {
388
90
  setDeployPassword(domain, username, pw);
389
91
  }
390
- project.devProperties.password = pw;
92
+ dev.password = pw;
391
93
  }
392
- const signed = Boolean(flags['signed']);
393
94
  let signingCredentials;
394
- // If signed mode, resolve signing password (keychain → env → prompt)
395
- if (signed) {
396
- if (!project.hasSigningProperties ||
397
- !project.devProperties.signingUsername) {
398
- console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
399
- console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
95
+ if (flags['signed']) {
96
+ signingCredentials = await resolveSigningForCli(project);
97
+ if (!signingCredentials)
400
98
  return;
401
- }
402
- const signingUsername = project.devProperties.signingUsername;
403
- const password = await resolveSigningPassword(signingUsername);
404
- if (!password) {
405
- console.log('\x1b[31mError: Password is required for signed mode\x1b[0m');
406
- return;
407
- }
408
- signingCredentials = {
409
- username: signingUsername,
410
- password,
411
- certificateName: project.devProperties.certificateName,
412
- };
413
99
  }
414
- const { waitUntilExit } = render(_jsx(DevScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, signed: signed, signingCredentials: signingCredentials }));
100
+ const { waitUntilExit } = render(_jsx(DevScreen, { project: project, options: {
101
+ deploy: true,
102
+ signingCredentials,
103
+ deployConfig: {
104
+ domain: dev.domain,
105
+ siteName: dev.siteName,
106
+ addonName: dev.addonName,
107
+ username: dev.username,
108
+ password: dev.password,
109
+ accessToken: dev.accessToken,
110
+ sessionCookie: dev.sessionCookie,
111
+ useHTTP: dev.useHTTPForDevDeploy,
112
+ },
113
+ } }));
415
114
  await waitUntilExit();
416
115
  },
417
116
  };
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from 'ink';
3
- import { DevScreen } from './dev.js';
4
- import { resolveSigningPassword } from '../utils/signing-password.js';
3
+ import { DevScreen, resolveSigningForCli } from './dev.js';
5
4
  export const watchCommand = {
6
5
  name: 'watch',
7
6
  description: 'Watch and rebuild (optionally signing) without deploying',
@@ -15,30 +14,13 @@ export const watchCommand = {
15
14
  },
16
15
  },
17
16
  async execute({ project, flags }) {
18
- const signed = Boolean(flags['signed']);
19
17
  let signingCredentials;
20
- // Signed mode: resolve signing credentials (keychain → env → prompt).
21
- // No deploy credentials are needed — watch never deploys.
22
- if (signed) {
23
- if (!project.hasSigningProperties ||
24
- !project.devProperties?.signingUsername) {
25
- console.log('\n\x1b[33mSigning credentials not configured.\x1b[0m');
26
- console.log('Run \x1b[36msetup-signing\x1b[0m to configure credentials.\n');
18
+ if (flags['signed']) {
19
+ signingCredentials = await resolveSigningForCli(project);
20
+ if (!signingCredentials)
27
21
  return;
28
- }
29
- const signingUsername = project.devProperties.signingUsername;
30
- const password = await resolveSigningPassword(signingUsername);
31
- if (!password) {
32
- console.log('\x1b[31mError: Password is required for signed mode\x1b[0m');
33
- return;
34
- }
35
- signingCredentials = {
36
- username: signingUsername,
37
- password,
38
- certificateName: project.devProperties.certificateName,
39
- };
40
22
  }
41
- const { waitUntilExit } = render(_jsx(DevScreen, { projectRoot: project.root, manifest: project.manifest, devProperties: project.devProperties, signed: signed, deploy: false, signingCredentials: signingCredentials }));
23
+ const { waitUntilExit } = render(_jsx(DevScreen, { project: project, options: { deploy: false, signingCredentials } }));
42
24
  await waitUntilExit();
43
25
  },
44
26
  };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Fragment, useEffect, useRef, useState } from 'react';
3
- import { Box, Text } from 'ink';
3
+ import { Box, Text, useStdout } from 'ink';
4
4
  import { AUTHOR, BIG_LOGO } from '../utils/branding.js';
5
5
  const FRAME_MS = 45;
6
6
  const SWEEP_COLS_PER_FRAME = 7; // how fast the wipe edge moves left → right
@@ -83,5 +83,11 @@ export function AnimatedLogo({ art = BIG_LOGO, onDone }) {
83
83
  }
84
84
  }, [frame, total, onDone]);
85
85
  const edge = (frame + 1) * SWEEP_COLS_PER_FRAME;
86
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [art.map((line, y) => (_jsx(Text, { children: buildSpans(line, y, frame, edge).map((span, index) => (_jsx(Fragment, { children: span.color ? (_jsx(Text, { color: span.color, children: span.text })) : (_jsx(Text, { children: span.text })) }, index))) }, y))), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ' a tool by ' }), _jsx(Text, { bold: true, children: AUTHOR })] })] }));
86
+ // Centre the wordmark on the alternate screen. `rows - 1` leaves room for
87
+ // Ink's trailing newline; a terminal too short for the art just renders it
88
+ // top-aligned rather than scrolling.
89
+ const { stdout } = useStdout();
90
+ const height = (stdout.rows || 24) - 1;
91
+ const fits = height >= art.length + 2;
92
+ return (_jsx(Box, { width: stdout.columns || 80, height: fits ? height : undefined, alignItems: "center", justifyContent: "center", children: _jsxs(Box, { flexDirection: "column", padding: 1, children: [art.map((line, y) => (_jsx(Text, { children: buildSpans(line, y, frame, edge).map((span, index) => (_jsx(Fragment, { children: span.color ? (_jsx(Text, { color: span.color, children: span.text })) : (_jsx(Text, { children: span.text })) }, index))) }, y))), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ' a tool by ' }), _jsx(Text, { bold: true, children: AUTHOR })] })] }) }));
87
93
  }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import type { DevProperties } from '../types/index.js';
3
+ interface Credential {
4
+ accessToken?: string;
5
+ sessionCookie?: string;
6
+ }
7
+ interface Props {
8
+ method: 'oauth2' | 'cookie';
9
+ devProperties: DevProperties;
10
+ onComplete: (credential: Credential) => void;
11
+ onError: (message: string) => void;
12
+ onCancel: () => void;
13
+ }
14
+ /**
15
+ * Ink-native interactive login for OAuth2 and session-cookie auth. Drives the
16
+ * browser and (for cookie) the "press Enter to capture" handoff through Ink's
17
+ * own input, so it works inside the TUI as well as the standalone command —
18
+ * neither needs to own raw stdin the way the old console prompt did.
19
+ */
20
+ export declare function AuthLoginScreen({ method, devProperties, onComplete, onError, onCancel, }: Props): React.JSX.Element;
21
+ export {};
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+ import { t } from '../utils/i18n.js';
6
+ import { beginOAuth2Login, openBrowser } from '../utils/oauth2-auth.js';
7
+ import { beginCookieLogin, } from '../utils/session-cookie-auth.js';
8
+ /**
9
+ * Ink-native interactive login for OAuth2 and session-cookie auth. Drives the
10
+ * browser and (for cookie) the "press Enter to capture" handoff through Ink's
11
+ * own input, so it works inside the TUI as well as the standalone command —
12
+ * neither needs to own raw stdin the way the old console prompt did.
13
+ */
14
+ export function AuthLoginScreen({ method, devProperties, onComplete, onError, onCancel, }) {
15
+ const [phase, setPhase] = React.useState('starting');
16
+ const [authUrl, setAuthUrl] = React.useState('');
17
+ const [note, setNote] = React.useState('');
18
+ const [loginUrl, setLoginUrl] = React.useState('');
19
+ const cookieRef = React.useRef(null);
20
+ const cancelOAuthRef = React.useRef(null);
21
+ React.useEffect(() => {
22
+ void (async () => {
23
+ if (method === 'oauth2') {
24
+ const session = beginOAuth2Login(devProperties);
25
+ if (!session) {
26
+ onError('OAuth2 is not fully configured (authorization/token endpoint or client ID missing).');
27
+ return;
28
+ }
29
+ cancelOAuthRef.current = session.cancel;
30
+ setAuthUrl(session.authUrl);
31
+ openBrowser(session.authUrl);
32
+ setPhase('awaiting');
33
+ const { token, error } = await session.complete();
34
+ cancelOAuthRef.current = null;
35
+ if (token) {
36
+ onComplete({ accessToken: token });
37
+ }
38
+ else {
39
+ onError(error ?? 'OAuth2 login failed.');
40
+ }
41
+ }
42
+ else {
43
+ const session = await beginCookieLogin(devProperties);
44
+ if (!session) {
45
+ onError('Could not open a login browser (is Chrome installed?). Set SITEVISION_SESSION_COOKIE or pass --cookie with a cookie copied from your browser.');
46
+ return;
47
+ }
48
+ cookieRef.current = session;
49
+ setLoginUrl(session.loginUrl);
50
+ setPhase('awaiting');
51
+ }
52
+ })();
53
+ // Release resources if the screen unmounts before completing: close the
54
+ // browser (cookie) and the loopback server (oauth2, frees the port).
55
+ return () => {
56
+ void cookieRef.current?.close();
57
+ cookieRef.current = null;
58
+ cancelOAuthRef.current?.();
59
+ cancelOAuthRef.current = null;
60
+ };
61
+ // Run once: the parent mounts this fresh when a login is needed.
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, []);
64
+ useInput((_input, key) => {
65
+ if (key.escape) {
66
+ onCancel();
67
+ return;
68
+ }
69
+ if (method === 'cookie' && phase === 'awaiting' && key.return) {
70
+ const session = cookieRef.current;
71
+ if (!session)
72
+ return;
73
+ setPhase('capturing');
74
+ void (async () => {
75
+ const result = await session.capture();
76
+ if (result.cookie) {
77
+ cookieRef.current = null;
78
+ await session.close();
79
+ onComplete({ sessionCookie: result.cookie });
80
+ }
81
+ else {
82
+ // Keep the browser open so the user can navigate and retry.
83
+ setNote(result.error ?? 'No session cookie found.');
84
+ setPhase('awaiting');
85
+ }
86
+ })();
87
+ }
88
+ });
89
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: method === 'oauth2' ? t('OAuth2 login') : t('Session cookie login') }) }), method === 'oauth2' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Waiting for you to finish login in the browser…')] })] }), authUrl && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: t("If the browser didn't open, visit:") }), _jsx(Text, { children: authUrl })] }))] })) : (_jsxs(Box, { flexDirection: "column", children: [phase === 'starting' && (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Opening browser…')] })] })), phase === 'awaiting' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: t('A Chrome window is open at:') }), _jsx(Text, { color: "cyan", children: loginUrl }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: t('1. Log in to the site there, single sign-on included.') }), _jsx(Text, { children: t('2. Wait until the site itself has finished loading.') }), _jsx(Text, { children: t('3. Come back here and press Enter.') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('Leave the browser window open; it closes once the session is captured.') }) })] })), phase === 'capturing' && (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Capturing session…')] })] })), note && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: note }) }))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('Press Esc to cancel.') }) })] }));
90
+ }