mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
* fix(desktop): preserve local file URL authority * fix(desktop): preserve local file URL authority * fix(desktop): preserve local file URL authority * fix(desktop): render captured file URLs at launch * fix(desktop): classify numeric timestamps using field context * fix(desktop): classify numeric timestamps using field context * fix(desktop): classify numeric timestamps using field context * fix(desktop): retain context for generated draft values * fix(desktop): retain context for generated draft values * fix(desktop): retain context for generated draft values * test(desktop): cover generated IDs in text guards * test(desktop): cover generated IDs in text guards * test(desktop): cover generated IDs in text guards * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): isolate launcher URL encoding * fix(desktop): bound private key redaction spans * fix(desktop): retain published bundle on copy failure * fix(desktop): retain secret file cleanup owners * fix(desktop): retain both published output cleanup owners * test(desktop): verify UTF-16 credential refusal * test(desktop): verify truncated secret redaction over MCP * fix(desktop): retain clean text in noisy record fields * fix(desktop): classify added line values in one pass * fix(desktop): scan private key runs in one pass * fix(desktop): apply XML quote rules to text drafts * fix(desktop): distinguish XML attributes from quoted text * fix(desktop): preserve parsed XML line context * Format appTaskChecks.ts with the pinned oxfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): retain partial XML syntax facts * fix(desktop): recognize markdown autolinks in drafts * fix(desktop): reuse export exclusions in task drafts * fix(desktop): report unchecked client build paths * docs(desktop): clarify gzip scrub limits * test(desktop): build the fixture JWT from its parts and reformat The literal token made every edit of this file trip secret scanners; the test checks the same string. Also applies the pinned oxfmt to the key-run test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): reuse export exclusions in task drafts * docs(desktop): clarify gzip scrub limits * fix(setup): own desktop build process cleanup * fix(desktop): find no private key in NSS's key4.db password check An Electron app's NSS key database holds a password-check value shaped like an encrypted private key, so every such export was refused. An encrypted key now counts only under a password-based encryption scheme, never PBKDF2 or PBMAC1, and with at least the 48 bytes of encrypted data the smallest key needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(doctor): compare desktop settings with resolved defaults * style(desktop): format the noisy-record draft files with the pinned oxfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(desktop): give the root-array draft test its own describe The records describe grew past the 150-line lint limit with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(doctor): inspect client-recorded desktop runners * fix(doctor): reuse matching runner probes * fix(setup): propagate cancellation to desktop actions * fix(setup): stop Hopper on cancel, never report a cancelled setup ready, and keep a failed action's client setting - The Hopper install gets setup's signal: Linux already honoured it, and the macOS installer now stops its downloads and stops before mounting or copying. - runSetup checks the signal after the final doctor and close, so a cancellation there raises instead of returning ready. - A failed pack or agentenv install no longer writes its archive or runner to client configs, so the setting the client already had stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): close five withheld-state gaps a review found - A sign-in window whose walk hit its entry limit, could not read a path or left a change unconfirmed is refused, rather than trusted as complete. - The export refuses app dconf keys the sign-in window changed and the seed would ship with a different value, naming the keys, not their values. - A bundle holding a gzip file that expands past what REA scans is refused. - Withheld bytes are kept under a handle of their own, created exclusively and never replaced; a later build's different bytes take the next free handle, and a handle the computer's answer names is checked before any write. - Setting names a Python keyword or a leading digit would break get file_, and names that collide get _2, _3 in a stable order. - Adds the feature's tasks.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): redact a cut secret's start only at REA's own cuts The boundary match for a secret cut short hid the start of any ellipsis-ended text, so "Please…" became a redaction. It now applies only where text ends one of REA's value cuts, whose lengths the producers and the redactor share, and the host redacts the app's stderr and progress before it quotes their end. Redacting before anything cuts stays a follow-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(setup): keep the completed setup result in a cancellation after cleanup A cancellation during the final doctor or host close carried only the applied actions, dropping each client's result and backup path and the doctor report. It now carries the whole result as setup_result, as doctor's cancellation carries its report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(desktop): publish secret files atomically, decompress a gzip seed once, and scan JSON strings linearly - A secret file is written under a private temporary name and linked to its handle, so a concurrent export never reads a half-written file and reuses its handle for other bytes. - The export decompresses each gzip seed file once and shares the result with the secret scan, the credential scan and the size refusal. - The cut-secret stopgap finds JSON string literals in one forward pass instead of restarting at every quote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(desktop): move launch execution to boundary suite * fix(desktop): redact build schema failure output * refactor(desktop): normalize schema failure once * fix(desktop): omit unsafe schema parser snippets * fix(desktop): preserve draft change and selected path evidence * test(desktop): group retained dconf note regressions * fix(desktop): respect archive entry format in draft checks * test(desktop): separate unowned-file draft note cases * fix(desktop): preserve explicitly selected journal documents * fix(xml): avoid scanning selected ancestors per tag * fix(desktop): exclude covered text runs from draft counts * fix: preserve text when drafting desktop checks * fix: restore nested desktop state aliases * refactor: share UUID recognition across draft checks * fix: retain multiline XML attribute context * fix: retain relationship identifiers in task drafts * docs: clarify desktop draft identifier handling * test(desktop): type formatting run cases as pairs * fix(desktop): carry document format into selector evaluation * test(desktop): separate markup boundary regression group * fix(desktop): propagate formats through draft selectors * fix(desktop): switch selector format within archive entries * test(desktop): remove duplicate seed filename case * fix(setup): retain diagnostics when cancellation overlaps failure * refactor(setup): keep command cancellation at its owner * test(setup): verify cancellation at the application boundary * test(desktop): pass JSON format in exported index parity * docs(desktop): clarify task checks before scratch export * fix(desktop): respell selected paths in file URLs * test(desktop): group captured document spelling cases * fix(desktop): retain temporary secret file cleanup * fix(desktop): preserve unmarked token-shaped values * fix(desktop): withhold drafts after secret use * fix(desktop): guard drafts against shared secret changes * test(desktop): compare preserved seed bytes without enumeration * fix(desktop): bound draft preview serialization * fix(desktop): restore selected state aliases * fix(desktop): respect entry format in draft noise * test(desktop): match exported alias evidence at its JSON boundary * fix(desktop): retain app state logs in exported seeds * fix: align desktop numeric seed comparisons * test(desktop): keep one draft exclusion fixture import * test(desktop): retain log eligibility across draft test groups * fix(doctor): resolve empty registered desktop runner * style: fix desktop launch test formatting * style: format native command path schema * fix(desktop): map selected external launch paths * style(desktop): format seed document regressions * test(desktop): group launch URL regressions * fix(desktop): omit redundant nested state aliases * fix(desktop): compare added free text as a whole * fix(desktop): check recompressed seeds as shipped * docs(desktop): clarify recovered seed format * test(desktop): group shipped gzip seed checks * fix: retain quoted identifier text in task drafts * fix: recognize Unicode XML in desktop state readers * fix(desktop): bound nested draft checks * fix(desktop): explain missing arrays in truncated views * fix(desktop): find XML type references in archives * test(desktop): remove duplicated capture assertions * fix(desktop): retain nested omitted text boundaries * style(desktop): format draft value imports * test(desktop): group model and draft value checks * fix(desktop): map captured launch paths once * refactor: remove unused whole-text guard state * refactor: stop passing unused text children * test: align free-text checks with whole-note policy * fix: validate recompressed desktop archive seeds * style(desktop): format archive seed regression * fix(desktop): retain placed-item sibling guards * fix(desktop): preserve archive paths and bound doctor reads * fix(doctor): refuse non-files before descriptor admission * fix(setup): track remaining cleanup resources across retries * test(desktop): specify XML format in heap pairing fixture * fix(doctor): query agentenv for model endpoint * fix(doctor): preserve uncertainty for empty endpoint override * fix(desktop): protect host controls and quote withheld paths * fix: redact declared form-encoded secrets * fix(desktop): redact runtime schema errors * fix(desktop): normalize captured file URLs * fix(desktop): report seeded files by capture path * fix(desktop): retain startup readiness failure stage * fix(desktop): retain JSON date checks * fix(desktop): preserve archive formatting checks * fix(desktop): preserve explicitly selected journal files * fix(desktop): report build command transport limits * docs(desktop): clarify unreadable dconf versions * test(desktop): remove duplicate history regression * fix(desktop): align XML names with exported reader * fix(desktop): create selected state alias targets * fix(desktop): inspect supported runtime archives * fix(desktop): retain record identity in draft checks * fix(desktop): refuse unchecked declared secrets * fix(desktop): retain partial withheld export receipts * fix(desktop): detect escaped secrets in JSON state * fix(desktop): scrub XML independently of reader compatibility * fix(desktop): preserve file aliases and reader diagnostics * fix(desktop): scan escaped secrets in UTF-16 JSON * style(desktop): format secret scan changes * style(desktop): format review fixes * style(desktop): format capture review fixes * style(desktop): format review changes --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
515 lines
18 KiB
TypeScript
515 lines
18 KiB
TypeScript
import { createInterface } from "node:readline";
|
|
|
|
import { z } from "incur";
|
|
import type { Logger } from "pino";
|
|
|
|
import { captureDesktopApplication } from "../application/desktop/DesktopApplicationCapture.js";
|
|
import type { DesktopStateReaders } from "../application/desktop/DesktopApplicationState.js";
|
|
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
|
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js";
|
|
import type { Evidence } from "../domain/evidence.js";
|
|
import { localPathStringSchema } from "../domain/localPath.js";
|
|
import { err, ok } from "../domain/result.js";
|
|
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
|
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
|
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
|
import { cliDecimalField } from "./options.js";
|
|
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
|
import { logCliCommand } from "../cliLogging.js";
|
|
import {
|
|
DESKTOP_CAPTURE_DEFAULTS,
|
|
type DesktopCaptureProgress,
|
|
} from "../domain/desktop/desktopCapture.js";
|
|
import { SessionRequestQueue } from "../desktop/DesktopSessionRequests.js";
|
|
import { withCommandCancellation } from "./commandCancellation.js";
|
|
import type { CliInstance } from "./types.js";
|
|
|
|
const positive = (fallback: number) =>
|
|
cliDecimalField(z.int().positive().default(fallback));
|
|
|
|
/** Options of `capture-desktop-application`. */
|
|
const captureOptions = z.object({
|
|
desktopEntry: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Desktop entry id (org.xfce.mousepad) or absolute .desktop path"),
|
|
executable: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe(
|
|
"Executable path or program name on PATH, instead of a desktop entry",
|
|
),
|
|
// An empty argument reaches the application as is. Path options refuse
|
|
// one, because normalizing it would select the current directory.
|
|
argument: z
|
|
.array(z.string())
|
|
.optional()
|
|
.describe("Argument appended to the launch command (repeatable)"),
|
|
output: localPathStringSchema.describe(
|
|
"New or empty directory for capture.json and retained content",
|
|
),
|
|
statePath: z
|
|
.array(localPathStringSchema)
|
|
.optional()
|
|
.describe(
|
|
"File or directory whose starting content is retained in addition to the application's state directories; a file brings its SQLite write-ahead log and rollback journal (repeatable)",
|
|
),
|
|
exclude: z
|
|
.array(localPathStringSchema)
|
|
.optional()
|
|
.describe("Path left out of every checkpoint walk (repeatable)"),
|
|
maxDurationSeconds: positive(
|
|
DESKTOP_CAPTURE_DEFAULTS.maxDurationSeconds,
|
|
).describe("Stop the session after this many seconds"),
|
|
maxEntries: positive(DESKTOP_CAPTURE_DEFAULTS.maxEntries).describe(
|
|
"Maximum paths recorded per checkpoint walk",
|
|
),
|
|
maxFileBytes: positive(DESKTOP_CAPTURE_DEFAULTS.maxFileBytes).describe(
|
|
"Largest file whose content is retained",
|
|
),
|
|
maxContentBytes: positive(DESKTOP_CAPTURE_DEFAULTS.maxContentBytes).describe(
|
|
"Total retained content",
|
|
),
|
|
packageFile: localPathStringSchema
|
|
.optional()
|
|
.describe(
|
|
"The .deb file the application was installed from; its control fields, SHA-256 and size are recorded, never the file",
|
|
),
|
|
});
|
|
|
|
/** The checkpoints a bundle's seed and a task's end state are read at. */
|
|
const checkpointOptions = {
|
|
seedCheckpoint: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Checkpoint whose state seeds the bundle; the baseline by default",
|
|
),
|
|
endCheckpoint: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Checkpoint whose state a task must pass on; the capture's last by default",
|
|
),
|
|
withholdFrom: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Checkpoint marked before a sign-in or licence activation; files under the owned paths that changed after it, up to --withhold-to, are withheld from the seed and the draft",
|
|
),
|
|
withholdTo: z
|
|
.string()
|
|
.optional()
|
|
.describe("Checkpoint marked after the sign-in or licence activation"),
|
|
withholdPath: z
|
|
.array(z.string())
|
|
.optional()
|
|
.describe(
|
|
"Home-relative file under the owned paths to withhold as sign-in or licence state, such as one written at first launch; repeat for more",
|
|
),
|
|
};
|
|
|
|
/** The withhold selection the checkpoint options name. */
|
|
const withholdOf = (options: {
|
|
readonly withholdFrom?: string | undefined;
|
|
readonly withholdTo?: string | undefined;
|
|
readonly withholdPath?: readonly string[] | undefined;
|
|
}) => ({
|
|
from: options.withholdFrom,
|
|
to: options.withholdTo,
|
|
paths: options.withholdPath ?? [],
|
|
});
|
|
|
|
/**
|
|
* Run a desktop operation with SQLite and dconf readers, finishing through
|
|
* the shared command owner so the result, cancellation and the SQLite
|
|
* service's cleanup are all kept. The readers load here, so registering the
|
|
* command catalog never loads them.
|
|
*/
|
|
const withStateReaders = async (
|
|
environment: NodeJS.ProcessEnv,
|
|
signal: AbortSignal,
|
|
operation: string,
|
|
run: (readers: DesktopStateReaders) => Promise<Evidence | AnalysisError>,
|
|
) => {
|
|
const [
|
|
{ createSqliteDatabaseService },
|
|
{ readSqliteStateView },
|
|
{ dumpDconfDatabase },
|
|
] = await Promise.all([
|
|
import("../composition/sqlite.js"),
|
|
import("../application/desktop/DesktopSqliteState.js"),
|
|
import("../desktop/DconfDatabaseReader.js"),
|
|
]);
|
|
const sqlite = createSqliteDatabaseService(environment);
|
|
return finishAnalysisCommand(
|
|
async () => {
|
|
const result = await run({
|
|
sqlite: (database, readSignal) =>
|
|
readSqliteStateView(sqlite, database, readSignal),
|
|
dconf: (database, readSignal) =>
|
|
dumpDconfDatabase(database, environment, readSignal),
|
|
});
|
|
return result instanceof AnalysisError ? err(result) : ok(result);
|
|
},
|
|
{
|
|
close: async () => {
|
|
const failures: unknown[] = [];
|
|
try {
|
|
await sqlite.close();
|
|
} catch (cause: unknown) {
|
|
failures.push(cause);
|
|
}
|
|
if (operation === "export_desktop_application") {
|
|
try {
|
|
const { OwnedWorkerRuntime } =
|
|
await import("../process/OwnedWorkerRuntime.js");
|
|
await OwnedWorkerRuntime.retryPending();
|
|
} catch (cause: unknown) {
|
|
failures.push(cause);
|
|
}
|
|
}
|
|
if (failures.length === 1) throw failures[0];
|
|
if (failures.length > 1) {
|
|
const typed = failures.map((cause) => {
|
|
if (cause instanceof AnalysisError) return cause;
|
|
const observation =
|
|
cause instanceof ArtifactReaderFailure
|
|
? ArtifactReaderFailure.cleanupObservation(cause, [])
|
|
: {
|
|
resources: [],
|
|
reason:
|
|
cause instanceof Error ? cause.message : String(cause),
|
|
};
|
|
const failure = new ProviderCleanupError(
|
|
"rea-desktop",
|
|
observation.resources,
|
|
{ reason: observation.reason },
|
|
{ operation, cause },
|
|
);
|
|
bindArtifactFileCleanup(cause, failure);
|
|
return failure;
|
|
});
|
|
const combined = analysisErrorWithCleanupFailure(
|
|
typed[0]!,
|
|
typed[1]!,
|
|
operation,
|
|
);
|
|
combined.bindCleanupDependencies(typed);
|
|
throw combined;
|
|
}
|
|
},
|
|
},
|
|
"rea-desktop",
|
|
operation,
|
|
signal,
|
|
).then(projectCliAnalysisResult);
|
|
};
|
|
|
|
/** The export's options for withheld state beyond the window: seeding it with consent, the consent file and the copy. */
|
|
const withheldExportOptions = {
|
|
seedWithheld: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe(
|
|
"Seed the withheld files after all, which needs a seed_sign_in consent in --consent",
|
|
),
|
|
consent: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"consent.json holding the person's consents, which the bundle ships",
|
|
),
|
|
withheldCopy: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"New or empty folder outside the bundle the withheld files are copied into, for whoever supplies them at run time",
|
|
),
|
|
};
|
|
|
|
/** The export request's withheld fields from the CLI options, paths made absolute. */
|
|
const withheldRequestOf = (
|
|
options: Parameters<typeof withholdOf>[0] & {
|
|
readonly seedWithheld: boolean;
|
|
readonly consent?: string | undefined;
|
|
readonly withheldCopy?: string | undefined;
|
|
},
|
|
) => ({
|
|
withhold: withholdOf(options),
|
|
seedWithheld: options.seedWithheld,
|
|
consent:
|
|
options.consent === undefined
|
|
? undefined
|
|
: absoluteCallerPath(options.consent),
|
|
withheldCopy:
|
|
options.withheldCopy === undefined
|
|
? undefined
|
|
: absoluteCallerPath(options.withheldCopy),
|
|
});
|
|
|
|
/** Register the export of a capture folder as a computer app bundle. */
|
|
const registerDesktopExportCommand = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
environment: NodeJS.ProcessEnv,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.exportDesktopApplication, {
|
|
description:
|
|
"Export a desktop application capture folder as an agentenv computer app bundle: class, env.toml, skill and seed",
|
|
args: z.object({
|
|
capture: localPathStringSchema.describe(
|
|
"Folder written by capture-desktop-application",
|
|
),
|
|
directory: localPathStringSchema.describe(
|
|
"New or empty bundle directory",
|
|
),
|
|
}),
|
|
options: z.object({
|
|
name: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Environment name; defaults to one derived from the application",
|
|
),
|
|
task: z
|
|
.array(localPathStringSchema)
|
|
.optional()
|
|
.describe(
|
|
"Task file to validate against the capture and add with its verifier and eval; a capture yields one",
|
|
),
|
|
packageUrl: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"http or https URL the computer downloads the captured --package-file from when the bundle is put, for an application installed from a .deb file",
|
|
),
|
|
downloadInstall: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"JSON file naming the AppImage or tarball a build computer unpacked the application from: kind, desktop_entry, url, sha256, architecture, and a tarball's program and name",
|
|
),
|
|
extraPackage: z
|
|
.array(z.string())
|
|
.optional()
|
|
.describe(
|
|
"Ubuntu 24.04 archive package installed beside the application, such as a runtime piece its package does not install; repeat for more",
|
|
),
|
|
syntheticSeed: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe(
|
|
"The seed was made for the task, as in a build computer, not taken from the user's own data; the README says so instead of keeping the bundle local",
|
|
),
|
|
screens: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Folder holding checkpoint screenshots for screen-judge examples.",
|
|
),
|
|
...withheldExportOptions,
|
|
...checkpointOptions,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.exportDesktopApplication,
|
|
async () => {
|
|
const { exportDesktopApplication } =
|
|
await import("../application/desktop/DesktopApplicationExport.js");
|
|
return withStateReaders(
|
|
environment,
|
|
signal,
|
|
"export_desktop_application",
|
|
(readers) =>
|
|
exportDesktopApplication(
|
|
{
|
|
capture: absoluteCallerPath(args.capture),
|
|
directory: absoluteCallerPath(args.directory),
|
|
name: options.name,
|
|
tasks: (options.task ?? []).map((path) =>
|
|
absoluteCallerPath(path),
|
|
),
|
|
packageUrl: options.packageUrl,
|
|
downloadInstall:
|
|
options.downloadInstall === undefined
|
|
? undefined
|
|
: absoluteCallerPath(options.downloadInstall),
|
|
extraPackages: options.extraPackage,
|
|
seedCheckpoint: options.seedCheckpoint,
|
|
endCheckpoint: options.endCheckpoint,
|
|
syntheticSeed: options.syntheticSeed,
|
|
screens:
|
|
options.screens === undefined
|
|
? undefined
|
|
: absoluteCallerPath(options.screens),
|
|
...withheldRequestOf(options),
|
|
},
|
|
readers,
|
|
signal,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
/** Register the change summary an agent writes a task from. */
|
|
const registerDesktopTaskCommand = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
environment: NodeJS.ProcessEnv,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.describeDesktopApplicationTask, {
|
|
description:
|
|
"Describe how a desktop application capture's files changed from the seed checkpoint to the end, with suggested checks and a task draft to complete for export-desktop-application --task",
|
|
args: z.object({
|
|
capture: localPathStringSchema.describe(
|
|
"Folder written by capture-desktop-application",
|
|
),
|
|
}),
|
|
options: z.object(checkpointOptions),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.describeDesktopApplicationTask,
|
|
async () => {
|
|
const { draftDesktopApplicationTask } =
|
|
await import("../application/desktop/DesktopTaskDraft.js");
|
|
return withStateReaders(
|
|
environment,
|
|
signal,
|
|
"describe_desktop_application_task",
|
|
(readers) =>
|
|
draftDesktopApplicationTask(
|
|
absoluteCallerPath(args.capture),
|
|
readers,
|
|
signal,
|
|
{
|
|
seed: options.seedCheckpoint,
|
|
end: options.endCheckpoint,
|
|
withhold: withholdOf(options),
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
/** Register the state view over a capture folder. */
|
|
const registerDesktopStateCommand = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
environment: NodeJS.ProcessEnv,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.inspectDesktopApplicationState, {
|
|
description:
|
|
"Read a desktop application capture folder into the application's state: roles, typed JSON views of each version and kinds of change",
|
|
args: z.object({
|
|
capture: localPathStringSchema.describe(
|
|
"Folder written by capture-desktop-application",
|
|
),
|
|
}),
|
|
run: ({ args }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.inspectDesktopApplicationState,
|
|
async () => {
|
|
const { inspectDesktopApplicationState } =
|
|
await import("../application/desktop/DesktopApplicationState.js");
|
|
return withStateReaders(
|
|
environment,
|
|
signal,
|
|
"inspect_desktop_application_state",
|
|
(readers) =>
|
|
inspectDesktopApplicationState(
|
|
absoluteCallerPath(args.capture),
|
|
readers,
|
|
signal,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
/** Register the interactive Linux desktop application capture. */
|
|
export const registerDesktopCaptureCommands = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
environment: NodeJS.ProcessEnv,
|
|
): void => {
|
|
registerDesktopStateCommand(cli, logger, environment);
|
|
registerDesktopExportCommand(cli, logger, environment);
|
|
registerDesktopTaskCommand(cli, logger, environment);
|
|
cli.command(CLI_COMMANDS.captureDesktopApplication, {
|
|
description:
|
|
"Launch a Linux desktop application, watch it while it is used and record its windows and state at checkpoints read from stdin",
|
|
options: captureOptions,
|
|
run: ({ options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.captureDesktopApplication,
|
|
async () => {
|
|
const requests = new SessionRequestQueue(
|
|
createInterface({ input: process.stdin, terminal: false }),
|
|
);
|
|
try {
|
|
return await captureDesktopApplication(
|
|
{
|
|
desktopEntry: options.desktopEntry,
|
|
executable: options.executable,
|
|
arguments: options.argument ?? [],
|
|
output: options.output,
|
|
statePaths: options.statePath ?? [],
|
|
excludes: options.exclude ?? [],
|
|
maxDurationSeconds: options.maxDurationSeconds,
|
|
maxEntries: options.maxEntries,
|
|
maxFileBytes: options.maxFileBytes,
|
|
maxContentBytes: options.maxContentBytes,
|
|
packageFile:
|
|
options.packageFile === undefined
|
|
? undefined
|
|
: absoluteCallerPath(options.packageFile),
|
|
},
|
|
{
|
|
platform: process.platform,
|
|
environment,
|
|
callerDirectory: process.cwd(),
|
|
nextRequest: () => requests.next(),
|
|
report: async (message, event) => {
|
|
const progress: DesktopCaptureProgress = {
|
|
rea_progress: {
|
|
phase: "desktop_capture",
|
|
message,
|
|
...(event === undefined ? {} : { event }),
|
|
},
|
|
};
|
|
process.stderr.write(`${JSON.stringify(progress)}\n`);
|
|
},
|
|
},
|
|
signal,
|
|
);
|
|
} finally {
|
|
requests.close();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
});
|
|
};
|