@codyswann/lisa 2.253.0 → 2.254.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.
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/rules/eager/automation-runbook-contract.md +4 -1
- package/plugins/lisa/rules/reference/automation-runbook-contract.md +16 -7
- package/plugins/lisa/scripts/automation-run-record.mjs +262 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/automation-run-record.mjs +262 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +4 -1
- package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +16 -7
- package/plugins/lisa-copilot/scripts/automation-run-record.mjs +262 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +16 -7
- package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +4 -1
- package/plugins/lisa-cursor/scripts/automation-run-record.mjs +262 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/rules/eager/automation-runbook-contract.md +4 -1
- package/plugins/src/base/rules/reference/automation-runbook-contract.md +16 -7
- package/plugins/src/base/scripts/automation-run-record.mjs +262 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.254.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.254.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.254.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.254.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.254.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -50,7 +50,10 @@ loop itself is broken, not that a work item was blocked.
|
|
|
50
50
|
|
|
51
51
|
Every run — including a trivial early termination — ends by naming exactly one run outcome plus a
|
|
52
52
|
one-line operator summary of what happened, recorded where the status surface can read it —
|
|
53
|
-
there is no silent exit.
|
|
53
|
+
there is no silent exit. The shared local substrate is
|
|
54
|
+
`plugins/src/base/scripts/automation-run-record.mjs`, which writes bounded JSONL records under
|
|
55
|
+
`.lisa/automations/runs/<loop-id>.jsonl`. Silence and health must never look identical to an
|
|
56
|
+
operator.
|
|
54
57
|
|
|
55
58
|
## Never block, always degrade
|
|
56
59
|
|
|
@@ -121,8 +121,17 @@ there is no silent exit for an operator to misread as health. A run that ends wi
|
|
|
121
121
|
recorded outcome is indistinguishable from a crashed scheduler, which is exactly the ambiguity this
|
|
122
122
|
contract removes. This generalizes `lisa-improve-harness`'s result-record discipline — every
|
|
123
123
|
terminal step posts its result record before stopping, so there is no silent exit — from one flow to
|
|
124
|
-
every registered loop.
|
|
125
|
-
|
|
124
|
+
every registered loop.
|
|
125
|
+
|
|
126
|
+
The local storage substrate is `plugins/src/base/scripts/automation-run-record.mjs`. It writes one
|
|
127
|
+
bounded JSONL file per loop at `.lisa/automations/runs/<loop-id>.jsonl`, with one object per line:
|
|
128
|
+
`{ ts, loop_id, outcome, summary, runbook, refs[], run_id }`. The helper rejects any outcome outside
|
|
129
|
+
the closed six-value vocabulary, requires a non-empty operator-readable summary, suppresses duplicate
|
|
130
|
+
re-appends for the same `run_id`, skips corrupt/truncated lines on read, and trims to the newest
|
|
131
|
+
configured records on write. The default bound is 50 records per loop, overridable via
|
|
132
|
+
`.lisa.config.json` / `.lisa.config.local.json` `automations.runHistory.maxEntries`. These records
|
|
133
|
+
are local scheduler observations, not project knowledge, so `.lisa/automations/runs/` is ignored;
|
|
134
|
+
the runbooks under `.lisa/automations/*.runbook.md` remain checked-in knowledge.
|
|
126
135
|
|
|
127
136
|
## A run outcome is not a work-item lifecycle terminal state
|
|
128
137
|
|
|
@@ -209,8 +218,8 @@ packet above. A degraded run never blocks other work and never leaves the run un
|
|
|
209
218
|
|
|
210
219
|
## Citing adjacent work
|
|
211
220
|
|
|
212
|
-
Cite these by name
|
|
213
|
-
|
|
214
|
-
that
|
|
215
|
-
|
|
216
|
-
|
|
221
|
+
Cite these by name. The run-record substrate ships with that ticket (#1797), do not assume its file
|
|
222
|
+
is present in older branches. The evidence packet ships with that ticket, do not assume its file is present in this branch. Other adjacent artifacts keep their own lifecycle: the runbook scaffolding
|
|
223
|
+
that instantiates this template (#1796), and the evidence packet defined by the evidence PRD (#1738)
|
|
224
|
+
— named here, with no evidence format defined by this contract. Rejection memory extends
|
|
225
|
+
`rejection-detection`; this contract neither restates nor overrides it.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Dependency-free run outcome recorder for registered Lisa automation loops.
|
|
4
|
+
*
|
|
5
|
+
* The file is local scheduler state, not project knowledge: one bounded JSONL
|
|
6
|
+
* file per loop under `.lisa/automations/runs/`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
export const AUTOMATION_RUN_OUTCOMES = [
|
|
13
|
+
"nothing-needed",
|
|
14
|
+
"candidate-proposed",
|
|
15
|
+
"change-proved",
|
|
16
|
+
"approval-requested",
|
|
17
|
+
"recovery-required",
|
|
18
|
+
"policy-obsolete",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_AUTOMATION_RUN_HISTORY_MAX_ENTRIES = 50;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {{
|
|
25
|
+
* readonly ts: string
|
|
26
|
+
* readonly loop_id: string
|
|
27
|
+
* readonly outcome: string
|
|
28
|
+
* readonly summary: string
|
|
29
|
+
* readonly runbook: string
|
|
30
|
+
* readonly refs: readonly string[]
|
|
31
|
+
* readonly run_id: string
|
|
32
|
+
* }} AutomationRunRecord
|
|
33
|
+
*
|
|
34
|
+
* @typedef {{
|
|
35
|
+
* readonly projectRoot?: string
|
|
36
|
+
* readonly loopId: string
|
|
37
|
+
* readonly outcome: string
|
|
38
|
+
* readonly summary: string
|
|
39
|
+
* readonly runbook: string
|
|
40
|
+
* readonly refs?: readonly string[]
|
|
41
|
+
* readonly runId?: string
|
|
42
|
+
* readonly ts?: string | Date
|
|
43
|
+
* readonly maxEntries?: number
|
|
44
|
+
* }} RecordAutomationRunInput
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Record one automation-loop outcome, suppressing duplicate re-appends for the
|
|
49
|
+
* same `run_id` and trimming the file to the configured history bound.
|
|
50
|
+
*
|
|
51
|
+
* @param {RecordAutomationRunInput} input
|
|
52
|
+
* @returns {Promise<{ readonly path: string, readonly record: AutomationRunRecord, readonly records: readonly AutomationRunRecord[], readonly appended: boolean, readonly skippedCorruptLines: number, readonly maxEntries: number }>}
|
|
53
|
+
*/
|
|
54
|
+
export async function recordAutomationRun(input) {
|
|
55
|
+
const projectRoot = path.resolve(input.projectRoot ?? process.cwd());
|
|
56
|
+
const maxEntries =
|
|
57
|
+
input.maxEntries ??
|
|
58
|
+
(await resolveAutomationRunHistoryMaxEntries(projectRoot));
|
|
59
|
+
const record = buildAutomationRunRecord(input);
|
|
60
|
+
const filePath = automationRunRecordPath(projectRoot, record.loop_id);
|
|
61
|
+
const readResult = await readAutomationRunRecords(filePath);
|
|
62
|
+
|
|
63
|
+
if (readResult.records.some(existing => existing.run_id === record.run_id)) {
|
|
64
|
+
return {
|
|
65
|
+
path: filePath,
|
|
66
|
+
record,
|
|
67
|
+
records: readResult.records,
|
|
68
|
+
appended: false,
|
|
69
|
+
skippedCorruptLines: readResult.skippedCorruptLines,
|
|
70
|
+
maxEntries,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const nextRecords = [...readResult.records, record].slice(-maxEntries);
|
|
75
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
76
|
+
await writeJsonlAtomically(filePath, nextRecords);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
path: filePath,
|
|
80
|
+
record,
|
|
81
|
+
records: nextRecords,
|
|
82
|
+
appended: true,
|
|
83
|
+
skippedCorruptLines: readResult.skippedCorruptLines,
|
|
84
|
+
maxEntries,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} projectRoot
|
|
90
|
+
* @returns {Promise<number>}
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveAutomationRunHistoryMaxEntries(projectRoot) {
|
|
93
|
+
const globalConfig = await readJsonIfPresent(
|
|
94
|
+
path.join(projectRoot, ".lisa.config.json")
|
|
95
|
+
);
|
|
96
|
+
const localConfig = await readJsonIfPresent(
|
|
97
|
+
path.join(projectRoot, ".lisa.config.local.json")
|
|
98
|
+
);
|
|
99
|
+
const configured =
|
|
100
|
+
localConfig?.automations?.runHistory?.maxEntries ??
|
|
101
|
+
globalConfig?.automations?.runHistory?.maxEntries;
|
|
102
|
+
|
|
103
|
+
if (Number.isInteger(configured) && configured > 0) {
|
|
104
|
+
return configured;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return DEFAULT_AUTOMATION_RUN_HISTORY_MAX_ENTRIES;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} projectRoot
|
|
112
|
+
* @param {string} loopId
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function automationRunRecordPath(projectRoot, loopId) {
|
|
116
|
+
return path.join(
|
|
117
|
+
path.resolve(projectRoot),
|
|
118
|
+
".lisa",
|
|
119
|
+
"automations",
|
|
120
|
+
"runs",
|
|
121
|
+
`${normalizeLoopId(loopId)}.jsonl`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} filePath
|
|
127
|
+
* @returns {Promise<{ readonly records: readonly AutomationRunRecord[], readonly skippedCorruptLines: number }>}
|
|
128
|
+
*/
|
|
129
|
+
export async function readAutomationRunRecords(filePath) {
|
|
130
|
+
let content = "";
|
|
131
|
+
try {
|
|
132
|
+
content = await readFile(filePath, "utf8");
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error?.code === "ENOENT") {
|
|
135
|
+
return { records: [], skippedCorruptLines: 0 };
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const records = [];
|
|
141
|
+
let skippedCorruptLines = 0;
|
|
142
|
+
for (const line of content.split(/\n/)) {
|
|
143
|
+
if (!line.trim()) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(line);
|
|
148
|
+
records.push(validateStoredRecord(parsed));
|
|
149
|
+
} catch {
|
|
150
|
+
skippedCorruptLines += 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { records, skippedCorruptLines };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {RecordAutomationRunInput} input
|
|
159
|
+
* @returns {AutomationRunRecord}
|
|
160
|
+
*/
|
|
161
|
+
function buildAutomationRunRecord(input) {
|
|
162
|
+
const loopId = normalizeLoopId(input.loopId);
|
|
163
|
+
const summary = String(input.summary ?? "").trim();
|
|
164
|
+
const runbook = String(input.runbook ?? "").trim();
|
|
165
|
+
const refs = Array.isArray(input.refs)
|
|
166
|
+
? input.refs.map(ref => String(ref))
|
|
167
|
+
: [];
|
|
168
|
+
const ts =
|
|
169
|
+
input.ts instanceof Date
|
|
170
|
+
? input.ts.toISOString()
|
|
171
|
+
: input.ts
|
|
172
|
+
? new Date(input.ts).toISOString()
|
|
173
|
+
: new Date().toISOString();
|
|
174
|
+
const runId = String(input.runId ?? `${loopId}:${ts}`).trim();
|
|
175
|
+
|
|
176
|
+
if (!AUTOMATION_RUN_OUTCOMES.includes(input.outcome)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Invalid automation run outcome "${input.outcome}". Valid outcomes: ${AUTOMATION_RUN_OUTCOMES.join(", ")}.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (!summary) {
|
|
182
|
+
throw new Error("Automation run summary is required.");
|
|
183
|
+
}
|
|
184
|
+
if (!runbook) {
|
|
185
|
+
throw new Error("Automation runbook path is required.");
|
|
186
|
+
}
|
|
187
|
+
if (!runId) {
|
|
188
|
+
throw new Error("Automation run_id is required.");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ts,
|
|
193
|
+
loop_id: loopId,
|
|
194
|
+
outcome: input.outcome,
|
|
195
|
+
summary,
|
|
196
|
+
runbook,
|
|
197
|
+
refs,
|
|
198
|
+
run_id: runId,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {unknown} value
|
|
204
|
+
* @returns {AutomationRunRecord}
|
|
205
|
+
*/
|
|
206
|
+
function validateStoredRecord(value) {
|
|
207
|
+
if (!value || typeof value !== "object") {
|
|
208
|
+
throw new Error("Automation run record must be an object.");
|
|
209
|
+
}
|
|
210
|
+
return buildAutomationRunRecord({
|
|
211
|
+
ts: String(value.ts ?? ""),
|
|
212
|
+
loopId: String(value.loop_id ?? ""),
|
|
213
|
+
outcome: String(value.outcome ?? ""),
|
|
214
|
+
summary: String(value.summary ?? ""),
|
|
215
|
+
runbook: String(value.runbook ?? ""),
|
|
216
|
+
refs: Array.isArray(value.refs) ? value.refs.map(ref => String(ref)) : [],
|
|
217
|
+
runId: String(value.run_id ?? ""),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {string} loopId
|
|
223
|
+
* @returns {string}
|
|
224
|
+
*/
|
|
225
|
+
function normalizeLoopId(loopId) {
|
|
226
|
+
const normalized = String(loopId ?? "").trim();
|
|
227
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(normalized)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
"Automation loop_id must be a non-empty slug containing only letters, numbers, dots, underscores, and hyphens."
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return normalized;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {string} filePath
|
|
237
|
+
* @returns {Promise<unknown | undefined>}
|
|
238
|
+
*/
|
|
239
|
+
async function readJsonIfPresent(filePath) {
|
|
240
|
+
try {
|
|
241
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error?.code === "ENOENT") {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
if (error instanceof SyntaxError) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {string} filePath
|
|
255
|
+
* @param {readonly AutomationRunRecord[]} records
|
|
256
|
+
*/
|
|
257
|
+
async function writeJsonlAtomically(filePath, records) {
|
|
258
|
+
const content = `${records.map(record => JSON.stringify(record)).join("\n")}\n`;
|
|
259
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
260
|
+
await writeFile(tempPath, content, "utf8");
|
|
261
|
+
await rename(tempPath, filePath);
|
|
262
|
+
}
|