screenci 0.0.74 → 0.0.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +150 -37
- package/dist/cli.js.map +1 -1
- package/dist/docs/manifest.d.ts +42 -11
- package/dist/docs/manifest.d.ts.map +1 -1
- package/dist/docs/manifest.js +14 -3
- package/dist/docs/manifest.js.map +1 -1
- package/dist/docs/video-sources/installation.screenci.js +7 -9
- package/dist/docs/video-sources/installation.screenci.js.map +1 -1
- package/dist/docs/video-sources/landing.screenci.js +6 -8
- package/dist/docs/video-sources/landing.screenci.js.map +1 -1
- package/dist/docs/video-sources/locator-highlight-animated.screenci.js +3 -4
- package/dist/docs/video-sources/locator-highlight-animated.screenci.js.map +1 -1
- package/dist/docs/video-sources/locator-highlight-still.screenci.js +3 -4
- package/dist/docs/video-sources/locator-highlight-still.screenci.js.map +1 -1
- package/dist/docs/video-sources/studio.screenci.js +8 -10
- package/dist/docs/video-sources/studio.screenci.js.map +1 -1
- package/dist/e2e/instrument.e2e.js +52 -1
- package/dist/e2e/instrument.e2e.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/src/asset.d.ts +20 -1
- package/dist/src/asset.d.ts.map +1 -1
- package/dist/src/asset.js +58 -7
- package/dist/src/asset.js.map +1 -1
- package/dist/src/audio.d.ts +16 -0
- package/dist/src/audio.d.ts.map +1 -1
- package/dist/src/audio.js +78 -1
- package/dist/src/audio.js.map +1 -1
- package/dist/src/autoZoom.d.ts.map +1 -1
- package/dist/src/autoZoom.js +3 -1
- package/dist/src/autoZoom.js.map +1 -1
- package/dist/src/browserLaunchOptions.d.ts +24 -2
- package/dist/src/browserLaunchOptions.d.ts.map +1 -1
- package/dist/src/browserLaunchOptions.js +57 -3
- package/dist/src/browserLaunchOptions.js.map +1 -1
- package/dist/src/builder.d.ts +135 -142
- package/dist/src/builder.d.ts.map +1 -1
- package/dist/src/builder.js +218 -77
- package/dist/src/builder.js.map +1 -1
- package/dist/src/callerFile.d.ts +12 -0
- package/dist/src/callerFile.d.ts.map +1 -0
- package/dist/src/callerFile.js +46 -0
- package/dist/src/callerFile.js.map +1 -0
- package/dist/src/changeFocus.d.ts.map +1 -1
- package/dist/src/changeFocus.js +6 -2
- package/dist/src/changeFocus.js.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +15 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/declare.d.ts +59 -0
- package/dist/src/declare.d.ts.map +1 -0
- package/dist/src/declare.js +93 -0
- package/dist/src/declare.js.map +1 -0
- package/dist/src/defaults.d.ts.map +1 -1
- package/dist/src/defaults.js +4 -3
- package/dist/src/defaults.js.map +1 -1
- package/dist/src/events.d.ts +51 -1
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +107 -15
- package/dist/src/events.js.map +1 -1
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +32 -30
- package/dist/src/init.js.map +1 -1
- package/dist/src/instrument.d.ts.map +1 -1
- package/dist/src/instrument.js +139 -1
- package/dist/src/instrument.js.map +1 -1
- package/dist/src/localize.d.ts +1 -0
- package/dist/src/localize.d.ts.map +1 -1
- package/dist/src/localize.js +1 -1
- package/dist/src/localize.js.map +1 -1
- package/dist/src/localizeRuntime.d.ts +17 -26
- package/dist/src/localizeRuntime.d.ts.map +1 -1
- package/dist/src/localizeRuntime.js +63 -48
- package/dist/src/localizeRuntime.js.map +1 -1
- package/dist/src/manualZoom.d.ts.map +1 -1
- package/dist/src/manualZoom.js +8 -5
- package/dist/src/manualZoom.js.map +1 -1
- package/dist/src/openBrowser.d.ts +34 -0
- package/dist/src/openBrowser.d.ts.map +1 -0
- package/dist/src/openBrowser.js +62 -0
- package/dist/src/openBrowser.js.map +1 -0
- package/dist/src/recordingData.d.ts +20 -0
- package/dist/src/recordingData.d.ts.map +1 -1
- package/dist/src/screenAudio.d.ts +58 -0
- package/dist/src/screenAudio.d.ts.map +1 -0
- package/dist/src/screenAudio.js +145 -0
- package/dist/src/screenAudio.js.map +1 -0
- package/dist/src/screenAudioSink.d.ts +34 -0
- package/dist/src/screenAudioSink.d.ts.map +1 -0
- package/dist/src/screenAudioSink.js +47 -0
- package/dist/src/screenAudioSink.js.map +1 -0
- package/dist/src/screenshot.d.ts +20 -24
- package/dist/src/screenshot.d.ts.map +1 -1
- package/dist/src/screenshot.js +34 -19
- package/dist/src/screenshot.js.map +1 -1
- package/dist/src/stillCapture.d.ts +5 -0
- package/dist/src/stillCapture.d.ts.map +1 -1
- package/dist/src/stillCapture.js +11 -1
- package/dist/src/stillCapture.js.map +1 -1
- package/dist/src/studio.d.ts +3 -59
- package/dist/src/studio.d.ts.map +1 -1
- package/dist/src/studio.js +1 -41
- package/dist/src/studio.js.map +1 -1
- package/dist/src/types.d.ts +133 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/video.d.ts +63 -52
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js +143 -32
- package/dist/src/video.js.map +1 -1
- package/dist/src/zoom.d.ts +1 -0
- package/dist/src/zoom.d.ts.map +1 -1
- package/dist/src/zoom.js +3 -0
- package/dist/src/zoom.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/skills/screenci/SKILL.md +2 -2
- package/skills/screenci/references/init.md +4 -3
- package/skills/screenci/references/record.md +1 -1
- package/dist/docs/video-sources/public-urls.screenci.d.ts +0 -2
- package/dist/docs/video-sources/public-urls.screenci.d.ts.map +0 -1
- package/dist/docs/video-sources/public-urls.screenci.js +0 -41
- package/dist/docs/video-sources/public-urls.screenci.js.map +0 -1
package/dist/cli.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ type PreparedUploadAsset = {
|
|
|
32
32
|
name?: string;
|
|
33
33
|
fileBuffer?: Buffer;
|
|
34
34
|
contentType?: string;
|
|
35
|
+
alwaysUpload?: boolean;
|
|
35
36
|
};
|
|
36
37
|
export type UploadStudioInfo = {
|
|
37
38
|
held: true;
|
|
@@ -79,6 +80,16 @@ export declare function ensureScreenciSecret(resolvedConfigPath?: string, opts?:
|
|
|
79
80
|
pollAuth?: boolean;
|
|
80
81
|
noPollAuth?: boolean;
|
|
81
82
|
}): Promise<string | undefined>;
|
|
83
|
+
/**
|
|
84
|
+
* `screenci login`: get a sign-in link in front of a human as early as
|
|
85
|
+
* possible. It reuses the link `init` (or a prior run) persisted, or creates a
|
|
86
|
+
* fresh one, tries to open it in the browser best-effort, and always prints it
|
|
87
|
+
* as a fallback. It does NOT wait for sign-in to finish: the agent can run this,
|
|
88
|
+
* keep authoring and `test`ing, then `record` (which blocks for sign-in) once
|
|
89
|
+
* the script is green. If sign-in already completed, it saves the secret and
|
|
90
|
+
* exits.
|
|
91
|
+
*/
|
|
92
|
+
export declare function runLogin(configPath?: string): Promise<void>;
|
|
82
93
|
export declare function main(): Promise<void>;
|
|
83
94
|
export declare function parseRecordCliArgs(args: string[]): {
|
|
84
95
|
configPath: string | undefined;
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EACV,uBAAuB,EACvB,aAAa,EAEd,MAAM,iBAAiB,CAAA;AASxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAkB,MAAM,gBAAgB,CAAA;AAKxE,OAAO,EAGL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EAOlB,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { SCREENCI_DISABLE_RECORDING_TIMINGS_ENV, SCREENCI_MOCK_RECORD_ENV, SCREE
|
|
|
14
14
|
import { DEFAULT_RECORD_UPLOAD_POLICY } from './src/defaults.js';
|
|
15
15
|
import { findDuplicateTitles, formatDuplicateTitlesMessage, } from './src/titleValidation.js';
|
|
16
16
|
import { createLinkSessionSpec, deletePersistedLinkSessionSpec, getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl, getLinkSessionFilePath, getScreenCIEnvironment, isStoredLinkSessionReusable, readPersistedLinkSessionSpec, SCREENCI_LINK_SESSION_FILE, writePersistedLinkSessionSpec, } from './src/linkSession.js';
|
|
17
|
+
import { openUrlInBrowser } from './src/openBrowser.js';
|
|
17
18
|
// Re-export the environment-aware URL helpers so existing importers (and tests)
|
|
18
19
|
// can keep importing them from the CLI entrypoint.
|
|
19
20
|
export { getCliLinkSessionApiUrl, getDevBackendUrl, getDevFrontendUrl };
|
|
@@ -214,6 +215,9 @@ function contentTypeForPath(filePath) {
|
|
|
214
215
|
mp4: 'video/mp4',
|
|
215
216
|
webm: 'video/webm',
|
|
216
217
|
mp3: 'audio/mpeg',
|
|
218
|
+
wav: 'audio/wav',
|
|
219
|
+
m4a: 'audio/mp4',
|
|
220
|
+
aac: 'audio/aac',
|
|
217
221
|
svg: 'image/svg+xml',
|
|
218
222
|
};
|
|
219
223
|
return contentTypeMap[ext] ?? 'application/octet-stream';
|
|
@@ -352,7 +356,7 @@ async function loadUploadCandidate(screenciDir, entry, verbose) {
|
|
|
352
356
|
preparedUploadAssets,
|
|
353
357
|
};
|
|
354
358
|
}
|
|
355
|
-
async function uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl, secret, elevenLabsApiKey, verbose, uploadAbort, progressReporter, progressIndex, recordId) {
|
|
359
|
+
async function uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl, secret, elevenLabsApiKey, verbose, uploadAbort, progressReporter, progressIndex, recordId, expectedScreenshotCount) {
|
|
356
360
|
const { entry, videoName, data, preparedUploadAssets } = candidate;
|
|
357
361
|
let projectId = null;
|
|
358
362
|
let videoId = null;
|
|
@@ -395,6 +399,7 @@ async function uploadRecordingCandidate(candidate, screenciDir, projectName, api
|
|
|
395
399
|
data,
|
|
396
400
|
recordingHash,
|
|
397
401
|
recordId,
|
|
402
|
+
...(isScreenshot ? { expectedScreenshotCount } : {}),
|
|
398
403
|
expectedAssets: preparedUploadAssets.map((asset) => ({
|
|
399
404
|
fileHash: asset.fileHash,
|
|
400
405
|
size: asset.size,
|
|
@@ -879,6 +884,27 @@ export async function collectUploadAssets(data, configDir) {
|
|
|
879
884
|
});
|
|
880
885
|
continue;
|
|
881
886
|
}
|
|
887
|
+
if (event.type === 'audioStart') {
|
|
888
|
+
// Studio audio tracks have no local file.
|
|
889
|
+
if ('studio' in event && event.studio === true)
|
|
890
|
+
continue;
|
|
891
|
+
if (!event.fileHash || assets.has(`hash:${event.fileHash}`))
|
|
892
|
+
continue;
|
|
893
|
+
const resolvedFile = await readRecordingFile(event.path, configDir, sourceFilePath);
|
|
894
|
+
if (resolvedFile === null) {
|
|
895
|
+
logger.warn(`Audio file not found, skipping upload: ${event.path}`);
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
assets.set(`hash:${event.fileHash}`, {
|
|
899
|
+
fileHash: event.fileHash,
|
|
900
|
+
path: event.path,
|
|
901
|
+
size: resolvedFile.buffer.byteLength,
|
|
902
|
+
fileBuffer: resolvedFile.buffer,
|
|
903
|
+
contentType: contentTypeForPath(resolvedFile.resolvedPath),
|
|
904
|
+
...(event.name === '__screen' && { alwaysUpload: true }),
|
|
905
|
+
});
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
882
908
|
if (event.type === 'videoCueStart') {
|
|
883
909
|
// Single-language: hash already computed during recording, use assetPath to read file
|
|
884
910
|
if (typeof event.assetHash === 'string' &&
|
|
@@ -999,7 +1025,7 @@ function hint401(status, secret) {
|
|
|
999
1025
|
if (status !== 401 || !secret)
|
|
1000
1026
|
return '';
|
|
1001
1027
|
const frontendUrl = getDevFrontendUrl();
|
|
1002
|
-
return `\nThe secret may have been deleted
|
|
1028
|
+
return `\nThe secret may have been deleted. Check your secrets at ${frontendUrl}/secrets`;
|
|
1003
1029
|
}
|
|
1004
1030
|
export function formatUploadStartFailureMessage(videoName, status, responseText, secret) {
|
|
1005
1031
|
if (responseText.trim().length > 0) {
|
|
@@ -1053,49 +1079,53 @@ async function uploadAssets(assets, apiUrl, secret, recordingId, signal, throwIf
|
|
|
1053
1079
|
for (const asset of assets) {
|
|
1054
1080
|
throwIfAborted();
|
|
1055
1081
|
try {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1082
|
+
if (!asset.alwaysUpload) {
|
|
1083
|
+
const checkRes = await withUploadRetry(() => fetch(`${apiUrl}/cli/upload/${recordingId}/asset/check`, {
|
|
1084
|
+
method: 'POST',
|
|
1085
|
+
headers: {
|
|
1086
|
+
'Content-Type': 'application/json',
|
|
1087
|
+
'X-ScreenCI-Secret': secret,
|
|
1088
|
+
},
|
|
1089
|
+
body: JSON.stringify({
|
|
1090
|
+
fileHash: asset.fileHash,
|
|
1091
|
+
contentType: asset.contentType,
|
|
1092
|
+
size: asset.size,
|
|
1093
|
+
path: asset.path,
|
|
1094
|
+
...(typeof asset.name === 'string' ? { name: asset.name } : {}),
|
|
1095
|
+
}),
|
|
1096
|
+
signal,
|
|
1097
|
+
}), signal);
|
|
1098
|
+
if (!checkRes.ok) {
|
|
1099
|
+
const text = await checkRes.text();
|
|
1100
|
+
throw new UploadAssetError(`Failed to check asset ${asset.path}: ${checkRes.status} ${text}${hint401(checkRes.status, secret)}`);
|
|
1101
|
+
}
|
|
1102
|
+
const checkBody = (await checkRes.json());
|
|
1103
|
+
if (checkBody.exists) {
|
|
1104
|
+
logInfo(`${pc.green('✔')} Asset already exists: ${displayAssetPath(asset.path)}`);
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1079
1107
|
}
|
|
1080
1108
|
if (!asset.fileBuffer || !asset.contentType) {
|
|
1081
1109
|
throw new UploadAssetError(`Asset bytes not available for upload and backend does not have it yet: ${asset.path}`);
|
|
1082
1110
|
}
|
|
1083
1111
|
const fileBuffer = asset.fileBuffer;
|
|
1112
|
+
const contentType = asset.contentType;
|
|
1084
1113
|
throwIfAborted();
|
|
1085
|
-
const res = await withUploadRetry(() =>
|
|
1114
|
+
const res = await withUploadRetry(() =>
|
|
1115
|
+
// Send the raw bytes, not base64-in-JSON: base64 builds a string that
|
|
1116
|
+
// exceeds Node's max string length (~536MB) for large assets. Metadata
|
|
1117
|
+
// travels in headers (URI-encoded so non-ASCII paths stay header-safe).
|
|
1118
|
+
fetch(`${apiUrl}/cli/upload/${recordingId}/asset/stream`, {
|
|
1086
1119
|
method: 'PUT',
|
|
1087
1120
|
headers: {
|
|
1088
|
-
'Content-Type':
|
|
1121
|
+
'Content-Type': contentType,
|
|
1122
|
+
'Content-Length': String(fileBuffer.byteLength),
|
|
1089
1123
|
'X-ScreenCI-Secret': secret,
|
|
1124
|
+
'X-ScreenCI-File-Hash': asset.fileHash,
|
|
1125
|
+
'X-ScreenCI-Asset-Size': String(asset.size),
|
|
1126
|
+
'X-ScreenCI-Asset-Path': encodeURIComponent(asset.path),
|
|
1090
1127
|
},
|
|
1091
|
-
body:
|
|
1092
|
-
fileHash: asset.fileHash,
|
|
1093
|
-
fileBase64: fileBuffer.toString('base64'),
|
|
1094
|
-
contentType: asset.contentType,
|
|
1095
|
-
size: asset.size,
|
|
1096
|
-
path: asset.path,
|
|
1097
|
-
...(typeof asset.name === 'string' ? { name: asset.name } : {}),
|
|
1098
|
-
}),
|
|
1128
|
+
body: fileBuffer,
|
|
1099
1129
|
signal,
|
|
1100
1130
|
}), signal);
|
|
1101
1131
|
if (!res.ok) {
|
|
@@ -1163,7 +1193,11 @@ export async function uploadRecordings(screenciDir, projectName, apiUrl, secret,
|
|
|
1163
1193
|
};
|
|
1164
1194
|
}
|
|
1165
1195
|
const progressReporter = createUploadProgressReporter(candidates.map((candidate) => candidate.videoName), verbose);
|
|
1166
|
-
|
|
1196
|
+
// Screenshots from this run render together on one machine; the backend
|
|
1197
|
+
// waits for all of them to land before dispatching the batch, so it needs
|
|
1198
|
+
// to know how many to expect.
|
|
1199
|
+
const screenshotCount = candidates.filter((candidate) => candidate.data.output === 'screenshot').length;
|
|
1200
|
+
const results = await Promise.all(candidates.map(async (candidate, index) => await uploadRecordingCandidate(candidate, screenciDir, projectName, apiUrl, secret, elevenLabsApiKey, verbose, uploadAbort, progressReporter, index, recordId, screenshotCount)));
|
|
1167
1201
|
firstProjectId =
|
|
1168
1202
|
results.find((result) => result.projectId !== null)?.projectId ?? null;
|
|
1169
1203
|
const resolvedPlan = results.find((result) => result.plan !== undefined)?.plan ?? null;
|
|
@@ -1757,10 +1791,82 @@ export async function ensureScreenciSecret(resolvedConfigPath, opts = {}) {
|
|
|
1757
1791
|
return undefined;
|
|
1758
1792
|
}
|
|
1759
1793
|
}
|
|
1794
|
+
/**
|
|
1795
|
+
* `screenci login`: get a sign-in link in front of a human as early as
|
|
1796
|
+
* possible. It reuses the link `init` (or a prior run) persisted, or creates a
|
|
1797
|
+
* fresh one, tries to open it in the browser best-effort, and always prints it
|
|
1798
|
+
* as a fallback. It does NOT wait for sign-in to finish: the agent can run this,
|
|
1799
|
+
* keep authoring and `test`ing, then `record` (which blocks for sign-in) once
|
|
1800
|
+
* the script is green. If sign-in already completed, it saves the secret and
|
|
1801
|
+
* exits.
|
|
1802
|
+
*/
|
|
1803
|
+
export async function runLogin(configPath) {
|
|
1804
|
+
const { resolvedConfigPath } = await loadScreenCIConfigAndEnv(configPath);
|
|
1805
|
+
if (process.env.SCREENCI_SECRET) {
|
|
1806
|
+
logger.info(`Already signed in (SCREENCI_SECRET is set). Run ${pc.cyan(getSuggestedScreenciCommand('record'))} to record.`);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const environment = getScreenCIEnvironment();
|
|
1810
|
+
const apiUrl = getCliLinkSessionApiUrl();
|
|
1811
|
+
const appUrl = getDevFrontendUrl();
|
|
1812
|
+
const envFilePath = await resolveProjectEnvFilePath(resolvedConfigPath);
|
|
1813
|
+
const projectDir = dirname(resolvedConfigPath);
|
|
1814
|
+
const specPath = getLinkSessionFilePath(projectDir);
|
|
1815
|
+
const linkSessionContext = { environment, envFilePath, resolvedConfigPath };
|
|
1816
|
+
const createSpec = async () => {
|
|
1817
|
+
const created = await createLinkSessionSpec({
|
|
1818
|
+
apiUrl,
|
|
1819
|
+
appUrl,
|
|
1820
|
+
...linkSessionContext,
|
|
1821
|
+
});
|
|
1822
|
+
await writePersistedLinkSessionSpec(specPath, created);
|
|
1823
|
+
return created;
|
|
1824
|
+
};
|
|
1825
|
+
try {
|
|
1826
|
+
// Reuse a still-valid persisted session so login and a later record share
|
|
1827
|
+
// the same link; otherwise create and persist a fresh one.
|
|
1828
|
+
const stored = await readPersistedLinkSessionSpec(specPath);
|
|
1829
|
+
let spec = stored && isStoredLinkSessionReusable(stored, linkSessionContext)
|
|
1830
|
+
? stored
|
|
1831
|
+
: await createSpec();
|
|
1832
|
+
// If sign-in already completed (e.g. between runs) save the secret and stop:
|
|
1833
|
+
// there is nothing left to open. Recreate a stale session before opening so
|
|
1834
|
+
// the link we hand off is valid.
|
|
1835
|
+
const result = await pollLinkSessionOnce(spec);
|
|
1836
|
+
if (result.status === 'completed' && result.secret) {
|
|
1837
|
+
process.env.SCREENCI_SECRET = result.secret;
|
|
1838
|
+
await persistScreenCISecret(envFilePath, result.secret);
|
|
1839
|
+
deletePersistedLinkSessionSpec(specPath);
|
|
1840
|
+
logger.info(`Already signed in. Saved SCREENCI_SECRET to ${envFilePath}.`);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (result.status === 'expired' ||
|
|
1844
|
+
result.status === 'consumed' ||
|
|
1845
|
+
result.status === 'invalid') {
|
|
1846
|
+
deletePersistedLinkSessionSpec(specPath);
|
|
1847
|
+
spec = await createSpec();
|
|
1848
|
+
}
|
|
1849
|
+
const opened = await openUrlInBrowser(spec.appUrl);
|
|
1850
|
+
if (opened.opened) {
|
|
1851
|
+
logger.info(`Opening the sign-in link in your browser:\n${pc.cyan(spec.appUrl)}`);
|
|
1852
|
+
}
|
|
1853
|
+
else {
|
|
1854
|
+
logger.info(`Could not open a browser automatically (${opened.reason}). Open this link to sign in:\n${pc.cyan(spec.appUrl)}`);
|
|
1855
|
+
}
|
|
1856
|
+
logger.info(`The link stays valid for 24 hours. You can keep authoring and run ${pc.cyan(getSuggestedScreenciCommand('test'))} while you sign in. Then run ${pc.cyan(getSuggestedScreenciCommand('record'))}, which waits for sign-in and records once you finish.`);
|
|
1857
|
+
}
|
|
1858
|
+
catch (err) {
|
|
1859
|
+
// Login is a best-effort convenience: never fail the run on a network or
|
|
1860
|
+
// browser hiccup. record can still create and wait on the link later.
|
|
1861
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1862
|
+
logger.warn(`Could not start sign-in: ${msg}`);
|
|
1863
|
+
logger.info(`You can sign in later: ${pc.cyan(getSuggestedScreenciCommand('record'))} prints a link and waits for sign-in. In CI, set SCREENCI_SECRET (from ${getScreenCISecretsUrl()}).`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1760
1866
|
export async function main() {
|
|
1761
1867
|
if (process.argv.length <= 2) {
|
|
1762
1868
|
logger.error('Error: No command provided');
|
|
1763
|
-
logger.error('Available commands: record, test, info, make-public, make-private, init');
|
|
1869
|
+
logger.error('Available commands: record, test, login, info, make-public, make-private, init');
|
|
1764
1870
|
process.exit(1);
|
|
1765
1871
|
}
|
|
1766
1872
|
const program = new Command();
|
|
@@ -1948,6 +2054,13 @@ export async function main() {
|
|
|
1948
2054
|
const recordCommand = getSuggestedScreenciCommand('record');
|
|
1949
2055
|
logger.info(`Tests passed. Run ${pc.cyan(recordCommand)} to render the videos.`);
|
|
1950
2056
|
});
|
|
2057
|
+
program
|
|
2058
|
+
.command('login')
|
|
2059
|
+
.description('Open the sign-in link in your browser (best-effort) so recording can finish later')
|
|
2060
|
+
.option('-c, --config <path>', 'path to screenci.config.ts')
|
|
2061
|
+
.action(async (options) => {
|
|
2062
|
+
await runLogin(options['config']);
|
|
2063
|
+
});
|
|
1951
2064
|
program
|
|
1952
2065
|
.command('info')
|
|
1953
2066
|
.description("Print the latest record run's video URLs and render status as JSON")
|