File size: 3,421 Bytes
eb3f11e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion";
// Cron scratch CLI: private per-job prompt context reads and compare-and-swap writes.
import type { Command } from "commander";
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
import { CronCliError } from "./cron-cli-error.js";
import { createCronOutputCommand } from "./output-mode.js";
import { handleCronCliError, printCronJson, requireCronJobId } from "./shared.js";
import { readCronScratchContent } from "./trigger-options.js";

type ScratchRecord = { content: string; revision: number; updatedAtMs: number };
type ScratchGetResult = {
  scratch: ScratchRecord | null;
  currentRevision: number;
  maxBytes: number;
};
type ScratchSetResult =
  | { ok: true; scratch: ScratchRecord | null; currentRevision: number; maxBytes: number }
  | { ok: false; reason: "revision-conflict"; currentRevision: number };

function parseExpectedRevision(value: string | undefined): number | undefined {
  if (value === undefined) {
    return undefined;
  }
  const revision = parseStrictNonNegativeInteger(value);
  if (revision === undefined) {
    throw new CronCliError("--expected-revision must be a non-negative integer");
  }
  return revision;
}

export function registerCronScratchCommand(cron: Command) {
  addGatewayClientOptions(
    createCronOutputCommand(cron, "scratch")
      .description("Read or replace an automation's private scratch")
      .argument("<id>", "Job id")
      .option("--set <text>", "Replace scratch with exact text")
      .option("--file <path>", "Replace scratch from a file, or - for stdin")
      .option("--unset", "Remove the scratch row", false)
      .option("--expected-revision <n>", "Require the current scratch revision")
      .action(async (idArg, opts) => {
        try {
          const id = requireCronJobId(idArg);
          const mutations = [
            opts.set !== undefined,
            opts.file !== undefined,
            opts.unset === true,
          ].filter(Boolean).length;
          if (mutations > 1) {
            throw new CronCliError("choose only one of --set, --file, or --unset");
          }
          const current = (await callGatewayFromCli("cron.scratch.get", opts, {
            id,
          })) as ScratchGetResult;
          if (mutations === 0) {
            if (opts.json) {
              printCronJson(current);
            } else if (current.scratch) {
              process.stdout.write(current.scratch.content);
            }
            return;
          }

          const explicitRevision = parseExpectedRevision(opts.expectedRevision);
          const expectedRevision = explicitRevision ?? current.currentRevision;
          const content = opts.unset
            ? null
            : opts.file !== undefined
              ? await readCronScratchContent(String(opts.file))
              : String(opts.set ?? "");
          const result = (await callGatewayFromCli("cron.scratch.set", opts, {
            id,
            content,
            expectedRevision,
          })) as ScratchSetResult;
          if (!result.ok) {
            throw new CronCliError(
              `cron scratch changed concurrently (current revision ${result.currentRevision})`,
            );
          }
          printCronJson(result);
        } catch (error) {
          handleCronCliError(error);
        }
      }),
  );
}