impel-cli 0.17.3 → 0.17.4
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/src/apps.js +84 -78
- package/src/windowsApps.js +39 -3
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -42,33 +42,33 @@ const APP_DEFINITIONS = {
|
|
|
42
42
|
// with its compatibility patches. Updating a pin requires a new CLI release.
|
|
43
43
|
export const PINNED_VENDOR_APPS = Object.freeze({
|
|
44
44
|
claude: Object.freeze({
|
|
45
|
-
version: "1.
|
|
46
|
-
claudeCodeVersion: "2.1.
|
|
47
|
-
claudeCodeCommit: "
|
|
45
|
+
version: "1.22209.3",
|
|
46
|
+
claudeCodeVersion: "2.1.215",
|
|
47
|
+
claudeCodeCommit: "316ce99628e89900bf0b1328fed3b8fec0c0c92d",
|
|
48
48
|
bundleName: "Claude.app",
|
|
49
49
|
downloads: Object.freeze({
|
|
50
50
|
arm64: Object.freeze({
|
|
51
|
-
url: "https://downloads.claude.ai/releases/darwin/universal/1.
|
|
52
|
-
sha256: "
|
|
51
|
+
url: "https://downloads.claude.ai/releases/darwin/universal/1.22209.3/Claude-babe11577dfefe3e209c06bd674628d862f0dbae.zip",
|
|
52
|
+
sha256: "b110e0fdb7b2601d80dfdd7893f7811de382de84c4586113e7873e28f17680fc",
|
|
53
53
|
}),
|
|
54
54
|
x64: Object.freeze({
|
|
55
|
-
url: "https://downloads.claude.ai/releases/darwin/universal/1.
|
|
56
|
-
sha256: "
|
|
55
|
+
url: "https://downloads.claude.ai/releases/darwin/universal/1.22209.3/Claude-babe11577dfefe3e209c06bd674628d862f0dbae.zip",
|
|
56
|
+
sha256: "b110e0fdb7b2601d80dfdd7893f7811de382de84c4586113e7873e28f17680fc",
|
|
57
57
|
}),
|
|
58
58
|
}),
|
|
59
59
|
}),
|
|
60
60
|
chatgpt: Object.freeze({
|
|
61
|
-
version: "26.
|
|
62
|
-
codexVersion: "0.
|
|
61
|
+
version: "26.715.31925",
|
|
62
|
+
codexVersion: "0.145.0-alpha.18",
|
|
63
63
|
bundleName: "ChatGPT.app",
|
|
64
64
|
downloads: Object.freeze({
|
|
65
65
|
arm64: Object.freeze({
|
|
66
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.
|
|
67
|
-
sha256: "
|
|
66
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-arm64-26.715.31925.zip",
|
|
67
|
+
sha256: "a0cded116975c71c723dc8601c07402533b5ff23881d15e52cfb6dc71413e45b",
|
|
68
68
|
}),
|
|
69
69
|
x64: Object.freeze({
|
|
70
|
-
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.
|
|
71
|
-
sha256: "
|
|
70
|
+
url: "https://persistent.oaistatic.com/codex-app-prod/ChatGPT-darwin-x64-26.715.31925.zip",
|
|
71
|
+
sha256: "f18972b893207ff5cbf047599369ce96768c2ace25128a09315058ef0a5cfdcb",
|
|
72
72
|
}),
|
|
73
73
|
}),
|
|
74
74
|
}),
|
|
@@ -85,12 +85,10 @@ const ULTRA_REASONING_LEVEL = { effort: "ultra", description: "Maximum reasoning
|
|
|
85
85
|
const FAST_SERVICE_TIER = { id: "priority", name: "Fast", description: "1.5x speed, increased usage" };
|
|
86
86
|
const FAST_MODE_AUTH_GATE = /([A-Za-z_$][\w$]*)=([A-Za-z_$][\w$]*)&&!([A-Za-z_$][\w$]*)&&([A-Za-z_$][\w$]*)!=null&&\4\?\.requirements\?\.featureRequirements\?\.fast_mode!==!1/gu;
|
|
87
87
|
const CHATGPT_RESOURCE_ROOT = "linked_vendor_resources_with_patched_asar";
|
|
88
|
-
const CLAUDE_PLAN_USAGE_GUARD = "if(!
|
|
89
|
-
const IMPEL_CLAUDE_PLAN_USAGE_GUARD =
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const CLAUDE_DRAFT_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===Be&&s';
|
|
93
|
-
const IMPEL_CLAUDE_BOUND_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===Be?s:Le?.agent??!1';
|
|
88
|
+
const CLAUDE_PLAN_USAGE_GUARD = "if(!Vve()){f8();return}if(!et().hasOrgPolicyBackend())return;if((r=yr())";
|
|
89
|
+
const IMPEL_CLAUDE_PLAN_USAGE_GUARD = `if(!Vve()){f8();return}${"".padEnd("if(!et().hasOrgPolicyBackend())return;".length, " ")}if((r=yr())`;
|
|
90
|
+
const CLAUDE_DRAFT_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===He&&s';
|
|
91
|
+
const IMPEL_CLAUDE_BOUND_AGENT_MENTIONS = 'agentMentionsEnabled:"draft"===He?s:Le?.agent??!1';
|
|
94
92
|
const CLAUDE_AGENT_MENTION_FILTER = 'return e.filter(e=>"user"===e.source&&(!s||e.name.toLowerCase().includes(s))).slice(0,10)';
|
|
95
93
|
const IMPEL_CLAUDE_BOUND_AGENT_MENTION_FILTER = 'return e.filter(e=>"user"===e.source&&(!s||e.name.toLowerCase().includes(s))&&("string"!=typeof y||e.name===y)).slice(0,10)';
|
|
96
94
|
const CLAUDE_AGENT_MENTION_SELECT = 'onSelect:t=>(e(String(t)),Promise.resolve(null))';
|
|
@@ -176,7 +174,7 @@ export const CURRENT_CONFIG_VERSION = 16;
|
|
|
176
174
|
// — which is what made every `impel update` re-trigger macOS permission
|
|
177
175
|
// prompts. Bump this ONLY when a code change alters the bytes of a built
|
|
178
176
|
// bundle; leave it alone for changes that don't touch bundle contents.
|
|
179
|
-
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-
|
|
177
|
+
export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-07-20.1";
|
|
180
178
|
|
|
181
179
|
/** Parse the tenant's install manifest, or null when absent/corrupt. */
|
|
182
180
|
export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
|
|
@@ -282,12 +280,15 @@ function wrapperLaunchesOnTahoe(status) {
|
|
|
282
280
|
|
|
283
281
|
function managedClaudeRendererMatchesCompatibility(status, compatibility) {
|
|
284
282
|
try {
|
|
285
|
-
const
|
|
286
|
-
if (!
|
|
283
|
+
const renderers = compatibility.rendererAssets?.agentMentions;
|
|
284
|
+
if (!Array.isArray(renderers) || renderers.length === 0) return false;
|
|
287
285
|
const root = path.resolve(status.launcher, "Contents", "Resources", "ion-dist");
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
286
|
+
return renderers.every((renderer) => {
|
|
287
|
+
if (!renderer?.path || !renderer.sha256 || path.isAbsolute(renderer.path)) return false;
|
|
288
|
+
const target = path.resolve(root, renderer.path);
|
|
289
|
+
if (!target.startsWith(`${root}${path.sep}`)) return false;
|
|
290
|
+
return sha256(fs.readFileSync(target)) === renderer.sha256;
|
|
291
|
+
});
|
|
291
292
|
} catch {
|
|
292
293
|
return false;
|
|
293
294
|
}
|
|
@@ -1406,7 +1407,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1406
1407
|
const vendorExecutable = path.join(macos, executableName);
|
|
1407
1408
|
const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
|
|
1408
1409
|
writeAtomic(path.join(staging, "Contents", "Resources", "impel-compatibility.json"), JSON.stringify({
|
|
1409
|
-
schemaVersion:
|
|
1410
|
+
schemaVersion: 5,
|
|
1410
1411
|
cliVersion: CLI_VERSION,
|
|
1411
1412
|
bundleFingerprint: BUNDLE_BUILD_FINGERPRINT,
|
|
1412
1413
|
signingMode: signingModeForIdentity(signingIdentity),
|
|
@@ -1427,10 +1428,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1427
1428
|
followupBoundAgentChip: agentMentionPatches.chip,
|
|
1428
1429
|
},
|
|
1429
1430
|
rendererAssets: {
|
|
1430
|
-
agentMentions:
|
|
1431
|
-
path: agentMentionPatches.path,
|
|
1432
|
-
sha256: agentMentionPatches.sha256,
|
|
1433
|
-
},
|
|
1431
|
+
agentMentions: agentMentionPatches.assets,
|
|
1434
1432
|
},
|
|
1435
1433
|
asarHeaderSha256: newAsarHash,
|
|
1436
1434
|
}, null, 2) + "\n", 0o644);
|
|
@@ -1576,42 +1574,63 @@ function patchClaudeFollowupAgentMentions(bundle) {
|
|
|
1576
1574
|
|
|
1577
1575
|
const ionDist = path.join(bundle, "Contents", "Resources", "ion-dist");
|
|
1578
1576
|
const contracts = [
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1577
|
+
{
|
|
1578
|
+
key: "enable",
|
|
1579
|
+
original: CLAUDE_DRAFT_AGENT_MENTIONS,
|
|
1580
|
+
replacement: IMPEL_CLAUDE_BOUND_AGENT_MENTIONS,
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
key: "filter",
|
|
1584
|
+
original: CLAUDE_AGENT_MENTION_FILTER,
|
|
1585
|
+
replacement: IMPEL_CLAUDE_BOUND_AGENT_MENTION_FILTER,
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
key: "chip",
|
|
1589
|
+
original: CLAUDE_AGENT_MENTION_SELECT,
|
|
1590
|
+
replacement: IMPEL_CLAUDE_BOUND_AGENT_MENTION_SELECT,
|
|
1591
|
+
},
|
|
1582
1592
|
];
|
|
1583
|
-
const
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1593
|
+
const sources = new Map(listJavaScriptFiles(ionDist).map((candidate) => [
|
|
1594
|
+
candidate,
|
|
1595
|
+
fs.readFileSync(candidate, "utf8"),
|
|
1596
|
+
]));
|
|
1597
|
+
const matches = new Map(contracts.map(({ key }) => [key, []]));
|
|
1598
|
+
for (const [candidate, source] of sources) {
|
|
1599
|
+
for (const contract of contracts) {
|
|
1600
|
+
const count = exactTextMatchCount(source, contract.original);
|
|
1601
|
+
if (count > 0) matches.get(contract.key).push({ candidate, count });
|
|
1602
|
+
}
|
|
1591
1603
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
let source = fs.readFileSync(renderer, "utf8");
|
|
1595
|
-
const enable = exactTextMatchCount(source, CLAUDE_DRAFT_AGENT_MENTIONS);
|
|
1596
|
-
const filter = exactTextMatchCount(source, CLAUDE_AGENT_MENTION_FILTER);
|
|
1597
|
-
const chip = exactTextMatchCount(source, CLAUDE_AGENT_MENTION_SELECT);
|
|
1598
|
-
for (const [label, count] of [["enable", enable], ["filter", filter], ["chip", chip]]) {
|
|
1604
|
+
for (const contract of contracts) {
|
|
1605
|
+
const count = matches.get(contract.key).reduce((total, match) => total + match.count, 0);
|
|
1599
1606
|
if (count !== 1) {
|
|
1600
|
-
throw new Error(`Claude follow-up agent-mention ${
|
|
1607
|
+
throw new Error(`Claude follow-up agent-mention ${contract.key} patch expected 1 match, found ${count}`);
|
|
1601
1608
|
}
|
|
1602
1609
|
}
|
|
1603
1610
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
.
|
|
1607
|
-
|
|
1608
|
-
|
|
1611
|
+
const assets = [];
|
|
1612
|
+
for (const candidate of [...new Set(
|
|
1613
|
+
[...matches.values()].flatMap((entries) => entries.map(({ candidate: file }) => file)),
|
|
1614
|
+
)].sort()) {
|
|
1615
|
+
const source = fs.readFileSync(candidate, "utf8");
|
|
1616
|
+
const applicable = contracts.filter((contract) => source.includes(contract.original));
|
|
1617
|
+
let patched = source;
|
|
1618
|
+
for (const contract of applicable) {
|
|
1619
|
+
patched = patched.replace(contract.original, contract.replacement);
|
|
1620
|
+
}
|
|
1621
|
+
writeAtomic(candidate, patched, fs.statSync(candidate).mode & 0o777);
|
|
1622
|
+
assets.push({
|
|
1623
|
+
path: path.relative(ionDist, candidate).split(path.sep).join("/"),
|
|
1624
|
+
sha256: sha256(Buffer.from(patched, "utf8")),
|
|
1625
|
+
patches: applicable.map(({ key }) => key),
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1609
1629
|
return {
|
|
1610
|
-
enable,
|
|
1611
|
-
filter,
|
|
1612
|
-
chip,
|
|
1613
|
-
|
|
1614
|
-
sha256: sha256(Buffer.from(source, "utf8")),
|
|
1630
|
+
enable: 1,
|
|
1631
|
+
filter: 1,
|
|
1632
|
+
chip: 1,
|
|
1633
|
+
assets,
|
|
1615
1634
|
};
|
|
1616
1635
|
}
|
|
1617
1636
|
|
|
@@ -1661,23 +1680,6 @@ function patchFixedWidthAsarString(asarPath, original, replacement, label) {
|
|
|
1661
1680
|
function patchClaudePlanUsageRequest(asarPath) {
|
|
1662
1681
|
const archive = fs.readFileSync(asarPath);
|
|
1663
1682
|
const source = archive.toString("latin1");
|
|
1664
|
-
const legacyPatches = [];
|
|
1665
|
-
for (
|
|
1666
|
-
let offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST);
|
|
1667
|
-
offset !== -1;
|
|
1668
|
-
offset = source.indexOf(CLAUDE_PLAN_USAGE_REQUEST, offset + CLAUDE_PLAN_USAGE_REQUEST.length)
|
|
1669
|
-
) {
|
|
1670
|
-
legacyPatches.push({
|
|
1671
|
-
offset,
|
|
1672
|
-
original: CLAUDE_PLAN_USAGE_REQUEST,
|
|
1673
|
-
replacement: IMPEL_CLAUDE_PLAN_USAGE_REQUEST,
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1676
|
-
if (legacyPatches.length > 0) {
|
|
1677
|
-
return applyFixedWidthAsarPatches(
|
|
1678
|
-
asarPath, archive, legacyPatches, "Claude plan-usage gateway request",
|
|
1679
|
-
);
|
|
1680
|
-
}
|
|
1681
1683
|
const credentialAccessor = source.match(
|
|
1682
1684
|
/function ([A-Za-z_$][\w$]*)\(e\)\{var t;return [\s\S]{0,500}?\[custom-3p\] Credentials loaded from managed config/u,
|
|
1683
1685
|
)?.[1];
|
|
@@ -1693,10 +1695,10 @@ function patchClaudePlanUsageRequest(asarPath) {
|
|
|
1693
1695
|
return applyFixedWidthAsarPatches(asarPath, archive, [], "Claude plan-usage gateway request");
|
|
1694
1696
|
}
|
|
1695
1697
|
|
|
1696
|
-
const requestPattern = /([A-Za-z_$][\w$]*)
|
|
1698
|
+
const requestPattern = /const ([A-Za-z_$][\w$]*)=`\$\{[A-Za-z_$][\w$]*\(\)\}\/api\/organizations\/\$\{[A-Za-z_$][\w$]*\}\/usage\$\{[A-Za-z_$][\w$]*\?"\?skip_spend=1":""\}`,([A-Za-z_$][\w$]*)=await ([A-Za-z_$][\w$]*)\.net\.fetch\(\1,\{signal:AbortSignal\.timeout\([A-Za-z_$][\w$]*\)\}\)/gu;
|
|
1697
1699
|
const patches = [...source.matchAll(requestPattern)].map((match) => {
|
|
1698
1700
|
const original = match[0];
|
|
1699
|
-
const replacement =
|
|
1701
|
+
const replacement = `const ${match[1]}="https://gateway.useimpel.com/ui/a/u",${match[2]}=await ${match[3]}.net.fetch(${match[1]},{headers:{key:${credentialAccessor}(${configAccessor}()).apiKey}})`;
|
|
1700
1702
|
if (replacement.length > original.length) {
|
|
1701
1703
|
throw new Error("Claude plan-usage gateway request replacement no longer fits the vendor ASAR contract");
|
|
1702
1704
|
}
|
|
@@ -1997,7 +1999,11 @@ function vendoredChatGPTWrapperPlist(vendorPath, asarHash, identity, executableN
|
|
|
1997
1999
|
<key>CFBundleShortVersionString</key><string>${xmlEscape(bundleVersion(vendorPath) || "1")}</string>
|
|
1998
2000
|
<key>ElectronAsarIntegrity</key><dict><key>Resources/app.asar</key><dict><key>algorithm</key><string>SHA256</string><key>hash</key><string>${xmlEscape(asarHash)}</string></dict></dict>
|
|
1999
2001
|
<key>LSMinimumSystemVersion</key><string>12.0</string>
|
|
2002
|
+
<key>NSCalendarsFullAccessUsageDescription</key><string>Allow access so that ChatGPT can read and update your calendar events when you ask.</string>
|
|
2003
|
+
<key>NSCalendarsUsageDescription</key><string>Allow access so that ChatGPT can read and update your calendar events when you ask.</string>
|
|
2000
2004
|
<key>NSHighResolutionCapable</key><true/>
|
|
2005
|
+
<key>NSRemindersFullAccessUsageDescription</key><string>Allow access so that ChatGPT can read and update your reminders when you ask.</string>
|
|
2006
|
+
<key>NSRemindersUsageDescription</key><string>Allow access so that ChatGPT can read and update your reminders when you ask.</string>
|
|
2001
2007
|
</dict></plist>
|
|
2002
2008
|
`;
|
|
2003
2009
|
}
|
package/src/windowsApps.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
@@ -251,14 +252,47 @@ export function findManagedWindowsChatGPTApp(appsRoot, dependencies = {}) {
|
|
|
251
252
|
return null;
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
function sha256File(file) {
|
|
256
|
+
const hash = crypto.createHash("sha256");
|
|
257
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
258
|
+
const descriptor = fs.openSync(file, "r");
|
|
259
|
+
try {
|
|
260
|
+
for (let offset = 0; ; offset += buffer.length) {
|
|
261
|
+
const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, offset);
|
|
262
|
+
if (bytesRead === 0) break;
|
|
263
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
264
|
+
}
|
|
265
|
+
return hash.digest("hex");
|
|
266
|
+
} finally {
|
|
267
|
+
fs.closeSync(descriptor);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function windowsChatGPTSourceIdentity(binary) {
|
|
272
|
+
const sourceRoot = path.dirname(binary);
|
|
273
|
+
const files = {
|
|
274
|
+
executable: binary,
|
|
275
|
+
appAsar: path.join(sourceRoot, "resources", "app.asar"),
|
|
276
|
+
codex: path.join(sourceRoot, "resources", "codex.exe"),
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
executableName: path.basename(binary),
|
|
280
|
+
sha256: Object.fromEntries(
|
|
281
|
+
Object.entries(files).map(([name, file]) => [name, sha256File(file)]),
|
|
282
|
+
),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
254
286
|
export function windowsChatGPTStageIsCurrent(binary, appsRoot) {
|
|
255
287
|
if (!binary || !findManagedWindowsChatGPTApp(appsRoot)) return false;
|
|
256
288
|
try {
|
|
257
289
|
const record = JSON.parse(
|
|
258
290
|
fs.readFileSync(path.join(windowsChatGPTCacheRoot(appsRoot), "source.json"), "utf8"),
|
|
259
291
|
);
|
|
260
|
-
return
|
|
261
|
-
|
|
292
|
+
return record.schemaVersion === 2
|
|
293
|
+
&& path.win32.normalize(record.sourceBinary || "").toLowerCase()
|
|
294
|
+
=== path.win32.normalize(binary).toLowerCase()
|
|
295
|
+
&& JSON.stringify(record.sourceIdentity) === JSON.stringify(windowsChatGPTSourceIdentity(binary));
|
|
262
296
|
} catch {
|
|
263
297
|
return false;
|
|
264
298
|
}
|
|
@@ -288,6 +322,7 @@ export function stageWindowsChatGPTApp(binary, appsRoot, dependencies = {}) {
|
|
|
288
322
|
if (!io.isFile(binary) || !io.isFile(sourceAsar) || !io.isFile(sourceCodex)) {
|
|
289
323
|
throw new Error(`unsupported ChatGPT/Codex Windows app layout at ${sourceRoot}`);
|
|
290
324
|
}
|
|
325
|
+
const sourceIdentity = windowsChatGPTSourceIdentity(binary);
|
|
291
326
|
|
|
292
327
|
const cacheRoot = windowsChatGPTCacheRoot(appsRoot);
|
|
293
328
|
const destination = windowsManagedChatGPTRoot(appsRoot);
|
|
@@ -309,8 +344,9 @@ export function stageWindowsChatGPTApp(binary, appsRoot, dependencies = {}) {
|
|
|
309
344
|
try {
|
|
310
345
|
io.rename(staging, destination);
|
|
311
346
|
fs.writeFileSync(path.join(cacheRoot, "source.json"), `${JSON.stringify({
|
|
312
|
-
schemaVersion:
|
|
347
|
+
schemaVersion: 2,
|
|
313
348
|
sourceBinary: path.win32.normalize(binary),
|
|
349
|
+
sourceIdentity,
|
|
314
350
|
stagedAt: new Date().toISOString(),
|
|
315
351
|
}, null, 2)}\n`, { mode: 0o600 });
|
|
316
352
|
io.remove(backup);
|