@soulguard/openclaw 0.1.4 → 0.2.1
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/README.md +1 -57
- package/dist/index.js +548 -1622
- package/dist/openclaw.plugin.json +11 -0
- package/package.json +3 -3
- package/src/context.test.ts +92 -0
- package/src/context.ts +43 -0
- package/src/guard.test.ts +41 -15
- package/src/guard.ts +31 -23
- package/src/index.ts +6 -4
- package/src/openclaw-types.ts +3 -1
- package/src/plugin.ts +25 -108
- package/src/templates.test.ts +17 -21
- package/src/templates.ts +89 -98
package/dist/index.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
1
|
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
-
for (let key of __getOwnPropNames(mod))
|
|
11
|
-
if (!__hasOwnProp.call(to, key))
|
|
12
|
-
__defProp(to, key, {
|
|
13
|
-
get: () => mod[key],
|
|
14
|
-
enumerable: true
|
|
15
|
-
});
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
2
|
var __export = (target, all) => {
|
|
19
3
|
for (var name in all)
|
|
20
4
|
__defProp(target, name, {
|
|
@@ -24,88 +8,76 @@ var __export = (target, all) => {
|
|
|
24
8
|
set: (newValue) => all[name] = () => newValue
|
|
25
9
|
});
|
|
26
10
|
};
|
|
27
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
28
11
|
|
|
29
12
|
// src/templates.ts
|
|
30
|
-
var SOULGUARD_CONFIG = ["soulguard.json"];
|
|
31
|
-
var CORE_IDENTITY = ["SOUL.md", "AGENTS.md", "IDENTITY.md", "USER.md"];
|
|
32
|
-
var CORE_SESSION = ["TOOLS.md", "HEARTBEAT.md", "BOOTSTRAP.md"];
|
|
33
|
-
var CORE_MEMORY = ["MEMORY.md"];
|
|
34
|
-
var MEMORY_DIR = ["memory/**"];
|
|
35
|
-
var SKILLS = ["skills/**"];
|
|
36
|
-
var OPENCLAW_CONFIG = ["openclaw.json"];
|
|
37
|
-
var CRON = ["cron/jobs.json"];
|
|
38
|
-
var EXTENSIONS = ["extensions/**"];
|
|
39
|
-
var SESSIONS = ["sessions/**"];
|
|
40
|
-
var ALL_KNOWN_PATHS = [
|
|
41
|
-
...SOULGUARD_CONFIG,
|
|
42
|
-
...CORE_IDENTITY,
|
|
43
|
-
...CORE_SESSION,
|
|
44
|
-
...CORE_MEMORY,
|
|
45
|
-
...MEMORY_DIR,
|
|
46
|
-
...SKILLS,
|
|
47
|
-
...OPENCLAW_CONFIG,
|
|
48
|
-
...CRON,
|
|
49
|
-
...EXTENSIONS,
|
|
50
|
-
...SESSIONS
|
|
51
|
-
];
|
|
52
|
-
var defaultTemplate = {
|
|
53
|
-
name: "default",
|
|
54
|
-
description: "Core identity and config in vault, memory and skills tracked in ledger",
|
|
55
|
-
vault: [
|
|
56
|
-
...SOULGUARD_CONFIG,
|
|
57
|
-
...CORE_IDENTITY,
|
|
58
|
-
...CORE_SESSION,
|
|
59
|
-
...OPENCLAW_CONFIG,
|
|
60
|
-
...CRON,
|
|
61
|
-
...EXTENSIONS
|
|
62
|
-
],
|
|
63
|
-
ledger: [...CORE_MEMORY, ...MEMORY_DIR, ...SKILLS],
|
|
64
|
-
unprotected: [...SESSIONS]
|
|
65
|
-
};
|
|
66
|
-
var paranoidTemplate = {
|
|
67
|
-
name: "paranoid",
|
|
68
|
-
description: "Everything possible in vault, only skills in ledger",
|
|
69
|
-
vault: [
|
|
70
|
-
...SOULGUARD_CONFIG,
|
|
71
|
-
...CORE_IDENTITY,
|
|
72
|
-
...CORE_SESSION,
|
|
73
|
-
...CORE_MEMORY,
|
|
74
|
-
...MEMORY_DIR,
|
|
75
|
-
...SKILLS,
|
|
76
|
-
...OPENCLAW_CONFIG,
|
|
77
|
-
...CRON,
|
|
78
|
-
...EXTENSIONS
|
|
79
|
-
],
|
|
80
|
-
ledger: [...SESSIONS],
|
|
81
|
-
unprotected: []
|
|
82
|
-
};
|
|
83
|
-
var relaxedTemplate = {
|
|
84
|
-
name: "relaxed",
|
|
85
|
-
description: "Only soulguard config locked, everything else tracked — good for initial setup",
|
|
86
|
-
vault: [...SOULGUARD_CONFIG],
|
|
87
|
-
ledger: [
|
|
88
|
-
...CORE_IDENTITY,
|
|
89
|
-
...CORE_SESSION,
|
|
90
|
-
...CORE_MEMORY,
|
|
91
|
-
...MEMORY_DIR,
|
|
92
|
-
...SKILLS,
|
|
93
|
-
...OPENCLAW_CONFIG,
|
|
94
|
-
...CRON,
|
|
95
|
-
...EXTENSIONS
|
|
96
|
-
],
|
|
97
|
-
unprotected: [...SESSIONS]
|
|
98
|
-
};
|
|
99
13
|
var templates = {
|
|
100
|
-
default:
|
|
101
|
-
|
|
102
|
-
|
|
14
|
+
default: {
|
|
15
|
+
name: "default",
|
|
16
|
+
description: "Core identity and config protected, memory and skills watched",
|
|
17
|
+
protect: [
|
|
18
|
+
"workspace/SOUL.md",
|
|
19
|
+
"workspace/AGENTS.md",
|
|
20
|
+
"workspace/IDENTITY.md",
|
|
21
|
+
"workspace/USER.md",
|
|
22
|
+
"workspace/TOOLS.md",
|
|
23
|
+
"workspace/HEARTBEAT.md",
|
|
24
|
+
"workspace/BOOTSTRAP.md",
|
|
25
|
+
"openclaw.json",
|
|
26
|
+
"cron/",
|
|
27
|
+
"extensions/"
|
|
28
|
+
],
|
|
29
|
+
watch: ["workspace/MEMORY.md", "workspace/memory/", "workspace/skills/"],
|
|
30
|
+
release: ["workspace/sessions/"]
|
|
31
|
+
},
|
|
32
|
+
paranoid: {
|
|
33
|
+
name: "paranoid",
|
|
34
|
+
description: "Everything protected, only sessions watched",
|
|
35
|
+
protect: [
|
|
36
|
+
"workspace/SOUL.md",
|
|
37
|
+
"workspace/AGENTS.md",
|
|
38
|
+
"workspace/IDENTITY.md",
|
|
39
|
+
"workspace/USER.md",
|
|
40
|
+
"workspace/TOOLS.md",
|
|
41
|
+
"workspace/HEARTBEAT.md",
|
|
42
|
+
"workspace/BOOTSTRAP.md",
|
|
43
|
+
"workspace/MEMORY.md",
|
|
44
|
+
"workspace/memory/",
|
|
45
|
+
"workspace/skills/",
|
|
46
|
+
"openclaw.json",
|
|
47
|
+
"cron/",
|
|
48
|
+
"extensions/"
|
|
49
|
+
],
|
|
50
|
+
watch: ["workspace/sessions/"],
|
|
51
|
+
release: []
|
|
52
|
+
},
|
|
53
|
+
relaxed: {
|
|
54
|
+
name: "relaxed",
|
|
55
|
+
description: "Everything watched — good for initial setup",
|
|
56
|
+
protect: [],
|
|
57
|
+
watch: [
|
|
58
|
+
"workspace/SOUL.md",
|
|
59
|
+
"workspace/AGENTS.md",
|
|
60
|
+
"workspace/IDENTITY.md",
|
|
61
|
+
"workspace/USER.md",
|
|
62
|
+
"workspace/TOOLS.md",
|
|
63
|
+
"workspace/HEARTBEAT.md",
|
|
64
|
+
"workspace/BOOTSTRAP.md",
|
|
65
|
+
"workspace/MEMORY.md",
|
|
66
|
+
"workspace/memory/",
|
|
67
|
+
"workspace/skills/",
|
|
68
|
+
"openclaw.json",
|
|
69
|
+
"cron/",
|
|
70
|
+
"extensions/"
|
|
71
|
+
],
|
|
72
|
+
release: ["workspace/sessions/"]
|
|
73
|
+
}
|
|
103
74
|
};
|
|
104
75
|
// src/plugin.ts
|
|
105
76
|
import { readFileSync } from "node:fs";
|
|
106
|
-
import
|
|
77
|
+
import os from "node:os";
|
|
78
|
+
import { join as join2 } from "node:path";
|
|
107
79
|
|
|
108
|
-
// ../core/src/result.ts
|
|
80
|
+
// ../core/src/util/result.ts
|
|
109
81
|
function ok(value) {
|
|
110
82
|
return { ok: true, value };
|
|
111
83
|
}
|
|
@@ -4085,82 +4057,29 @@ var coerce = {
|
|
|
4085
4057
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
4086
4058
|
};
|
|
4087
4059
|
var NEVER = INVALID;
|
|
4088
|
-
// ../core/src/schema.ts
|
|
4060
|
+
// ../core/src/sdk/schema.ts
|
|
4061
|
+
var tierSchema = exports_external.enum(["protect", "watch"]);
|
|
4062
|
+
var ownershipSchema = exports_external.object({
|
|
4063
|
+
user: exports_external.string(),
|
|
4064
|
+
group: exports_external.string(),
|
|
4065
|
+
mode: exports_external.string()
|
|
4066
|
+
});
|
|
4067
|
+
var daemonConfigSchema = exports_external.object({
|
|
4068
|
+
channel: exports_external.string()
|
|
4069
|
+
}).passthrough();
|
|
4089
4070
|
var soulguardConfigSchema = exports_external.object({
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4071
|
+
version: exports_external.literal(1),
|
|
4072
|
+
guardian: exports_external.string(),
|
|
4073
|
+
files: exports_external.record(exports_external.string(), tierSchema),
|
|
4074
|
+
git: exports_external.boolean().optional(),
|
|
4075
|
+
defaultOwnership: ownershipSchema.optional(),
|
|
4076
|
+
daemon: daemonConfigSchema.optional()
|
|
4093
4077
|
});
|
|
4094
4078
|
function parseConfig(raw) {
|
|
4095
4079
|
return soulguardConfigSchema.parse(raw);
|
|
4096
4080
|
}
|
|
4097
|
-
// ../core/src/system-ops.ts
|
|
4098
|
-
async function getFileInfo(path, ops) {
|
|
4099
|
-
const statResult = await ops.stat(path);
|
|
4100
|
-
if (!statResult.ok)
|
|
4101
|
-
return statResult;
|
|
4102
|
-
const hashResult = await ops.hashFile(path);
|
|
4103
|
-
if (!hashResult.ok)
|
|
4104
|
-
return hashResult;
|
|
4105
|
-
return ok({
|
|
4106
|
-
path,
|
|
4107
|
-
ownership: statResult.value.ownership,
|
|
4108
|
-
hash: hashResult.value
|
|
4109
|
-
});
|
|
4110
|
-
}
|
|
4111
|
-
// ../core/src/system-ops-mock.ts
|
|
4081
|
+
// ../core/src/util/system-ops-mock.ts
|
|
4112
4082
|
import { resolve } from "node:path";
|
|
4113
|
-
|
|
4114
|
-
// ../core/src/glob.ts
|
|
4115
|
-
function isGlob(path) {
|
|
4116
|
-
return path.includes("*");
|
|
4117
|
-
}
|
|
4118
|
-
function createGlobMatcher(pattern) {
|
|
4119
|
-
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4120
|
-
let normalized = pattern;
|
|
4121
|
-
normalized = normalized.replace(/\/\*\*\//g, "<GLOBSTAR>");
|
|
4122
|
-
normalized = normalized.replace(/^\*\*\//, "<GLOBSTAR_PREFIX>");
|
|
4123
|
-
normalized = normalized.replace(/\/\*\*$/, "<GLOBSTAR_SUFFIX>");
|
|
4124
|
-
normalized = normalized.replace(/^\*\*$/, "<GLOBSTAR_ALL>");
|
|
4125
|
-
const regexStr = normalized.split(/(<GLOBSTAR(?:_PREFIX|_SUFFIX|_ALL)?>)/).map((part) => {
|
|
4126
|
-
if (part === "<GLOBSTAR>")
|
|
4127
|
-
return "/(?:.+/)?";
|
|
4128
|
-
if (part === "<GLOBSTAR_PREFIX>")
|
|
4129
|
-
return "(?:.+/)?";
|
|
4130
|
-
if (part === "<GLOBSTAR_SUFFIX>")
|
|
4131
|
-
return "(?:/.*)?";
|
|
4132
|
-
if (part === "<GLOBSTAR_ALL>")
|
|
4133
|
-
return ".*";
|
|
4134
|
-
return part.split("*").map(escape).join("[^/]*");
|
|
4135
|
-
}).join("");
|
|
4136
|
-
const regex = new RegExp(`^${regexStr}$`);
|
|
4137
|
-
return (path) => regex.test(path);
|
|
4138
|
-
}
|
|
4139
|
-
function matchGlob(pattern, path) {
|
|
4140
|
-
return createGlobMatcher(pattern)(path);
|
|
4141
|
-
}
|
|
4142
|
-
async function resolvePatterns(ops, patterns) {
|
|
4143
|
-
const files = new Set;
|
|
4144
|
-
for (const pattern of patterns) {
|
|
4145
|
-
if (isGlob(pattern)) {
|
|
4146
|
-
const result = await ops.glob(pattern);
|
|
4147
|
-
if (!result.ok) {
|
|
4148
|
-
return err(result.error);
|
|
4149
|
-
}
|
|
4150
|
-
for (const match of result.value) {
|
|
4151
|
-
const statResult = await ops.stat(match);
|
|
4152
|
-
if (statResult.ok && !statResult.value.isDirectory) {
|
|
4153
|
-
files.add(match);
|
|
4154
|
-
}
|
|
4155
|
-
}
|
|
4156
|
-
} else {
|
|
4157
|
-
files.add(pattern);
|
|
4158
|
-
}
|
|
4159
|
-
}
|
|
4160
|
-
return ok([...files].sort());
|
|
4161
|
-
}
|
|
4162
|
-
|
|
4163
|
-
// ../core/src/system-ops-mock.ts
|
|
4164
4083
|
class MockSystemOps {
|
|
4165
4084
|
workspace;
|
|
4166
4085
|
files = new Map;
|
|
@@ -4169,10 +4088,21 @@ class MockSystemOps {
|
|
|
4169
4088
|
ops = [];
|
|
4170
4089
|
failingExecs = new Set;
|
|
4171
4090
|
failingDeletes = new Set;
|
|
4091
|
+
failingStats = new Set;
|
|
4092
|
+
failingHashes = new Set;
|
|
4093
|
+
failingListDirs = new Set;
|
|
4094
|
+
failingReads = new Set;
|
|
4172
4095
|
execCallCounts = new Map;
|
|
4173
4096
|
execFailOnCall = new Map;
|
|
4174
4097
|
constructor(workspace) {
|
|
4175
4098
|
this.workspace = workspace;
|
|
4099
|
+
this.files.set(workspace, {
|
|
4100
|
+
content: "",
|
|
4101
|
+
owner: "root",
|
|
4102
|
+
group: "root",
|
|
4103
|
+
mode: "755",
|
|
4104
|
+
isDirectory: true
|
|
4105
|
+
});
|
|
4176
4106
|
}
|
|
4177
4107
|
resolve(path) {
|
|
4178
4108
|
return resolve(this.workspace, path);
|
|
@@ -4185,6 +4115,15 @@ class MockSystemOps {
|
|
|
4185
4115
|
mode: opts.mode ?? "644"
|
|
4186
4116
|
});
|
|
4187
4117
|
}
|
|
4118
|
+
addDirectory(path, opts = {}) {
|
|
4119
|
+
this.files.set(this.resolve(path), {
|
|
4120
|
+
content: "",
|
|
4121
|
+
owner: opts.owner ?? "unknown",
|
|
4122
|
+
group: opts.group ?? "unknown",
|
|
4123
|
+
mode: opts.mode ?? "755",
|
|
4124
|
+
isDirectory: true
|
|
4125
|
+
});
|
|
4126
|
+
}
|
|
4188
4127
|
addUser(name) {
|
|
4189
4128
|
this.users.add(name);
|
|
4190
4129
|
}
|
|
@@ -4198,6 +4137,9 @@ class MockSystemOps {
|
|
|
4198
4137
|
return ok(this.groups.has(name));
|
|
4199
4138
|
}
|
|
4200
4139
|
async stat(path) {
|
|
4140
|
+
if (this.failingStats.has(path)) {
|
|
4141
|
+
return err({ kind: "permission_denied", path, operation: "stat" });
|
|
4142
|
+
}
|
|
4201
4143
|
const full = this.resolve(path);
|
|
4202
4144
|
const file = this.files.get(full);
|
|
4203
4145
|
if (!file)
|
|
@@ -4205,7 +4147,7 @@ class MockSystemOps {
|
|
|
4205
4147
|
return ok({
|
|
4206
4148
|
path,
|
|
4207
4149
|
ownership: { user: file.owner, group: file.group, mode: file.mode },
|
|
4208
|
-
isDirectory: false
|
|
4150
|
+
isDirectory: file.isDirectory ?? false
|
|
4209
4151
|
});
|
|
4210
4152
|
}
|
|
4211
4153
|
async chown(path, owner) {
|
|
@@ -4218,6 +4160,23 @@ class MockSystemOps {
|
|
|
4218
4160
|
file.group = owner.group;
|
|
4219
4161
|
return ok(undefined);
|
|
4220
4162
|
}
|
|
4163
|
+
async chownRecursive(path, owner) {
|
|
4164
|
+
const full = this.resolve(path);
|
|
4165
|
+
const file = this.files.get(full);
|
|
4166
|
+
if (!file)
|
|
4167
|
+
return err({ kind: "not_found", path });
|
|
4168
|
+
this.ops.push({ kind: "chown", path, owner });
|
|
4169
|
+
file.owner = owner.user;
|
|
4170
|
+
file.group = owner.group;
|
|
4171
|
+
const prefix = full + "/";
|
|
4172
|
+
for (const [childPath, childFile] of this.files) {
|
|
4173
|
+
if (childPath.startsWith(prefix)) {
|
|
4174
|
+
childFile.owner = owner.user;
|
|
4175
|
+
childFile.group = owner.group;
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
return ok(undefined);
|
|
4179
|
+
}
|
|
4221
4180
|
async chmod(path, mode) {
|
|
4222
4181
|
const full = this.resolve(path);
|
|
4223
4182
|
const file = this.files.get(full);
|
|
@@ -4227,7 +4186,25 @@ class MockSystemOps {
|
|
|
4227
4186
|
file.mode = mode;
|
|
4228
4187
|
return ok(undefined);
|
|
4229
4188
|
}
|
|
4189
|
+
async chmodRecursive(path, mode) {
|
|
4190
|
+
const full = this.resolve(path);
|
|
4191
|
+
const file = this.files.get(full);
|
|
4192
|
+
if (!file)
|
|
4193
|
+
return err({ kind: "not_found", path });
|
|
4194
|
+
this.ops.push({ kind: "chmod", path, mode });
|
|
4195
|
+
file.mode = mode;
|
|
4196
|
+
const prefix = full + "/";
|
|
4197
|
+
for (const [childPath, childFile] of this.files) {
|
|
4198
|
+
if (childPath.startsWith(prefix)) {
|
|
4199
|
+
childFile.mode = mode;
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
return ok(undefined);
|
|
4203
|
+
}
|
|
4230
4204
|
async readFile(path) {
|
|
4205
|
+
if (this.failingReads.has(path)) {
|
|
4206
|
+
return err({ kind: "permission_denied", path, operation: "read" });
|
|
4207
|
+
}
|
|
4231
4208
|
const full = this.resolve(path);
|
|
4232
4209
|
const file = this.files.get(full);
|
|
4233
4210
|
if (!file)
|
|
@@ -4250,14 +4227,26 @@ class MockSystemOps {
|
|
|
4250
4227
|
async mkdir(path) {
|
|
4251
4228
|
const full = this.resolve(path);
|
|
4252
4229
|
if (!this.files.has(full)) {
|
|
4253
|
-
this.files.set(full, {
|
|
4230
|
+
this.files.set(full, {
|
|
4231
|
+
content: "",
|
|
4232
|
+
owner: "root",
|
|
4233
|
+
group: "root",
|
|
4234
|
+
mode: "755",
|
|
4235
|
+
isDirectory: true
|
|
4236
|
+
});
|
|
4254
4237
|
}
|
|
4255
4238
|
const parts = path.split("/");
|
|
4256
4239
|
for (let i = 1;i < parts.length; i++) {
|
|
4257
4240
|
const parent = parts.slice(0, i).join("/");
|
|
4258
4241
|
const parentFull = this.resolve(parent);
|
|
4259
4242
|
if (!this.files.has(parentFull)) {
|
|
4260
|
-
this.files.set(parentFull, {
|
|
4243
|
+
this.files.set(parentFull, {
|
|
4244
|
+
content: "",
|
|
4245
|
+
owner: "root",
|
|
4246
|
+
group: "root",
|
|
4247
|
+
mode: "755",
|
|
4248
|
+
isDirectory: true
|
|
4249
|
+
});
|
|
4261
4250
|
}
|
|
4262
4251
|
}
|
|
4263
4252
|
return ok(undefined);
|
|
@@ -4286,6 +4275,9 @@ class MockSystemOps {
|
|
|
4286
4275
|
return ok(undefined);
|
|
4287
4276
|
}
|
|
4288
4277
|
async hashFile(path) {
|
|
4278
|
+
if (this.failingHashes.has(path)) {
|
|
4279
|
+
return err({ kind: "io_error", path, message: "simulated hash failure" });
|
|
4280
|
+
}
|
|
4289
4281
|
const full = this.resolve(path);
|
|
4290
4282
|
const file = this.files.get(full);
|
|
4291
4283
|
if (!file)
|
|
@@ -4294,18 +4286,37 @@ class MockSystemOps {
|
|
|
4294
4286
|
hash.update(file.content);
|
|
4295
4287
|
return ok(hash.digest("hex"));
|
|
4296
4288
|
}
|
|
4297
|
-
async
|
|
4298
|
-
const
|
|
4299
|
-
const
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4289
|
+
async chmodDirectoryTree(path, modes) {
|
|
4290
|
+
const full = this.resolve(path);
|
|
4291
|
+
const dir = this.files.get(full);
|
|
4292
|
+
if (!dir)
|
|
4293
|
+
return err({ kind: "not_found", path });
|
|
4294
|
+
this.ops.push({ kind: "chmod", path, mode: modes.dirMode });
|
|
4295
|
+
dir.mode = modes.dirMode;
|
|
4296
|
+
const prefix = full + "/";
|
|
4297
|
+
for (const [childPath, childFile] of this.files) {
|
|
4298
|
+
if (childPath.startsWith(prefix)) {
|
|
4299
|
+
childFile.mode = childFile.isDirectory ? modes.dirMode : modes.fileMode;
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
return ok(undefined);
|
|
4303
|
+
}
|
|
4304
|
+
async listDir(path) {
|
|
4305
|
+
if (this.failingListDirs.has(path)) {
|
|
4306
|
+
return err({ kind: "permission_denied", path, operation: "listDir" });
|
|
4307
|
+
}
|
|
4308
|
+
const full = this.resolve(path);
|
|
4309
|
+
const dir = this.files.get(full);
|
|
4310
|
+
if (!dir)
|
|
4311
|
+
return err({ kind: "not_found", path });
|
|
4312
|
+
const prefix = full + "/";
|
|
4313
|
+
const files = [];
|
|
4314
|
+
for (const [childPath, childFile] of this.files) {
|
|
4315
|
+
if (childPath.startsWith(prefix) && !childFile.isDirectory) {
|
|
4316
|
+
files.push(childPath.slice((this.workspace + "/").length));
|
|
4306
4317
|
}
|
|
4307
4318
|
}
|
|
4308
|
-
return ok(
|
|
4319
|
+
return ok(files.sort());
|
|
4309
4320
|
}
|
|
4310
4321
|
async exec(command, args) {
|
|
4311
4322
|
this.ops.push({ kind: "exec", command, args });
|
|
@@ -4321,1411 +4332,389 @@ class MockSystemOps {
|
|
|
4321
4332
|
}
|
|
4322
4333
|
return ok(undefined);
|
|
4323
4334
|
}
|
|
4335
|
+
execCaptureResults = new Map;
|
|
4336
|
+
async execCapture(command, args) {
|
|
4337
|
+
this.ops.push({ kind: "exec", command, args });
|
|
4338
|
+
const key = [command, ...args].join(" ");
|
|
4339
|
+
if (this.failingExecs.has(key)) {
|
|
4340
|
+
return err({ kind: "io_error", path: "", message: `${key} failed` });
|
|
4341
|
+
}
|
|
4342
|
+
return ok(this.execCaptureResults.get(key) ?? "");
|
|
4343
|
+
}
|
|
4324
4344
|
}
|
|
4325
|
-
// ../core/src/system-ops-node.ts
|
|
4326
|
-
import { resolve as resolve2, relative, dirname } from "node:path";
|
|
4327
|
-
import {
|
|
4328
|
-
stat as fsStat,
|
|
4329
|
-
readFile,
|
|
4330
|
-
chmod as fsChmod,
|
|
4331
|
-
writeFile as fsWriteFile,
|
|
4332
|
-
mkdir as fsMkdir,
|
|
4333
|
-
copyFile as fsCopyFile,
|
|
4334
|
-
unlink as fsUnlink,
|
|
4335
|
-
chown as fsChown,
|
|
4336
|
-
access
|
|
4337
|
-
} from "node:fs/promises";
|
|
4338
|
-
import { createHash } from "node:crypto";
|
|
4339
|
-
import { createReadStream } from "node:fs";
|
|
4345
|
+
// ../core/src/util/system-ops-node.ts
|
|
4340
4346
|
import { execFile } from "node:child_process";
|
|
4341
4347
|
import { promisify } from "node:util";
|
|
4342
4348
|
var execFileAsync = promisify(execFile);
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
}
|
|
4352
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
4353
|
-
return { kind: "io_error", path, message };
|
|
4354
|
-
}
|
|
4355
|
-
async function nameToUid(name) {
|
|
4356
|
-
const { stdout } = await execFileAsync("id", ["-u", name]);
|
|
4357
|
-
return parseInt(stdout.trim(), 10);
|
|
4358
|
-
}
|
|
4359
|
-
async function nameToGid(name) {
|
|
4360
|
-
if (process.platform === "darwin") {
|
|
4361
|
-
const { stdout: stdout2 } = await execFileAsync("dscl", [
|
|
4362
|
-
".",
|
|
4363
|
-
"-read",
|
|
4364
|
-
`/Groups/${name}`,
|
|
4365
|
-
"PrimaryGroupID"
|
|
4366
|
-
]);
|
|
4367
|
-
const gid = stdout2.trim().split(/\s+/).pop();
|
|
4368
|
-
if (!gid || !/^\d+$/.test(gid)) {
|
|
4369
|
-
throw new Error(`Could not resolve GID for group '${name}'`);
|
|
4370
|
-
}
|
|
4371
|
-
return parseInt(gid, 10);
|
|
4372
|
-
}
|
|
4373
|
-
const { stdout } = await execFileAsync("getent", ["group", name]);
|
|
4374
|
-
const field = stdout.split(":")[2];
|
|
4375
|
-
if (!field || !/^\d+$/.test(field.trim())) {
|
|
4376
|
-
throw new Error(`Could not resolve GID for group '${name}'`);
|
|
4377
|
-
}
|
|
4378
|
-
return parseInt(field.trim(), 10);
|
|
4349
|
+
// ../core/src/sdk/state.ts
|
|
4350
|
+
import { createHash } from "node:crypto";
|
|
4351
|
+
|
|
4352
|
+
// ../core/src/sdk/staging.ts
|
|
4353
|
+
import { join } from "node:path";
|
|
4354
|
+
var STAGING_DIR = ".soulguard-staging";
|
|
4355
|
+
function stagingPath(filePath) {
|
|
4356
|
+
return join(STAGING_DIR, filePath);
|
|
4379
4357
|
}
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
const { stdout } = await execFileAsync("id", ["-un", String(uid)]);
|
|
4383
|
-
return stdout.trim();
|
|
4384
|
-
} catch {
|
|
4385
|
-
return String(uid);
|
|
4386
|
-
}
|
|
4358
|
+
function isStagingPath(filePath) {
|
|
4359
|
+
return filePath === STAGING_DIR || filePath.startsWith(STAGING_DIR + "/");
|
|
4387
4360
|
}
|
|
4388
|
-
|
|
4389
|
-
const platform = process.platform;
|
|
4361
|
+
function isDeleteSentinel(content) {
|
|
4390
4362
|
try {
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
".",
|
|
4394
|
-
"-search",
|
|
4395
|
-
"/Groups",
|
|
4396
|
-
"PrimaryGroupID",
|
|
4397
|
-
String(gid)
|
|
4398
|
-
]);
|
|
4399
|
-
const match = stdout.match(/^(\S+)/);
|
|
4400
|
-
return match?.[1] ?? String(gid);
|
|
4401
|
-
} else {
|
|
4402
|
-
const { stdout } = await execFileAsync("getent", ["group", String(gid)]);
|
|
4403
|
-
const name = stdout.split(":")[0];
|
|
4404
|
-
return name || String(gid);
|
|
4405
|
-
}
|
|
4363
|
+
const parsed = JSON.parse(content);
|
|
4364
|
+
return parsed != null && parsed.__soulguard_delete_sentinel__ === true;
|
|
4406
4365
|
} catch {
|
|
4407
|
-
return
|
|
4366
|
+
return false;
|
|
4408
4367
|
}
|
|
4409
4368
|
}
|
|
4410
|
-
function modeToString(mode) {
|
|
4411
|
-
return (mode & 511).toString(8).padStart(3, "0");
|
|
4412
|
-
}
|
|
4413
4369
|
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
}
|
|
4419
|
-
resolvePath(path) {
|
|
4420
|
-
const full = resolve2(this.workspace, path);
|
|
4421
|
-
const rel = relative(this.workspace, full);
|
|
4422
|
-
if (rel.startsWith("..")) {
|
|
4423
|
-
return err({
|
|
4424
|
-
kind: "io_error",
|
|
4425
|
-
path,
|
|
4426
|
-
message: "Path traversal outside workspace"
|
|
4427
|
-
});
|
|
4428
|
-
}
|
|
4429
|
-
return ok(full);
|
|
4430
|
-
}
|
|
4431
|
-
async userExists(name) {
|
|
4432
|
-
try {
|
|
4433
|
-
await execFileAsync("id", ["-u", name]);
|
|
4434
|
-
return ok(true);
|
|
4435
|
-
} catch (e) {
|
|
4436
|
-
if (e instanceof Error && "code" in e && typeof e.code === "number") {
|
|
4437
|
-
return ok(false);
|
|
4438
|
-
}
|
|
4439
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
4440
|
-
return err({ kind: "io_error", path: "", message: `userExists(${name}): ${message}` });
|
|
4441
|
-
}
|
|
4442
|
-
}
|
|
4443
|
-
async groupExists(name) {
|
|
4444
|
-
try {
|
|
4445
|
-
if (process.platform === "darwin") {
|
|
4446
|
-
await execFileAsync("dscl", [".", "-read", `/Groups/${name}`, "PrimaryGroupID"]);
|
|
4447
|
-
} else {
|
|
4448
|
-
await execFileAsync("getent", ["group", name]);
|
|
4449
|
-
}
|
|
4450
|
-
return ok(true);
|
|
4451
|
-
} catch (e) {
|
|
4452
|
-
if (e instanceof Error && "code" in e && typeof e.code === "number") {
|
|
4453
|
-
return ok(false);
|
|
4454
|
-
}
|
|
4455
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
4456
|
-
return err({ kind: "io_error", path: "", message: `groupExists(${name}): ${message}` });
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
async stat(path) {
|
|
4460
|
-
const resolved = this.resolvePath(path);
|
|
4461
|
-
if (!resolved.ok)
|
|
4462
|
-
return resolved;
|
|
4463
|
-
try {
|
|
4464
|
-
const s = await fsStat(resolved.value);
|
|
4465
|
-
const [user, group] = await Promise.all([uidToName(s.uid), gidToName(s.gid)]);
|
|
4466
|
-
return ok({
|
|
4467
|
-
path,
|
|
4468
|
-
ownership: { user, group, mode: modeToString(s.mode) },
|
|
4469
|
-
isDirectory: s.isDirectory()
|
|
4470
|
-
});
|
|
4471
|
-
} catch (e) {
|
|
4472
|
-
return err(mapError(e, path, "stat"));
|
|
4473
|
-
}
|
|
4474
|
-
}
|
|
4475
|
-
async chown(path, owner) {
|
|
4476
|
-
const resolved = this.resolvePath(path);
|
|
4477
|
-
if (!resolved.ok)
|
|
4478
|
-
return resolved;
|
|
4479
|
-
try {
|
|
4480
|
-
const uid = await nameToUid(owner.user);
|
|
4481
|
-
const gid = await nameToGid(owner.group);
|
|
4482
|
-
await fsChown(resolved.value, uid, gid);
|
|
4483
|
-
return ok(undefined);
|
|
4484
|
-
} catch (e) {
|
|
4485
|
-
return err(mapError(e, path, "chown"));
|
|
4486
|
-
}
|
|
4487
|
-
}
|
|
4488
|
-
async chmod(path, mode) {
|
|
4489
|
-
const resolved = this.resolvePath(path);
|
|
4490
|
-
if (!resolved.ok)
|
|
4491
|
-
return resolved;
|
|
4492
|
-
try {
|
|
4493
|
-
await fsChmod(resolved.value, parseInt(mode, 8));
|
|
4494
|
-
return ok(undefined);
|
|
4495
|
-
} catch (e) {
|
|
4496
|
-
return err(mapError(e, path, "chmod"));
|
|
4497
|
-
}
|
|
4498
|
-
}
|
|
4499
|
-
async readFile(path) {
|
|
4500
|
-
const resolved = this.resolvePath(path);
|
|
4501
|
-
if (!resolved.ok)
|
|
4502
|
-
return resolved;
|
|
4503
|
-
try {
|
|
4504
|
-
const content = await readFile(resolved.value, "utf-8");
|
|
4505
|
-
return ok(content);
|
|
4506
|
-
} catch (e) {
|
|
4507
|
-
return err(mapError(e, path, "readFile"));
|
|
4508
|
-
}
|
|
4509
|
-
}
|
|
4510
|
-
async createUser(name, group) {
|
|
4511
|
-
try {
|
|
4512
|
-
if (process.platform === "darwin") {
|
|
4513
|
-
const { stdout: gidOutput } = await execFileAsync("dscl", [
|
|
4514
|
-
".",
|
|
4515
|
-
"-read",
|
|
4516
|
-
`/Groups/${group}`,
|
|
4517
|
-
"PrimaryGroupID"
|
|
4518
|
-
]);
|
|
4519
|
-
const gid = gidOutput.trim().split(/\s+/).pop();
|
|
4520
|
-
if (!gid || !/^\d+$/.test(gid)) {
|
|
4521
|
-
return err({
|
|
4522
|
-
kind: "io_error",
|
|
4523
|
-
path: `/Groups/${group}`,
|
|
4524
|
-
message: `Could not resolve GID for group ${group}`
|
|
4525
|
-
});
|
|
4526
|
-
}
|
|
4527
|
-
await execFileAsync("dscl", [".", "-create", `/Users/${name}`]);
|
|
4528
|
-
const MAX_SYSTEM_ID = 499;
|
|
4529
|
-
let uid = 400;
|
|
4530
|
-
while (uid <= MAX_SYSTEM_ID) {
|
|
4531
|
-
const { stdout: uidSearch } = await execFileAsync("dscl", [
|
|
4532
|
-
".",
|
|
4533
|
-
"-search",
|
|
4534
|
-
"/Users",
|
|
4535
|
-
"UniqueID",
|
|
4536
|
-
String(uid)
|
|
4537
|
-
]);
|
|
4538
|
-
if (!uidSearch.trim())
|
|
4539
|
-
break;
|
|
4540
|
-
uid++;
|
|
4541
|
-
}
|
|
4542
|
-
if (uid > MAX_SYSTEM_ID) {
|
|
4543
|
-
return err({
|
|
4544
|
-
kind: "io_error",
|
|
4545
|
-
path: "",
|
|
4546
|
-
message: `No available UID in system range (400-${MAX_SYSTEM_ID})`
|
|
4547
|
-
});
|
|
4548
|
-
}
|
|
4549
|
-
await execFileAsync("dscl", [".", "-create", `/Users/${name}`, "UniqueID", String(uid)]);
|
|
4550
|
-
await execFileAsync("dscl", [".", "-create", `/Users/${name}`, "PrimaryGroupID", gid]);
|
|
4551
|
-
await execFileAsync("dscl", [
|
|
4552
|
-
".",
|
|
4553
|
-
"-create",
|
|
4554
|
-
`/Users/${name}`,
|
|
4555
|
-
"UserShell",
|
|
4556
|
-
"/usr/bin/false"
|
|
4557
|
-
]);
|
|
4558
|
-
} else {
|
|
4559
|
-
await execFileAsync("useradd", ["-r", "-g", group, "-s", "/usr/bin/false", name]);
|
|
4560
|
-
}
|
|
4561
|
-
return ok(undefined);
|
|
4562
|
-
} catch (e) {
|
|
4563
|
-
return err({
|
|
4564
|
-
kind: "io_error",
|
|
4565
|
-
path: "",
|
|
4566
|
-
message: `createUser ${name}: ${e instanceof Error ? e.message : String(e)}`
|
|
4567
|
-
});
|
|
4568
|
-
}
|
|
4569
|
-
}
|
|
4570
|
-
async createGroup(name) {
|
|
4571
|
-
try {
|
|
4572
|
-
if (process.platform === "darwin") {
|
|
4573
|
-
await execFileAsync("dscl", [".", "-create", `/Groups/${name}`]);
|
|
4574
|
-
const MAX_SYSTEM_GID = 499;
|
|
4575
|
-
let gid = 400;
|
|
4576
|
-
while (gid <= MAX_SYSTEM_GID) {
|
|
4577
|
-
const { stdout: searchOut } = await execFileAsync("dscl", [
|
|
4578
|
-
".",
|
|
4579
|
-
"-search",
|
|
4580
|
-
"/Groups",
|
|
4581
|
-
"PrimaryGroupID",
|
|
4582
|
-
String(gid)
|
|
4583
|
-
]);
|
|
4584
|
-
if (!searchOut.trim())
|
|
4585
|
-
break;
|
|
4586
|
-
gid++;
|
|
4587
|
-
}
|
|
4588
|
-
if (gid > MAX_SYSTEM_GID) {
|
|
4589
|
-
return err({
|
|
4590
|
-
kind: "io_error",
|
|
4591
|
-
path: "",
|
|
4592
|
-
message: `No available GID in system range (400-${MAX_SYSTEM_GID})`
|
|
4593
|
-
});
|
|
4594
|
-
}
|
|
4595
|
-
await execFileAsync("dscl", [
|
|
4596
|
-
".",
|
|
4597
|
-
"-create",
|
|
4598
|
-
`/Groups/${name}`,
|
|
4599
|
-
"PrimaryGroupID",
|
|
4600
|
-
String(gid)
|
|
4601
|
-
]);
|
|
4602
|
-
} else {
|
|
4603
|
-
await execFileAsync("groupadd", [name]);
|
|
4604
|
-
}
|
|
4605
|
-
return ok(undefined);
|
|
4606
|
-
} catch (e) {
|
|
4607
|
-
return err({
|
|
4608
|
-
kind: "io_error",
|
|
4609
|
-
path: "",
|
|
4610
|
-
message: `createGroup ${name}: ${e instanceof Error ? e.message : String(e)}`
|
|
4611
|
-
});
|
|
4612
|
-
}
|
|
4613
|
-
}
|
|
4614
|
-
async writeFile(path, content) {
|
|
4615
|
-
const resolved = this.resolvePath(path);
|
|
4616
|
-
if (!resolved.ok)
|
|
4617
|
-
return resolved;
|
|
4618
|
-
try {
|
|
4619
|
-
await fsMkdir(dirname(resolved.value), { recursive: true });
|
|
4620
|
-
await fsWriteFile(resolved.value, content, "utf-8");
|
|
4621
|
-
return ok(undefined);
|
|
4622
|
-
} catch (e) {
|
|
4623
|
-
return err(mapError(e, path, "writeFile"));
|
|
4624
|
-
}
|
|
4625
|
-
}
|
|
4626
|
-
async mkdir(path) {
|
|
4627
|
-
const resolved = this.resolvePath(path);
|
|
4628
|
-
if (!resolved.ok)
|
|
4629
|
-
return resolved;
|
|
4630
|
-
try {
|
|
4631
|
-
await fsMkdir(resolved.value, { recursive: true });
|
|
4632
|
-
return ok(undefined);
|
|
4633
|
-
} catch (e) {
|
|
4634
|
-
return err(mapError(e, path, "mkdir"));
|
|
4635
|
-
}
|
|
4636
|
-
}
|
|
4637
|
-
async copyFile(src, dest) {
|
|
4638
|
-
const resolvedSrc = this.resolvePath(src);
|
|
4639
|
-
if (!resolvedSrc.ok)
|
|
4640
|
-
return resolvedSrc;
|
|
4641
|
-
const resolvedDest = this.resolvePath(dest);
|
|
4642
|
-
if (!resolvedDest.ok)
|
|
4643
|
-
return resolvedDest;
|
|
4644
|
-
try {
|
|
4645
|
-
await fsMkdir(dirname(resolvedDest.value), { recursive: true });
|
|
4646
|
-
await fsCopyFile(resolvedSrc.value, resolvedDest.value);
|
|
4647
|
-
return ok(undefined);
|
|
4648
|
-
} catch (e) {
|
|
4649
|
-
return err(mapError(e, src, "copyFile"));
|
|
4650
|
-
}
|
|
4651
|
-
}
|
|
4652
|
-
async exists(path) {
|
|
4653
|
-
const resolved = this.resolvePath(path);
|
|
4654
|
-
if (!resolved.ok)
|
|
4655
|
-
return ok(false);
|
|
4656
|
-
try {
|
|
4657
|
-
await access(resolved.value);
|
|
4658
|
-
return ok(true);
|
|
4659
|
-
} catch (e) {
|
|
4660
|
-
if (e instanceof Error && "code" in e && e.code === "ENOENT") {
|
|
4661
|
-
return ok(false);
|
|
4662
|
-
}
|
|
4663
|
-
return err({
|
|
4664
|
-
kind: "io_error",
|
|
4665
|
-
path,
|
|
4666
|
-
message: `exists: ${e instanceof Error ? e.message : String(e)}`
|
|
4667
|
-
});
|
|
4668
|
-
}
|
|
4669
|
-
}
|
|
4670
|
-
async deleteFile(path) {
|
|
4671
|
-
const resolved = this.resolvePath(path);
|
|
4672
|
-
if (!resolved.ok)
|
|
4673
|
-
return resolved;
|
|
4674
|
-
try {
|
|
4675
|
-
await fsUnlink(resolved.value);
|
|
4676
|
-
return ok(undefined);
|
|
4677
|
-
} catch (e) {
|
|
4678
|
-
return err(mapError(e, path, "unlink"));
|
|
4679
|
-
}
|
|
4680
|
-
}
|
|
4681
|
-
async hashFile(path) {
|
|
4682
|
-
const resolved = this.resolvePath(path);
|
|
4683
|
-
if (!resolved.ok)
|
|
4684
|
-
return resolved;
|
|
4685
|
-
return new Promise((resolve3) => {
|
|
4686
|
-
const hash = createHash("sha256");
|
|
4687
|
-
const stream = createReadStream(resolved.value);
|
|
4688
|
-
stream.on("data", (chunk) => hash.update(chunk));
|
|
4689
|
-
stream.on("end", () => resolve3(ok(hash.digest("hex"))));
|
|
4690
|
-
stream.on("error", (e) => resolve3(err(mapError(e, path, "hashFile"))));
|
|
4691
|
-
});
|
|
4692
|
-
}
|
|
4693
|
-
async glob(pattern) {
|
|
4694
|
-
try {
|
|
4695
|
-
const fs = await import("node:fs");
|
|
4696
|
-
const { promisify: promisify2 } = await import("node:util");
|
|
4697
|
-
const globFn = fs.glob;
|
|
4698
|
-
if (typeof globFn !== "function") {
|
|
4699
|
-
return err({
|
|
4700
|
-
kind: "io_error",
|
|
4701
|
-
path: pattern,
|
|
4702
|
-
message: "fs.glob requires Node.js 22+"
|
|
4703
|
-
});
|
|
4704
|
-
}
|
|
4705
|
-
const globAsync = promisify2(globFn);
|
|
4706
|
-
const matches = await globAsync(pattern, { cwd: this.workspace });
|
|
4707
|
-
return ok([...matches].sort());
|
|
4708
|
-
} catch (e) {
|
|
4709
|
-
return err({
|
|
4710
|
-
kind: "io_error",
|
|
4711
|
-
path: pattern,
|
|
4712
|
-
message: `glob: ${e instanceof Error ? e.message : String(e)}`
|
|
4713
|
-
});
|
|
4714
|
-
}
|
|
4715
|
-
}
|
|
4716
|
-
async exec(command, args) {
|
|
4717
|
-
const execFileAsync2 = promisify(execFile);
|
|
4718
|
-
try {
|
|
4719
|
-
await execFileAsync2(command, args, { cwd: this.workspace });
|
|
4720
|
-
return ok(undefined);
|
|
4721
|
-
} catch (e) {
|
|
4722
|
-
return err({
|
|
4723
|
-
kind: "io_error",
|
|
4724
|
-
path: this.workspace,
|
|
4725
|
-
message: `exec ${command}: ${e instanceof Error ? e.message : String(e)}`
|
|
4726
|
-
});
|
|
4727
|
-
}
|
|
4728
|
-
}
|
|
4370
|
+
// ../core/src/util/constants.ts
|
|
4371
|
+
var SOULGUARD_GROUP = "soulguard";
|
|
4372
|
+
function getProtectOwnership(guardian) {
|
|
4373
|
+
return { user: guardian, group: SOULGUARD_GROUP, mode: "444" };
|
|
4729
4374
|
}
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
return vaultResult;
|
|
4739
|
-
if (!ledgerResult.ok)
|
|
4740
|
-
return ledgerResult;
|
|
4741
|
-
const vaultPaths = vaultResult.value;
|
|
4742
|
-
const ledgerPaths = ledgerResult.value;
|
|
4743
|
-
const [vault, ledger] = await Promise.all([
|
|
4744
|
-
Promise.all(vaultPaths.map((path) => checkPath(path, "vault", expectedVaultOwnership, ops))),
|
|
4745
|
-
Promise.all(ledgerPaths.map((path) => checkPath(path, "ledger", expectedLedgerOwnership, ops)))
|
|
4746
|
-
]);
|
|
4747
|
-
const issues = [...vault, ...ledger].filter((f) => f.status !== "ok");
|
|
4748
|
-
return ok({ vault, ledger, issues });
|
|
4749
|
-
}
|
|
4750
|
-
async function checkPath(filePath, tier, expectedOwnership, ops) {
|
|
4751
|
-
const infoResult = await getFileInfo(filePath, ops);
|
|
4752
|
-
if (!infoResult.ok) {
|
|
4753
|
-
if (infoResult.error.kind === "not_found") {
|
|
4754
|
-
return { tier, status: "missing", path: filePath };
|
|
4755
|
-
}
|
|
4756
|
-
return { tier, status: "error", path: filePath, error: infoResult.error };
|
|
4757
|
-
}
|
|
4758
|
-
const file = infoResult.value;
|
|
4759
|
-
const issues = [];
|
|
4760
|
-
if (file.ownership.user !== expectedOwnership.user) {
|
|
4761
|
-
issues.push({
|
|
4762
|
-
kind: "wrong_owner",
|
|
4763
|
-
expected: expectedOwnership.user,
|
|
4764
|
-
actual: file.ownership.user
|
|
4765
|
-
});
|
|
4766
|
-
}
|
|
4767
|
-
if (file.ownership.group !== expectedOwnership.group) {
|
|
4768
|
-
issues.push({
|
|
4769
|
-
kind: "wrong_group",
|
|
4770
|
-
expected: expectedOwnership.group,
|
|
4771
|
-
actual: file.ownership.group
|
|
4772
|
-
});
|
|
4773
|
-
}
|
|
4774
|
-
if (file.ownership.mode !== expectedOwnership.mode) {
|
|
4775
|
-
issues.push({
|
|
4776
|
-
kind: "wrong_mode",
|
|
4777
|
-
expected: expectedOwnership.mode,
|
|
4778
|
-
actual: file.ownership.mode
|
|
4779
|
-
});
|
|
4780
|
-
}
|
|
4781
|
-
if (issues.length === 0) {
|
|
4782
|
-
return { tier, status: "ok", file };
|
|
4375
|
+
|
|
4376
|
+
// ../core/src/sdk/state.ts
|
|
4377
|
+
var PROTECT_DIR_MODE = "555";
|
|
4378
|
+
|
|
4379
|
+
class StateTreeBuildError extends Error {
|
|
4380
|
+
constructor(error) {
|
|
4381
|
+
super(`Failed to build state tree: ${error.message}`);
|
|
4382
|
+
this.name = "StateTreeBuildError";
|
|
4783
4383
|
}
|
|
4784
|
-
return { tier, status: "drifted", file, issues };
|
|
4785
4384
|
}
|
|
4786
|
-
// ../core/src/diff.ts
|
|
4787
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
4788
4385
|
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
const
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
const
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
return value;
|
|
4816
|
-
}
|
|
4817
|
-
};
|
|
4818
|
-
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
4819
|
-
let editLength = 1;
|
|
4820
|
-
let maxEditLength = newLen + oldLen;
|
|
4821
|
-
if (options.maxEditLength != null) {
|
|
4822
|
-
maxEditLength = Math.min(maxEditLength, options.maxEditLength);
|
|
4823
|
-
}
|
|
4824
|
-
const maxExecutionTime = (_a = options.timeout) !== null && _a !== undefined ? _a : Infinity;
|
|
4825
|
-
const abortAfterTimestamp = Date.now() + maxExecutionTime;
|
|
4826
|
-
const bestPath = [{ oldPos: -1, lastComponent: undefined }];
|
|
4827
|
-
let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
|
|
4828
|
-
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
4829
|
-
return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
|
|
4830
|
-
}
|
|
4831
|
-
let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
|
|
4832
|
-
const execEditLength = () => {
|
|
4833
|
-
for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength);diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
|
|
4834
|
-
let basePath;
|
|
4835
|
-
const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
|
|
4836
|
-
if (removePath) {
|
|
4837
|
-
bestPath[diagonalPath - 1] = undefined;
|
|
4838
|
-
}
|
|
4839
|
-
let canAdd = false;
|
|
4840
|
-
if (addPath) {
|
|
4841
|
-
const addPathNewPos = addPath.oldPos - diagonalPath;
|
|
4842
|
-
canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
|
|
4843
|
-
}
|
|
4844
|
-
const canRemove = removePath && removePath.oldPos + 1 < oldLen;
|
|
4845
|
-
if (!canAdd && !canRemove) {
|
|
4846
|
-
bestPath[diagonalPath] = undefined;
|
|
4847
|
-
continue;
|
|
4848
|
-
}
|
|
4849
|
-
if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
|
|
4850
|
-
basePath = this.addToPath(addPath, true, false, 0, options);
|
|
4851
|
-
} else {
|
|
4852
|
-
basePath = this.addToPath(removePath, false, true, 1, options);
|
|
4853
|
-
}
|
|
4854
|
-
newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
|
|
4855
|
-
if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
4856
|
-
return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
|
|
4857
|
-
} else {
|
|
4858
|
-
bestPath[diagonalPath] = basePath;
|
|
4859
|
-
if (basePath.oldPos + 1 >= oldLen) {
|
|
4860
|
-
maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
|
|
4861
|
-
}
|
|
4862
|
-
if (newPos + 1 >= newLen) {
|
|
4863
|
-
minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
|
|
4864
|
-
}
|
|
4865
|
-
}
|
|
4866
|
-
}
|
|
4867
|
-
editLength++;
|
|
4868
|
-
};
|
|
4869
|
-
if (callback) {
|
|
4870
|
-
(function exec() {
|
|
4871
|
-
setTimeout(function() {
|
|
4872
|
-
if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
|
|
4873
|
-
return callback(undefined);
|
|
4874
|
-
}
|
|
4875
|
-
if (!execEditLength()) {
|
|
4876
|
-
exec();
|
|
4877
|
-
}
|
|
4878
|
-
}, 0);
|
|
4879
|
-
})();
|
|
4880
|
-
} else {
|
|
4881
|
-
while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
|
|
4882
|
-
const ret = execEditLength();
|
|
4883
|
-
if (ret) {
|
|
4884
|
-
return ret;
|
|
4386
|
+
class StateTree {
|
|
4387
|
+
entities;
|
|
4388
|
+
config;
|
|
4389
|
+
protectOwnership;
|
|
4390
|
+
constructor(entities, config, protectOwnership) {
|
|
4391
|
+
this.entities = entities;
|
|
4392
|
+
this.config = config;
|
|
4393
|
+
this.protectOwnership = protectOwnership;
|
|
4394
|
+
}
|
|
4395
|
+
static async buildOrThrow(options) {
|
|
4396
|
+
const result = await StateTree.build(options);
|
|
4397
|
+
if (!result.ok)
|
|
4398
|
+
throw new StateTreeBuildError(result.error);
|
|
4399
|
+
return result.value;
|
|
4400
|
+
}
|
|
4401
|
+
static async build(options) {
|
|
4402
|
+
const { ops, config } = options;
|
|
4403
|
+
const protectOwnership = getProtectOwnership(config.guardian);
|
|
4404
|
+
const entities = [];
|
|
4405
|
+
for (const [key, tier] of Object.entries(config.files)) {
|
|
4406
|
+
let isDir = key.endsWith("/");
|
|
4407
|
+
const path = isDir ? key.slice(0, -1) : key;
|
|
4408
|
+
if (!isDir) {
|
|
4409
|
+
const statResult = await ops.stat(path);
|
|
4410
|
+
if (statResult.ok && statResult.value.isDirectory) {
|
|
4411
|
+
isDir = true;
|
|
4885
4412
|
}
|
|
4886
4413
|
}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
oldPos: path.oldPos + oldPosInc,
|
|
4894
|
-
lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
|
|
4895
|
-
};
|
|
4896
|
-
} else {
|
|
4897
|
-
return {
|
|
4898
|
-
oldPos: path.oldPos + oldPosInc,
|
|
4899
|
-
lastComponent: { count: 1, added, removed, previousComponent: last }
|
|
4900
|
-
};
|
|
4901
|
-
}
|
|
4902
|
-
}
|
|
4903
|
-
extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
|
|
4904
|
-
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
4905
|
-
let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
|
|
4906
|
-
while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
|
|
4907
|
-
newPos++;
|
|
4908
|
-
oldPos++;
|
|
4909
|
-
commonCount++;
|
|
4910
|
-
if (options.oneChangePerToken) {
|
|
4911
|
-
basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
|
|
4912
|
-
}
|
|
4913
|
-
}
|
|
4914
|
-
if (commonCount && !options.oneChangePerToken) {
|
|
4915
|
-
basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
|
|
4916
|
-
}
|
|
4917
|
-
basePath.oldPos = oldPos;
|
|
4918
|
-
return newPos;
|
|
4919
|
-
}
|
|
4920
|
-
equals(left, right, options) {
|
|
4921
|
-
if (options.comparator) {
|
|
4922
|
-
return options.comparator(left, right);
|
|
4923
|
-
} else {
|
|
4924
|
-
return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
|
|
4925
|
-
}
|
|
4926
|
-
}
|
|
4927
|
-
removeEmpty(array) {
|
|
4928
|
-
const ret = [];
|
|
4929
|
-
for (let i = 0;i < array.length; i++) {
|
|
4930
|
-
if (array[i]) {
|
|
4931
|
-
ret.push(array[i]);
|
|
4932
|
-
}
|
|
4933
|
-
}
|
|
4934
|
-
return ret;
|
|
4935
|
-
}
|
|
4936
|
-
castInput(value, options) {
|
|
4937
|
-
return value;
|
|
4938
|
-
}
|
|
4939
|
-
tokenize(value, options) {
|
|
4940
|
-
return Array.from(value);
|
|
4941
|
-
}
|
|
4942
|
-
join(chars) {
|
|
4943
|
-
return chars.join("");
|
|
4944
|
-
}
|
|
4945
|
-
postProcess(changeObjects, options) {
|
|
4946
|
-
return changeObjects;
|
|
4947
|
-
}
|
|
4948
|
-
get useLongestToken() {
|
|
4949
|
-
return false;
|
|
4950
|
-
}
|
|
4951
|
-
buildValues(lastComponent, newTokens, oldTokens) {
|
|
4952
|
-
const components = [];
|
|
4953
|
-
let nextComponent;
|
|
4954
|
-
while (lastComponent) {
|
|
4955
|
-
components.push(lastComponent);
|
|
4956
|
-
nextComponent = lastComponent.previousComponent;
|
|
4957
|
-
delete lastComponent.previousComponent;
|
|
4958
|
-
lastComponent = nextComponent;
|
|
4959
|
-
}
|
|
4960
|
-
components.reverse();
|
|
4961
|
-
const componentLen = components.length;
|
|
4962
|
-
let componentPos = 0, newPos = 0, oldPos = 0;
|
|
4963
|
-
for (;componentPos < componentLen; componentPos++) {
|
|
4964
|
-
const component = components[componentPos];
|
|
4965
|
-
if (!component.removed) {
|
|
4966
|
-
if (!component.added && this.useLongestToken) {
|
|
4967
|
-
let value = newTokens.slice(newPos, newPos + component.count);
|
|
4968
|
-
value = value.map(function(value2, i) {
|
|
4969
|
-
const oldValue = oldTokens[oldPos + i];
|
|
4970
|
-
return oldValue.length > value2.length ? oldValue : value2;
|
|
4971
|
-
});
|
|
4972
|
-
component.value = this.join(value);
|
|
4973
|
-
} else {
|
|
4974
|
-
component.value = this.join(newTokens.slice(newPos, newPos + component.count));
|
|
4975
|
-
}
|
|
4976
|
-
newPos += component.count;
|
|
4977
|
-
if (!component.added) {
|
|
4978
|
-
oldPos += component.count;
|
|
4979
|
-
}
|
|
4414
|
+
if (isDir) {
|
|
4415
|
+
const result = await buildDirectory(ops, path, tier);
|
|
4416
|
+
if (!result.ok)
|
|
4417
|
+
return result;
|
|
4418
|
+
if (result.value)
|
|
4419
|
+
entities.push(result.value);
|
|
4980
4420
|
} else {
|
|
4981
|
-
|
|
4982
|
-
|
|
4421
|
+
const result = await buildFile(ops, key, tier);
|
|
4422
|
+
if (!result.ok)
|
|
4423
|
+
return result;
|
|
4424
|
+
if (result.value)
|
|
4425
|
+
entities.push(result.value);
|
|
4983
4426
|
}
|
|
4984
4427
|
}
|
|
4985
|
-
return
|
|
4428
|
+
return ok(new StateTree(entities, config, protectOwnership));
|
|
4986
4429
|
}
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
// ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/character.js
|
|
4990
|
-
class CharacterDiff extends Diff {
|
|
4991
|
-
}
|
|
4992
|
-
var characterDiff = new CharacterDiff;
|
|
4993
|
-
|
|
4994
|
-
// ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/util/string.js
|
|
4995
|
-
function longestCommonPrefix(str1, str2) {
|
|
4996
|
-
let i;
|
|
4997
|
-
for (i = 0;i < str1.length && i < str2.length; i++) {
|
|
4998
|
-
if (str1[i] != str2[i]) {
|
|
4999
|
-
return str1.slice(0, i);
|
|
5000
|
-
}
|
|
5001
|
-
}
|
|
5002
|
-
return str1.slice(0, i);
|
|
5003
|
-
}
|
|
5004
|
-
function longestCommonSuffix(str1, str2) {
|
|
5005
|
-
let i;
|
|
5006
|
-
if (!str1 || !str2 || str1[str1.length - 1] != str2[str2.length - 1]) {
|
|
5007
|
-
return "";
|
|
4430
|
+
flatFiles() {
|
|
4431
|
+
return collectFiles(this.entities);
|
|
5008
4432
|
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
4433
|
+
get approvalHash() {
|
|
4434
|
+
const changed = this.changedFiles();
|
|
4435
|
+
if (changed.length === 0)
|
|
4436
|
+
return null;
|
|
4437
|
+
const sorted = [...changed].sort((a, b) => a.path.localeCompare(b.path));
|
|
4438
|
+
const hasher = createHash("sha256");
|
|
4439
|
+
for (const file of sorted) {
|
|
4440
|
+
hasher.update(file.path);
|
|
4441
|
+
hasher.update(file.status);
|
|
4442
|
+
hasher.update(file.canonicalHash ?? "null");
|
|
4443
|
+
hasher.update(file.stagedHash ?? "null");
|
|
5012
4444
|
}
|
|
4445
|
+
return hasher.digest("hex");
|
|
5013
4446
|
}
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
function replacePrefix(string, oldPrefix, newPrefix) {
|
|
5017
|
-
if (string.slice(0, oldPrefix.length) != oldPrefix) {
|
|
5018
|
-
throw Error(`string ${JSON.stringify(string)} doesn't start with prefix ${JSON.stringify(oldPrefix)}; this is a bug`);
|
|
4447
|
+
changedFiles() {
|
|
4448
|
+
return this.flatFiles().filter((f) => f.status !== "unchanged");
|
|
5019
4449
|
}
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
function replaceSuffix(string, oldSuffix, newSuffix) {
|
|
5023
|
-
if (!oldSuffix) {
|
|
5024
|
-
return string + newSuffix;
|
|
4450
|
+
stagedFiles() {
|
|
4451
|
+
return this.flatFiles().filter((f) => f.stagedHash !== null || f.status === "deleted");
|
|
5025
4452
|
}
|
|
5026
|
-
|
|
5027
|
-
|
|
4453
|
+
driftedEntities() {
|
|
4454
|
+
return collectDrifts(this.entities, this.protectOwnership);
|
|
5028
4455
|
}
|
|
5029
|
-
return string.slice(0, -oldSuffix.length) + newSuffix;
|
|
5030
|
-
}
|
|
5031
|
-
function removePrefix(string, oldPrefix) {
|
|
5032
|
-
return replacePrefix(string, oldPrefix, "");
|
|
5033
|
-
}
|
|
5034
|
-
function removeSuffix(string, oldSuffix) {
|
|
5035
|
-
return replaceSuffix(string, oldSuffix, "");
|
|
5036
4456
|
}
|
|
5037
|
-
function
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
if (a.length > b.length) {
|
|
5043
|
-
startA = a.length - b.length;
|
|
5044
|
-
}
|
|
5045
|
-
let endB = b.length;
|
|
5046
|
-
if (a.length < b.length) {
|
|
5047
|
-
endB = a.length;
|
|
5048
|
-
}
|
|
5049
|
-
const map = Array(endB);
|
|
5050
|
-
let k = 0;
|
|
5051
|
-
map[0] = 0;
|
|
5052
|
-
for (let j = 1;j < endB; j++) {
|
|
5053
|
-
if (b[j] == b[k]) {
|
|
5054
|
-
map[j] = map[k];
|
|
4457
|
+
function collectFiles(entities) {
|
|
4458
|
+
const files = [];
|
|
4459
|
+
for (const entity of entities) {
|
|
4460
|
+
if (entity.kind === "file") {
|
|
4461
|
+
files.push(entity);
|
|
5055
4462
|
} else {
|
|
5056
|
-
|
|
5057
|
-
}
|
|
5058
|
-
while (k > 0 && b[j] != b[k]) {
|
|
5059
|
-
k = map[k];
|
|
5060
|
-
}
|
|
5061
|
-
if (b[j] == b[k]) {
|
|
5062
|
-
k++;
|
|
4463
|
+
files.push(...collectFiles(entity.children));
|
|
5063
4464
|
}
|
|
5064
4465
|
}
|
|
5065
|
-
|
|
5066
|
-
for (let i = startA;i < a.length; i++) {
|
|
5067
|
-
while (k > 0 && a[i] != b[k]) {
|
|
5068
|
-
k = map[k];
|
|
5069
|
-
}
|
|
5070
|
-
if (a[i] == b[k]) {
|
|
5071
|
-
k++;
|
|
5072
|
-
}
|
|
5073
|
-
}
|
|
5074
|
-
return k;
|
|
5075
|
-
}
|
|
5076
|
-
function trailingWs(string) {
|
|
5077
|
-
let i;
|
|
5078
|
-
for (i = string.length - 1;i >= 0; i--) {
|
|
5079
|
-
if (!string[i].match(/\s/)) {
|
|
5080
|
-
break;
|
|
5081
|
-
}
|
|
5082
|
-
}
|
|
5083
|
-
return string.substring(i + 1);
|
|
4466
|
+
return files;
|
|
5084
4467
|
}
|
|
5085
|
-
function
|
|
5086
|
-
const
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
left = left.toLowerCase();
|
|
5098
|
-
right = right.toLowerCase();
|
|
5099
|
-
}
|
|
5100
|
-
return left.trim() === right.trim();
|
|
5101
|
-
}
|
|
5102
|
-
tokenize(value, options = {}) {
|
|
5103
|
-
let parts;
|
|
5104
|
-
if (options.intlSegmenter) {
|
|
5105
|
-
const segmenter = options.intlSegmenter;
|
|
5106
|
-
if (segmenter.resolvedOptions().granularity != "word") {
|
|
5107
|
-
throw new Error('The segmenter passed must have a granularity of "word"');
|
|
5108
|
-
}
|
|
5109
|
-
parts = [];
|
|
5110
|
-
for (const segmentObj of Array.from(segmenter.segment(value))) {
|
|
5111
|
-
const segment = segmentObj.segment;
|
|
5112
|
-
if (parts.length && /\s/.test(parts[parts.length - 1]) && /\s/.test(segment)) {
|
|
5113
|
-
parts[parts.length - 1] += segment;
|
|
5114
|
-
} else {
|
|
5115
|
-
parts.push(segment);
|
|
5116
|
-
}
|
|
4468
|
+
function collectDrifts(entities, protectOwnership) {
|
|
4469
|
+
const drifts = [];
|
|
4470
|
+
for (const entity of entities) {
|
|
4471
|
+
if (entity.configTier === "protect" && entity.ownership) {
|
|
4472
|
+
const details = [];
|
|
4473
|
+
const expectedMode = entity.kind === "directory" ? PROTECT_DIR_MODE : protectOwnership.mode;
|
|
4474
|
+
if (entity.ownership.user !== protectOwnership.user) {
|
|
4475
|
+
details.push({
|
|
4476
|
+
kind: "wrong_owner",
|
|
4477
|
+
expected: protectOwnership.user,
|
|
4478
|
+
actual: entity.ownership.user
|
|
4479
|
+
});
|
|
5117
4480
|
}
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
if (/\s/.test(part)) {
|
|
5125
|
-
if (prevPart == null) {
|
|
5126
|
-
tokens.push(part);
|
|
5127
|
-
} else {
|
|
5128
|
-
tokens.push(tokens.pop() + part);
|
|
5129
|
-
}
|
|
5130
|
-
} else if (prevPart != null && /\s/.test(prevPart)) {
|
|
5131
|
-
if (tokens[tokens.length - 1] == prevPart) {
|
|
5132
|
-
tokens.push(tokens.pop() + part);
|
|
5133
|
-
} else {
|
|
5134
|
-
tokens.push(prevPart + part);
|
|
5135
|
-
}
|
|
5136
|
-
} else {
|
|
5137
|
-
tokens.push(part);
|
|
4481
|
+
if (entity.ownership.group !== protectOwnership.group) {
|
|
4482
|
+
details.push({
|
|
4483
|
+
kind: "wrong_group",
|
|
4484
|
+
expected: protectOwnership.group,
|
|
4485
|
+
actual: entity.ownership.group
|
|
4486
|
+
});
|
|
5138
4487
|
}
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
return tokens;
|
|
5142
|
-
}
|
|
5143
|
-
join(tokens) {
|
|
5144
|
-
return tokens.map((token, i) => {
|
|
5145
|
-
if (i == 0) {
|
|
5146
|
-
return token;
|
|
5147
|
-
} else {
|
|
5148
|
-
return token.replace(/^\s+/, "");
|
|
4488
|
+
if (entity.ownership.mode !== expectedMode) {
|
|
4489
|
+
details.push({ kind: "wrong_mode", expected: expectedMode, actual: entity.ownership.mode });
|
|
5149
4490
|
}
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
postProcess(changes, options) {
|
|
5153
|
-
if (!changes || options.oneChangePerToken) {
|
|
5154
|
-
return changes;
|
|
5155
|
-
}
|
|
5156
|
-
let lastKeep = null;
|
|
5157
|
-
let insertion = null;
|
|
5158
|
-
let deletion = null;
|
|
5159
|
-
changes.forEach((change) => {
|
|
5160
|
-
if (change.added) {
|
|
5161
|
-
insertion = change;
|
|
5162
|
-
} else if (change.removed) {
|
|
5163
|
-
deletion = change;
|
|
5164
|
-
} else {
|
|
5165
|
-
if (insertion || deletion) {
|
|
5166
|
-
dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change);
|
|
5167
|
-
}
|
|
5168
|
-
lastKeep = change;
|
|
5169
|
-
insertion = null;
|
|
5170
|
-
deletion = null;
|
|
4491
|
+
if (details.length > 0) {
|
|
4492
|
+
drifts.push({ entity, details });
|
|
5171
4493
|
}
|
|
5172
|
-
});
|
|
5173
|
-
if (insertion || deletion) {
|
|
5174
|
-
dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null);
|
|
5175
4494
|
}
|
|
5176
|
-
|
|
4495
|
+
if (entity.kind === "directory") {
|
|
4496
|
+
drifts.push(...collectDrifts(entity.children, protectOwnership));
|
|
4497
|
+
}
|
|
5177
4498
|
}
|
|
4499
|
+
return drifts;
|
|
5178
4500
|
}
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
}
|
|
5192
|
-
if (endKeep) {
|
|
5193
|
-
const commonWsSuffix = longestCommonSuffix(oldWsSuffix, newWsSuffix);
|
|
5194
|
-
endKeep.value = replacePrefix(endKeep.value, newWsSuffix, commonWsSuffix);
|
|
5195
|
-
deletion.value = removeSuffix(deletion.value, commonWsSuffix);
|
|
5196
|
-
insertion.value = removeSuffix(insertion.value, commonWsSuffix);
|
|
5197
|
-
}
|
|
5198
|
-
} else if (insertion) {
|
|
5199
|
-
if (startKeep) {
|
|
5200
|
-
const ws = leadingWs(insertion.value);
|
|
5201
|
-
insertion.value = insertion.value.substring(ws.length);
|
|
5202
|
-
}
|
|
5203
|
-
if (endKeep) {
|
|
5204
|
-
const ws = leadingWs(endKeep.value);
|
|
5205
|
-
endKeep.value = endKeep.value.substring(ws.length);
|
|
5206
|
-
}
|
|
5207
|
-
} else if (startKeep && endKeep) {
|
|
5208
|
-
const newWsFull = leadingWs(endKeep.value), delWsStart = leadingWs(deletion.value), delWsEnd = trailingWs(deletion.value);
|
|
5209
|
-
const newWsStart = longestCommonPrefix(newWsFull, delWsStart);
|
|
5210
|
-
deletion.value = removePrefix(deletion.value, newWsStart);
|
|
5211
|
-
const newWsEnd = longestCommonSuffix(removePrefix(newWsFull, newWsStart), delWsEnd);
|
|
5212
|
-
deletion.value = removeSuffix(deletion.value, newWsEnd);
|
|
5213
|
-
endKeep.value = replacePrefix(endKeep.value, newWsFull, newWsEnd);
|
|
5214
|
-
startKeep.value = replaceSuffix(startKeep.value, newWsFull, newWsFull.slice(0, newWsFull.length - newWsEnd.length));
|
|
5215
|
-
} else if (endKeep) {
|
|
5216
|
-
const endKeepWsPrefix = leadingWs(endKeep.value);
|
|
5217
|
-
const deletionWsSuffix = trailingWs(deletion.value);
|
|
5218
|
-
const overlap = maximumOverlap(deletionWsSuffix, endKeepWsPrefix);
|
|
5219
|
-
deletion.value = removeSuffix(deletion.value, overlap);
|
|
5220
|
-
} else if (startKeep) {
|
|
5221
|
-
const startKeepWsSuffix = trailingWs(startKeep.value);
|
|
5222
|
-
const deletionWsPrefix = leadingWs(deletion.value);
|
|
5223
|
-
const overlap = maximumOverlap(startKeepWsSuffix, deletionWsPrefix);
|
|
5224
|
-
deletion.value = removePrefix(deletion.value, overlap);
|
|
4501
|
+
async function buildFile(ops, path, tier) {
|
|
4502
|
+
const statResult = await ops.stat(path);
|
|
4503
|
+
let diskExists = false;
|
|
4504
|
+
let ownership = null;
|
|
4505
|
+
if (statResult.ok) {
|
|
4506
|
+
diskExists = true;
|
|
4507
|
+
ownership = statResult.value.ownership;
|
|
4508
|
+
} else if (statResult.error.kind !== "not_found") {
|
|
4509
|
+
return err({
|
|
4510
|
+
kind: "build_failed",
|
|
4511
|
+
message: `stat failed for ${path}: ${statResult.error.kind}`
|
|
4512
|
+
});
|
|
5225
4513
|
}
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
4514
|
+
let canonicalHash = null;
|
|
4515
|
+
if (diskExists) {
|
|
4516
|
+
const hashResult = await ops.hashFile(path);
|
|
4517
|
+
if (!hashResult.ok) {
|
|
4518
|
+
return err({
|
|
4519
|
+
kind: "build_failed",
|
|
4520
|
+
message: `hash failed for ${path}: ${hashResult.error.kind}`
|
|
4521
|
+
});
|
|
4522
|
+
}
|
|
4523
|
+
canonicalHash = hashResult.value;
|
|
5232
4524
|
}
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
this.tokenize = tokenize;
|
|
4525
|
+
const staged = stagingPath(path);
|
|
4526
|
+
const stagedExists = await ops.exists(staged);
|
|
4527
|
+
if (!stagedExists.ok) {
|
|
4528
|
+
return err({
|
|
4529
|
+
kind: "build_failed",
|
|
4530
|
+
message: `exists check failed for ${staged}: ${stagedExists.error.kind}`
|
|
4531
|
+
});
|
|
5241
4532
|
}
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
`
|
|
5250
|
-
|
|
5251
|
-
}
|
|
5252
|
-
} else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
|
|
5253
|
-
if (left.endsWith(`
|
|
5254
|
-
`)) {
|
|
5255
|
-
left = left.slice(0, -1);
|
|
5256
|
-
}
|
|
5257
|
-
if (right.endsWith(`
|
|
5258
|
-
`)) {
|
|
5259
|
-
right = right.slice(0, -1);
|
|
5260
|
-
}
|
|
4533
|
+
let stagedHash = null;
|
|
4534
|
+
let isDelete = false;
|
|
4535
|
+
if (stagedExists.value) {
|
|
4536
|
+
const content = await ops.readFile(staged);
|
|
4537
|
+
if (!content.ok) {
|
|
4538
|
+
return err({
|
|
4539
|
+
kind: "build_failed",
|
|
4540
|
+
message: `read failed for ${staged}: ${content.error.kind}`
|
|
4541
|
+
});
|
|
5261
4542
|
}
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
}
|
|
5265
|
-
var lineDiff = new LineDiff;
|
|
5266
|
-
function diffLines(oldStr, newStr, options) {
|
|
5267
|
-
return lineDiff.diff(oldStr, newStr, options);
|
|
5268
|
-
}
|
|
5269
|
-
function tokenize(value, options) {
|
|
5270
|
-
if (options.stripTrailingCr) {
|
|
5271
|
-
value = value.replace(/\r\n/g, `
|
|
5272
|
-
`);
|
|
5273
|
-
}
|
|
5274
|
-
const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
|
|
5275
|
-
if (!linesAndNewlines[linesAndNewlines.length - 1]) {
|
|
5276
|
-
linesAndNewlines.pop();
|
|
5277
|
-
}
|
|
5278
|
-
for (let i = 0;i < linesAndNewlines.length; i++) {
|
|
5279
|
-
const line = linesAndNewlines[i];
|
|
5280
|
-
if (i % 2 && !options.newlineIsToken) {
|
|
5281
|
-
retLines[retLines.length - 1] += line;
|
|
4543
|
+
if (isDeleteSentinel(content.value)) {
|
|
4544
|
+
isDelete = true;
|
|
5282
4545
|
} else {
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
}
|
|
5288
|
-
|
|
5289
|
-
// ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/sentence.js
|
|
5290
|
-
function isSentenceEndPunct(char) {
|
|
5291
|
-
return char == "." || char == "!" || char == "?";
|
|
5292
|
-
}
|
|
5293
|
-
|
|
5294
|
-
class SentenceDiff extends Diff {
|
|
5295
|
-
tokenize(value) {
|
|
5296
|
-
var _a;
|
|
5297
|
-
const result = [];
|
|
5298
|
-
let tokenStartI = 0;
|
|
5299
|
-
for (let i = 0;i < value.length; i++) {
|
|
5300
|
-
if (i == value.length - 1) {
|
|
5301
|
-
result.push(value.slice(tokenStartI));
|
|
5302
|
-
break;
|
|
5303
|
-
}
|
|
5304
|
-
if (isSentenceEndPunct(value[i]) && value[i + 1].match(/\s/)) {
|
|
5305
|
-
result.push(value.slice(tokenStartI, i + 1));
|
|
5306
|
-
i = tokenStartI = i + 1;
|
|
5307
|
-
while ((_a = value[i + 1]) === null || _a === undefined ? undefined : _a.match(/\s/)) {
|
|
5308
|
-
i++;
|
|
5309
|
-
}
|
|
5310
|
-
result.push(value.slice(tokenStartI, i + 1));
|
|
5311
|
-
tokenStartI = i + 1;
|
|
4546
|
+
const hashResult = await ops.hashFile(staged);
|
|
4547
|
+
if (!hashResult.ok) {
|
|
4548
|
+
return err({
|
|
4549
|
+
kind: "build_failed",
|
|
4550
|
+
message: `hash failed for ${staged}: ${hashResult.error.kind}`
|
|
4551
|
+
});
|
|
5312
4552
|
}
|
|
4553
|
+
stagedHash = hashResult.value;
|
|
5313
4554
|
}
|
|
5314
|
-
return result;
|
|
5315
|
-
}
|
|
5316
|
-
}
|
|
5317
|
-
var sentenceDiff = new SentenceDiff;
|
|
5318
|
-
|
|
5319
|
-
// ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/css.js
|
|
5320
|
-
class CssDiff extends Diff {
|
|
5321
|
-
tokenize(value) {
|
|
5322
|
-
return value.split(/([{}:;,]|\s+)/);
|
|
5323
4555
|
}
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
// ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/json.js
|
|
5328
|
-
class JsonDiff extends Diff {
|
|
5329
|
-
constructor() {
|
|
5330
|
-
super(...arguments);
|
|
5331
|
-
this.tokenize = tokenize;
|
|
4556
|
+
if (!diskExists && !stagedExists.value) {
|
|
4557
|
+
return ok(null);
|
|
5332
4558
|
}
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
}
|
|
5343
|
-
}
|
|
5344
|
-
var jsonDiff = new JsonDiff;
|
|
5345
|
-
function canonicalize(obj, stack, replacementStack, replacer, key) {
|
|
5346
|
-
stack = stack || [];
|
|
5347
|
-
replacementStack = replacementStack || [];
|
|
5348
|
-
if (replacer) {
|
|
5349
|
-
obj = replacer(key === undefined ? "" : key, obj);
|
|
5350
|
-
}
|
|
5351
|
-
let i;
|
|
5352
|
-
for (i = 0;i < stack.length; i += 1) {
|
|
5353
|
-
if (stack[i] === obj) {
|
|
5354
|
-
return replacementStack[i];
|
|
5355
|
-
}
|
|
5356
|
-
}
|
|
5357
|
-
let canonicalizedObj;
|
|
5358
|
-
if (Object.prototype.toString.call(obj) === "[object Array]") {
|
|
5359
|
-
stack.push(obj);
|
|
5360
|
-
canonicalizedObj = new Array(obj.length);
|
|
5361
|
-
replacementStack.push(canonicalizedObj);
|
|
5362
|
-
for (i = 0;i < obj.length; i += 1) {
|
|
5363
|
-
canonicalizedObj[i] = canonicalize(obj[i], stack, replacementStack, replacer, String(i));
|
|
5364
|
-
}
|
|
5365
|
-
stack.pop();
|
|
5366
|
-
replacementStack.pop();
|
|
5367
|
-
return canonicalizedObj;
|
|
5368
|
-
}
|
|
5369
|
-
if (obj && obj.toJSON) {
|
|
5370
|
-
obj = obj.toJSON();
|
|
5371
|
-
}
|
|
5372
|
-
if (typeof obj === "object" && obj !== null) {
|
|
5373
|
-
stack.push(obj);
|
|
5374
|
-
canonicalizedObj = {};
|
|
5375
|
-
replacementStack.push(canonicalizedObj);
|
|
5376
|
-
const sortedKeys = [];
|
|
5377
|
-
let key2;
|
|
5378
|
-
for (key2 in obj) {
|
|
5379
|
-
if (Object.prototype.hasOwnProperty.call(obj, key2)) {
|
|
5380
|
-
sortedKeys.push(key2);
|
|
5381
|
-
}
|
|
5382
|
-
}
|
|
5383
|
-
sortedKeys.sort();
|
|
5384
|
-
for (i = 0;i < sortedKeys.length; i += 1) {
|
|
5385
|
-
key2 = sortedKeys[i];
|
|
5386
|
-
canonicalizedObj[key2] = canonicalize(obj[key2], stack, replacementStack, replacer, key2);
|
|
5387
|
-
}
|
|
5388
|
-
stack.pop();
|
|
5389
|
-
replacementStack.pop();
|
|
4559
|
+
let status;
|
|
4560
|
+
if (isDelete) {
|
|
4561
|
+
status = "deleted";
|
|
4562
|
+
} else if (!diskExists) {
|
|
4563
|
+
status = "created";
|
|
4564
|
+
} else if (stagedHash === null) {
|
|
4565
|
+
status = "unchanged";
|
|
4566
|
+
} else if (stagedHash === canonicalHash) {
|
|
4567
|
+
status = "unchanged";
|
|
5390
4568
|
} else {
|
|
5391
|
-
|
|
5392
|
-
}
|
|
5393
|
-
return canonicalizedObj;
|
|
5394
|
-
}
|
|
5395
|
-
|
|
5396
|
-
// ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/array.js
|
|
5397
|
-
class ArrayDiff extends Diff {
|
|
5398
|
-
tokenize(value) {
|
|
5399
|
-
return value.slice();
|
|
5400
|
-
}
|
|
5401
|
-
join(value) {
|
|
5402
|
-
return value;
|
|
5403
|
-
}
|
|
5404
|
-
removeEmpty(value) {
|
|
5405
|
-
return value;
|
|
4569
|
+
status = "modified";
|
|
5406
4570
|
}
|
|
4571
|
+
return ok({
|
|
4572
|
+
kind: "file",
|
|
4573
|
+
path,
|
|
4574
|
+
configTier: tier,
|
|
4575
|
+
ownership: ownership ? { user: ownership.user, group: ownership.group, mode: ownership.mode } : null,
|
|
4576
|
+
canonicalHash,
|
|
4577
|
+
stagedHash,
|
|
4578
|
+
status
|
|
4579
|
+
});
|
|
5407
4580
|
}
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
} else {
|
|
5423
|
-
optionsObj = options;
|
|
5424
|
-
}
|
|
5425
|
-
if (typeof optionsObj.context === "undefined") {
|
|
5426
|
-
optionsObj.context = 4;
|
|
5427
|
-
}
|
|
5428
|
-
const context = optionsObj.context;
|
|
5429
|
-
if (optionsObj.newlineIsToken) {
|
|
5430
|
-
throw new Error("newlineIsToken may not be used with patch-generation functions, only with diffing functions");
|
|
4581
|
+
async function buildDirectory(ops, dirPath, tier) {
|
|
4582
|
+
const statResult = await ops.stat(dirPath);
|
|
4583
|
+
let diskExists = false;
|
|
4584
|
+
let ownership = null;
|
|
4585
|
+
if (statResult.ok) {
|
|
4586
|
+
if (statResult.value.isDirectory) {
|
|
4587
|
+
diskExists = true;
|
|
4588
|
+
ownership = statResult.value.ownership;
|
|
4589
|
+
}
|
|
4590
|
+
} else if (statResult.error.kind !== "not_found") {
|
|
4591
|
+
return err({
|
|
4592
|
+
kind: "build_failed",
|
|
4593
|
+
message: `stat failed for ${dirPath}: ${statResult.error.kind}`
|
|
4594
|
+
});
|
|
5431
4595
|
}
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
function diffLinesResultToPatch(diff) {
|
|
5442
|
-
if (!diff) {
|
|
5443
|
-
return;
|
|
5444
|
-
}
|
|
5445
|
-
diff.push({ value: "", lines: [] });
|
|
5446
|
-
function contextLines(lines) {
|
|
5447
|
-
return lines.map(function(entry) {
|
|
5448
|
-
return " " + entry;
|
|
4596
|
+
const staged = stagingPath(dirPath);
|
|
4597
|
+
const stagedStat = await ops.stat(staged);
|
|
4598
|
+
let dirDelete = false;
|
|
4599
|
+
if (stagedStat.ok && !stagedStat.value.isDirectory) {
|
|
4600
|
+
const content = await ops.readFile(staged);
|
|
4601
|
+
if (!content.ok) {
|
|
4602
|
+
return err({
|
|
4603
|
+
kind: "build_failed",
|
|
4604
|
+
message: `read failed for ${staged}: ${content.error.kind}`
|
|
5449
4605
|
});
|
|
5450
4606
|
}
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
for (let i = 0;i < diff.length; i++) {
|
|
5454
|
-
const current = diff[i], lines = current.lines || splitLines(current.value);
|
|
5455
|
-
current.lines = lines;
|
|
5456
|
-
if (current.added || current.removed) {
|
|
5457
|
-
if (!oldRangeStart) {
|
|
5458
|
-
const prev = diff[i - 1];
|
|
5459
|
-
oldRangeStart = oldLine;
|
|
5460
|
-
newRangeStart = newLine;
|
|
5461
|
-
if (prev) {
|
|
5462
|
-
curRange = context > 0 ? contextLines(prev.lines.slice(-context)) : [];
|
|
5463
|
-
oldRangeStart -= curRange.length;
|
|
5464
|
-
newRangeStart -= curRange.length;
|
|
5465
|
-
}
|
|
5466
|
-
}
|
|
5467
|
-
for (const line of lines) {
|
|
5468
|
-
curRange.push((current.added ? "+" : "-") + line);
|
|
5469
|
-
}
|
|
5470
|
-
if (current.added) {
|
|
5471
|
-
newLine += lines.length;
|
|
5472
|
-
} else {
|
|
5473
|
-
oldLine += lines.length;
|
|
5474
|
-
}
|
|
5475
|
-
} else {
|
|
5476
|
-
if (oldRangeStart) {
|
|
5477
|
-
if (lines.length <= context * 2 && i < diff.length - 2) {
|
|
5478
|
-
for (const line of contextLines(lines)) {
|
|
5479
|
-
curRange.push(line);
|
|
5480
|
-
}
|
|
5481
|
-
} else {
|
|
5482
|
-
const contextSize = Math.min(lines.length, context);
|
|
5483
|
-
for (const line of contextLines(lines.slice(0, contextSize))) {
|
|
5484
|
-
curRange.push(line);
|
|
5485
|
-
}
|
|
5486
|
-
const hunk = {
|
|
5487
|
-
oldStart: oldRangeStart,
|
|
5488
|
-
oldLines: oldLine - oldRangeStart + contextSize,
|
|
5489
|
-
newStart: newRangeStart,
|
|
5490
|
-
newLines: newLine - newRangeStart + contextSize,
|
|
5491
|
-
lines: curRange
|
|
5492
|
-
};
|
|
5493
|
-
hunks.push(hunk);
|
|
5494
|
-
oldRangeStart = 0;
|
|
5495
|
-
newRangeStart = 0;
|
|
5496
|
-
curRange = [];
|
|
5497
|
-
}
|
|
5498
|
-
}
|
|
5499
|
-
oldLine += lines.length;
|
|
5500
|
-
newLine += lines.length;
|
|
5501
|
-
}
|
|
5502
|
-
}
|
|
5503
|
-
for (const hunk of hunks) {
|
|
5504
|
-
for (let i = 0;i < hunk.lines.length; i++) {
|
|
5505
|
-
if (hunk.lines[i].endsWith(`
|
|
5506
|
-
`)) {
|
|
5507
|
-
hunk.lines[i] = hunk.lines[i].slice(0, -1);
|
|
5508
|
-
} else {
|
|
5509
|
-
hunk.lines.splice(i + 1, 0, "\");
|
|
5510
|
-
i++;
|
|
5511
|
-
}
|
|
5512
|
-
}
|
|
5513
|
-
}
|
|
5514
|
-
return {
|
|
5515
|
-
oldFileName,
|
|
5516
|
-
newFileName,
|
|
5517
|
-
oldHeader,
|
|
5518
|
-
newHeader,
|
|
5519
|
-
hunks
|
|
5520
|
-
};
|
|
5521
|
-
}
|
|
5522
|
-
}
|
|
5523
|
-
function formatPatch(patch, headerOptions) {
|
|
5524
|
-
if (!headerOptions) {
|
|
5525
|
-
headerOptions = INCLUDE_HEADERS;
|
|
5526
|
-
}
|
|
5527
|
-
if (Array.isArray(patch)) {
|
|
5528
|
-
if (patch.length > 1 && !headerOptions.includeFileHeaders) {
|
|
5529
|
-
throw new Error("Cannot omit file headers on a multi-file patch. " + "(The result would be unparseable; how would a tool trying to apply " + "the patch know which changes are to which file?)");
|
|
4607
|
+
if (isDeleteSentinel(content.value)) {
|
|
4608
|
+
dirDelete = true;
|
|
5530
4609
|
}
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
ret.push("Index: " + patch.oldFileName);
|
|
5537
|
-
}
|
|
5538
|
-
if (headerOptions.includeUnderline) {
|
|
5539
|
-
ret.push("===================================================================");
|
|
5540
|
-
}
|
|
5541
|
-
if (headerOptions.includeFileHeaders) {
|
|
5542
|
-
ret.push("--- " + patch.oldFileName + (typeof patch.oldHeader === "undefined" ? "" : "\t" + patch.oldHeader));
|
|
5543
|
-
ret.push("+++ " + patch.newFileName + (typeof patch.newHeader === "undefined" ? "" : "\t" + patch.newHeader));
|
|
5544
|
-
}
|
|
5545
|
-
for (let i = 0;i < patch.hunks.length; i++) {
|
|
5546
|
-
const hunk = patch.hunks[i];
|
|
5547
|
-
if (hunk.oldLines === 0) {
|
|
5548
|
-
hunk.oldStart -= 1;
|
|
5549
|
-
}
|
|
5550
|
-
if (hunk.newLines === 0) {
|
|
5551
|
-
hunk.newStart -= 1;
|
|
5552
|
-
}
|
|
5553
|
-
ret.push("@@ -" + hunk.oldStart + "," + hunk.oldLines + " +" + hunk.newStart + "," + hunk.newLines + " @@");
|
|
5554
|
-
for (const line of hunk.lines) {
|
|
5555
|
-
ret.push(line);
|
|
5556
|
-
}
|
|
5557
|
-
}
|
|
5558
|
-
return ret.join(`
|
|
5559
|
-
`) + `
|
|
5560
|
-
`;
|
|
5561
|
-
}
|
|
5562
|
-
function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
|
|
5563
|
-
if (typeof options === "function") {
|
|
5564
|
-
options = { callback: options };
|
|
5565
|
-
}
|
|
5566
|
-
if (!(options === null || options === undefined ? undefined : options.callback)) {
|
|
5567
|
-
const patchObj = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options);
|
|
5568
|
-
if (!patchObj) {
|
|
5569
|
-
return;
|
|
5570
|
-
}
|
|
5571
|
-
return formatPatch(patchObj, options === null || options === undefined ? undefined : options.headerOptions);
|
|
5572
|
-
} else {
|
|
5573
|
-
const { callback } = options;
|
|
5574
|
-
structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, Object.assign(Object.assign({}, options), { callback: (patchObj) => {
|
|
5575
|
-
if (!patchObj) {
|
|
5576
|
-
callback(undefined);
|
|
5577
|
-
} else {
|
|
5578
|
-
callback(formatPatch(patchObj, options.headerOptions));
|
|
5579
|
-
}
|
|
5580
|
-
} }));
|
|
4610
|
+
} else if (!stagedStat.ok && stagedStat.error.kind !== "not_found") {
|
|
4611
|
+
return err({
|
|
4612
|
+
kind: "build_failed",
|
|
4613
|
+
message: `stat failed for ${staged}: ${stagedStat.error.kind}`
|
|
4614
|
+
});
|
|
5581
4615
|
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
const result = text.split(`
|
|
5587
|
-
`).map((line) => line + `
|
|
5588
|
-
`);
|
|
5589
|
-
if (hasTrailingNl) {
|
|
5590
|
-
result.pop();
|
|
5591
|
-
} else {
|
|
5592
|
-
result.push(result.pop().slice(0, -1));
|
|
4616
|
+
if (!diskExists && !dirDelete) {
|
|
4617
|
+
const stagedDirExists = stagedStat.ok && stagedStat.value.isDirectory;
|
|
4618
|
+
if (!stagedDirExists)
|
|
4619
|
+
return ok(null);
|
|
5593
4620
|
}
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
async function diff(options) {
|
|
5600
|
-
const { ops, config, files: filterFiles } = options;
|
|
5601
|
-
const stagingExists = await ops.exists(STAGING_DIR);
|
|
5602
|
-
if (!stagingExists.ok) {
|
|
5603
|
-
return err({ kind: "read_failed", path: STAGING_DIR, message: stagingExists.error.message });
|
|
5604
|
-
}
|
|
5605
|
-
if (!stagingExists.value) {
|
|
5606
|
-
return err({ kind: "no_staging" });
|
|
5607
|
-
}
|
|
5608
|
-
const resolved = await resolvePatterns(ops, config.vault);
|
|
5609
|
-
if (!resolved.ok) {
|
|
5610
|
-
return err({ kind: "read_failed", path: "glob", message: resolved.error.message });
|
|
5611
|
-
}
|
|
5612
|
-
let vaultFiles = resolved.value;
|
|
5613
|
-
if (filterFiles && filterFiles.length > 0) {
|
|
5614
|
-
const filterSet = new Set(filterFiles);
|
|
5615
|
-
vaultFiles = vaultFiles.filter((p) => filterSet.has(p));
|
|
5616
|
-
}
|
|
5617
|
-
const fileDiffs = [];
|
|
5618
|
-
for (const path of vaultFiles) {
|
|
5619
|
-
const stagingPath = `${STAGING_DIR}/${path}`;
|
|
5620
|
-
const [vaultExists, stagingFileExists] = await Promise.all([
|
|
5621
|
-
ops.exists(path),
|
|
5622
|
-
ops.exists(stagingPath)
|
|
5623
|
-
]);
|
|
5624
|
-
if (!vaultExists.ok) {
|
|
5625
|
-
return err({ kind: "read_failed", path, message: vaultExists.error.message });
|
|
5626
|
-
}
|
|
5627
|
-
if (!stagingFileExists.ok) {
|
|
4621
|
+
const children = [];
|
|
4622
|
+
const diskChildren = new Set;
|
|
4623
|
+
if (diskExists) {
|
|
4624
|
+
const listResult = await ops.listDir(dirPath);
|
|
4625
|
+
if (!listResult.ok) {
|
|
5628
4626
|
return err({
|
|
5629
|
-
kind: "
|
|
5630
|
-
|
|
5631
|
-
message: stagingFileExists.error.message
|
|
5632
|
-
});
|
|
5633
|
-
}
|
|
5634
|
-
if (vaultExists.value && !stagingFileExists.value) {
|
|
5635
|
-
const vaultHash2 = await ops.hashFile(path);
|
|
5636
|
-
fileDiffs.push({
|
|
5637
|
-
path,
|
|
5638
|
-
status: "deleted",
|
|
5639
|
-
protectedHash: vaultHash2.ok ? vaultHash2.value : undefined
|
|
5640
|
-
});
|
|
5641
|
-
continue;
|
|
5642
|
-
}
|
|
5643
|
-
if (!vaultExists.value && stagingFileExists.value) {
|
|
5644
|
-
const newHash = await ops.hashFile(stagingPath);
|
|
5645
|
-
fileDiffs.push({
|
|
5646
|
-
path,
|
|
5647
|
-
status: "vault_missing",
|
|
5648
|
-
stagedHash: newHash.ok ? newHash.value : undefined
|
|
5649
|
-
});
|
|
5650
|
-
continue;
|
|
5651
|
-
}
|
|
5652
|
-
if (!vaultExists.value && !stagingFileExists.value) {
|
|
5653
|
-
fileDiffs.push({ path, status: "staging_missing" });
|
|
5654
|
-
continue;
|
|
5655
|
-
}
|
|
5656
|
-
const [vaultHash, stagingHash] = await Promise.all([
|
|
5657
|
-
ops.hashFile(path),
|
|
5658
|
-
ops.hashFile(stagingPath)
|
|
5659
|
-
]);
|
|
5660
|
-
if (!vaultHash.ok) {
|
|
5661
|
-
return err({ kind: "read_failed", path, message: "hash failed" });
|
|
5662
|
-
}
|
|
5663
|
-
if (!stagingHash.ok) {
|
|
5664
|
-
return err({ kind: "read_failed", path: stagingPath, message: "hash failed" });
|
|
5665
|
-
}
|
|
5666
|
-
if (vaultHash.value === stagingHash.value) {
|
|
5667
|
-
fileDiffs.push({
|
|
5668
|
-
path,
|
|
5669
|
-
status: "unchanged",
|
|
5670
|
-
protectedHash: vaultHash.value,
|
|
5671
|
-
stagedHash: stagingHash.value
|
|
4627
|
+
kind: "build_failed",
|
|
4628
|
+
message: `listDir failed for ${dirPath}: ${listResult.error.kind}`
|
|
5672
4629
|
});
|
|
5673
|
-
continue;
|
|
5674
4630
|
}
|
|
5675
|
-
const
|
|
5676
|
-
|
|
5677
|
-
ops.readFile(stagingPath)
|
|
5678
|
-
]);
|
|
5679
|
-
if (!vaultContent.ok) {
|
|
5680
|
-
return err({ kind: "read_failed", path, message: "read failed" });
|
|
4631
|
+
for (const childPath of listResult.value) {
|
|
4632
|
+
diskChildren.add(childPath);
|
|
5681
4633
|
}
|
|
5682
|
-
if (!stagingContent.ok) {
|
|
5683
|
-
return err({ kind: "read_failed", path: stagingPath, message: "read failed" });
|
|
5684
|
-
}
|
|
5685
|
-
const unifiedDiff = createTwoFilesPatch(`a/${path}`, `b/${path}`, vaultContent.value, stagingContent.value);
|
|
5686
|
-
fileDiffs.push({
|
|
5687
|
-
path,
|
|
5688
|
-
status: "modified",
|
|
5689
|
-
diff: unifiedDiff,
|
|
5690
|
-
protectedHash: vaultHash.value,
|
|
5691
|
-
stagedHash: stagingHash.value
|
|
5692
|
-
});
|
|
5693
4634
|
}
|
|
5694
|
-
const
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
4635
|
+
const stagedChildren = new Set;
|
|
4636
|
+
if (!dirDelete) {
|
|
4637
|
+
if (stagedStat.ok && stagedStat.value.isDirectory) {
|
|
4638
|
+
const stagedList = await ops.listDir(staged);
|
|
4639
|
+
if (!stagedList.ok) {
|
|
4640
|
+
return err({
|
|
4641
|
+
kind: "build_failed",
|
|
4642
|
+
message: `listDir failed for ${staged}: ${stagedList.error.kind}`
|
|
4643
|
+
});
|
|
4644
|
+
}
|
|
4645
|
+
for (const stagedChildPath of stagedList.value) {
|
|
4646
|
+
const canonical = stagedChildPath.slice(STAGING_DIR.length + 1);
|
|
4647
|
+
stagedChildren.add(canonical);
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
5698
4650
|
}
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
4651
|
+
const allChildren = new Set([...diskChildren, ...stagedChildren]);
|
|
4652
|
+
for (const childPath of [...allChildren].sort()) {
|
|
4653
|
+
if (dirDelete) {
|
|
4654
|
+
const childStat = await ops.stat(childPath);
|
|
4655
|
+
let childOwnership = null;
|
|
4656
|
+
let childHash = null;
|
|
4657
|
+
if (childStat.ok) {
|
|
4658
|
+
childOwnership = {
|
|
4659
|
+
user: childStat.value.ownership.user,
|
|
4660
|
+
group: childStat.value.ownership.group,
|
|
4661
|
+
mode: childStat.value.ownership.mode
|
|
4662
|
+
};
|
|
4663
|
+
const hashResult = await ops.hashFile(childPath);
|
|
4664
|
+
if (!hashResult.ok) {
|
|
4665
|
+
return err({
|
|
4666
|
+
kind: "build_failed",
|
|
4667
|
+
message: `hash failed for ${childPath}: ${hashResult.error.kind}`
|
|
4668
|
+
});
|
|
4669
|
+
}
|
|
4670
|
+
childHash = hashResult.value;
|
|
4671
|
+
} else if (childStat.error.kind !== "not_found") {
|
|
4672
|
+
return err({
|
|
4673
|
+
kind: "build_failed",
|
|
4674
|
+
message: `stat failed for ${childPath}: ${childStat.error.kind}`
|
|
4675
|
+
});
|
|
4676
|
+
}
|
|
4677
|
+
children.push({
|
|
4678
|
+
kind: "file",
|
|
4679
|
+
path: childPath,
|
|
4680
|
+
configTier: tier,
|
|
4681
|
+
ownership: childOwnership,
|
|
4682
|
+
canonicalHash: childHash,
|
|
4683
|
+
stagedHash: null,
|
|
4684
|
+
status: "deleted"
|
|
4685
|
+
});
|
|
5707
4686
|
} else {
|
|
5708
|
-
|
|
4687
|
+
const childResult = await buildFile(ops, childPath, tier);
|
|
4688
|
+
if (!childResult.ok)
|
|
4689
|
+
return childResult;
|
|
4690
|
+
if (childResult.value)
|
|
4691
|
+
children.push(childResult.value);
|
|
5709
4692
|
}
|
|
5710
4693
|
}
|
|
5711
|
-
|
|
4694
|
+
if (!diskExists && children.length === 0)
|
|
4695
|
+
return ok(null);
|
|
4696
|
+
return ok({
|
|
4697
|
+
kind: "directory",
|
|
4698
|
+
path: dirPath,
|
|
4699
|
+
configTier: tier,
|
|
4700
|
+
ownership: ownership ? { user: ownership.user, group: ownership.group, mode: ownership.mode } : null,
|
|
4701
|
+
deleted: dirDelete,
|
|
4702
|
+
children
|
|
4703
|
+
});
|
|
5712
4704
|
}
|
|
5713
|
-
// ../core/src/
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
}
|
|
5720
|
-
// ../core/src/
|
|
5721
|
-
function
|
|
4705
|
+
// ../core/src/sdk/config.ts
|
|
4706
|
+
function patternsForTier(config, tier) {
|
|
4707
|
+
return Object.entries(config.files).filter(([, t]) => t === tier).map(([pattern]) => pattern);
|
|
4708
|
+
}
|
|
4709
|
+
function protectPatterns(config) {
|
|
4710
|
+
return patternsForTier(config, "protect");
|
|
4711
|
+
}
|
|
4712
|
+
// ../core/src/sdk/protect-check.ts
|
|
4713
|
+
function isProtectedFile(protectFiles, filePath) {
|
|
5722
4714
|
const norm = normalizePath(filePath);
|
|
5723
|
-
return
|
|
5724
|
-
const
|
|
5725
|
-
|
|
5726
|
-
return matchGlob(normPattern, norm);
|
|
5727
|
-
}
|
|
5728
|
-
return norm === normPattern;
|
|
4715
|
+
return protectFiles.some((entry) => {
|
|
4716
|
+
const normEntry = normalizePath(entry);
|
|
4717
|
+
return norm === normEntry || norm.startsWith(normEntry + "/");
|
|
5729
4718
|
});
|
|
5730
4719
|
}
|
|
5731
4720
|
function normalizePath(p) {
|
|
@@ -5734,16 +4723,18 @@ function normalizePath(p) {
|
|
|
5734
4723
|
s = s.slice(2);
|
|
5735
4724
|
if (s.startsWith("/"))
|
|
5736
4725
|
s = s.slice(1);
|
|
4726
|
+
if (s.endsWith("/"))
|
|
4727
|
+
s = s.slice(0, -1);
|
|
5737
4728
|
return s;
|
|
5738
4729
|
}
|
|
5739
4730
|
// src/guard.ts
|
|
5740
|
-
import
|
|
5741
|
-
var WRITE_TOOLS = new Set(["
|
|
4731
|
+
import path from "node:path";
|
|
4732
|
+
var WRITE_TOOLS = new Set(["write", "edit"]);
|
|
5742
4733
|
var PATH_KEYS = ["file_path", "path", "file"];
|
|
5743
|
-
var STAGING_PREFIX = ".soulguard/staging/";
|
|
5744
4734
|
function guardToolCall(toolName, params, options) {
|
|
5745
|
-
if (!WRITE_TOOLS.has(toolName))
|
|
4735
|
+
if (!WRITE_TOOLS.has(toolName.toLowerCase())) {
|
|
5746
4736
|
return { blocked: false };
|
|
4737
|
+
}
|
|
5747
4738
|
let targetPath;
|
|
5748
4739
|
for (const key of PATH_KEYS) {
|
|
5749
4740
|
const v = params[key];
|
|
@@ -5752,138 +4743,59 @@ function guardToolCall(toolName, params, options) {
|
|
|
5752
4743
|
break;
|
|
5753
4744
|
}
|
|
5754
4745
|
}
|
|
4746
|
+
if (targetPath && path.isAbsolute(targetPath)) {
|
|
4747
|
+
targetPath = path.relative(options.stateDir, targetPath);
|
|
4748
|
+
}
|
|
5755
4749
|
if (!targetPath)
|
|
5756
4750
|
return { blocked: false };
|
|
5757
|
-
|
|
5758
|
-
if (norm.startsWith(STAGING_PREFIX))
|
|
4751
|
+
if (isStagingPath(targetPath))
|
|
5759
4752
|
return { blocked: false };
|
|
5760
|
-
if (!
|
|
4753
|
+
if (!isProtectedFile(options.protectFiles, targetPath))
|
|
5761
4754
|
return { blocked: false };
|
|
5762
|
-
const fileName = basename(targetPath);
|
|
5763
4755
|
return {
|
|
5764
4756
|
blocked: true,
|
|
5765
|
-
reason:
|
|
4757
|
+
reason: [
|
|
4758
|
+
`${targetPath} is protected by soulguard.`,
|
|
4759
|
+
`To propose changes, run \`soulguard stage ${targetPath}\` to create a working copy,`,
|
|
4760
|
+
`then edit the staged file at ${stagingPath(targetPath)}.`,
|
|
4761
|
+
`Run \`soulguard diff\` to review your changes.`,
|
|
4762
|
+
`Your owner will review and apply the changes.`
|
|
4763
|
+
].join(" ")
|
|
5766
4764
|
};
|
|
5767
4765
|
}
|
|
5768
4766
|
|
|
5769
4767
|
// src/plugin.ts
|
|
5770
|
-
var
|
|
5771
|
-
vault: ["openclaw.json", "soulguard.json"],
|
|
5772
|
-
ledger: []
|
|
5773
|
-
};
|
|
4768
|
+
var PKG_VERSION = "0.2.1";
|
|
5774
4769
|
var PLUGIN_DESCRIPTION = "Identity protection for AI agents";
|
|
5775
4770
|
function createSoulguardPlugin(options) {
|
|
5776
4771
|
return {
|
|
5777
4772
|
id: "soulguard",
|
|
5778
4773
|
name: "Soulguard",
|
|
5779
4774
|
description: PLUGIN_DESCRIPTION,
|
|
5780
|
-
version:
|
|
4775
|
+
version: PKG_VERSION,
|
|
5781
4776
|
activate(api) {
|
|
5782
|
-
const
|
|
4777
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() ?? join2(os.homedir(), ".openclaw");
|
|
5783
4778
|
const configFile = options?.configPath ?? "soulguard.json";
|
|
5784
|
-
const configPath =
|
|
5785
|
-
let
|
|
5786
|
-
let vaultFiles;
|
|
4779
|
+
const configPath = join2(stateDir, configFile);
|
|
4780
|
+
let protectFiles;
|
|
5787
4781
|
try {
|
|
5788
4782
|
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
5789
|
-
|
|
5790
|
-
vaultFiles = config.vault;
|
|
4783
|
+
protectFiles = protectPatterns(parseConfig(raw));
|
|
5791
4784
|
} catch {
|
|
5792
|
-
config
|
|
5793
|
-
|
|
5794
|
-
api.logger?.warn("soulguard: no soulguard.json found — using OpenClaw defaults");
|
|
4785
|
+
api.logger?.warn(`soulguard: no config found at ${configPath} — plugin inactive`);
|
|
4786
|
+
return;
|
|
5795
4787
|
}
|
|
5796
|
-
if (
|
|
4788
|
+
if (protectFiles.length === 0)
|
|
5797
4789
|
return;
|
|
5798
|
-
|
|
5799
|
-
api.registerTool({
|
|
5800
|
-
name: "soulguard_status",
|
|
5801
|
-
description: "Check soulguard protection status of vault and ledger files",
|
|
5802
|
-
parameters: { type: "object", properties: {}, required: [] },
|
|
5803
|
-
async execute(_id, _params) {
|
|
5804
|
-
const ops = createOps();
|
|
5805
|
-
const result = await status({
|
|
5806
|
-
config,
|
|
5807
|
-
expectedVaultOwnership: { user: "soulguardian", group: "soulguard", mode: "444" },
|
|
5808
|
-
expectedLedgerOwnership: { user: "agent", group: "staff", mode: "644" },
|
|
5809
|
-
ops
|
|
5810
|
-
});
|
|
5811
|
-
if (!result.ok) {
|
|
5812
|
-
return { content: [{ type: "text", text: "Status check failed" }] };
|
|
5813
|
-
}
|
|
5814
|
-
const lines = ["Soulguard Status:", ""];
|
|
5815
|
-
for (const f of [...result.value.vault, ...result.value.ledger]) {
|
|
5816
|
-
if (f.status === "ok")
|
|
5817
|
-
lines.push(` ✅ ${f.file.path}`);
|
|
5818
|
-
else if (f.status === "drifted")
|
|
5819
|
-
lines.push(` ⚠️ ${f.file.path} — ${f.issues.map((i) => i.kind).join(", ")}`);
|
|
5820
|
-
else if (f.status === "missing")
|
|
5821
|
-
lines.push(` ❌ ${f.path} — missing`);
|
|
5822
|
-
else if (f.status === "error")
|
|
5823
|
-
lines.push(` ❌ ${f.path} — error: ${f.error.kind}`);
|
|
5824
|
-
}
|
|
5825
|
-
if (result.value.issues.length === 0)
|
|
5826
|
-
lines.push("", "All files ok.");
|
|
5827
|
-
else
|
|
5828
|
-
lines.push("", `${result.value.issues.length} issue(s) found.`);
|
|
5829
|
-
return { content: [{ type: "text", text: lines.join(`
|
|
5830
|
-
`) }] };
|
|
5831
|
-
}
|
|
5832
|
-
}, { optional: true });
|
|
5833
|
-
api.registerTool({
|
|
5834
|
-
name: "soulguard_diff",
|
|
5835
|
-
description: "Show differences between vault files and their staging copies",
|
|
5836
|
-
parameters: {
|
|
5837
|
-
type: "object",
|
|
5838
|
-
properties: {
|
|
5839
|
-
files: {
|
|
5840
|
-
type: "array",
|
|
5841
|
-
items: { type: "string" },
|
|
5842
|
-
description: "Specific files to diff (default: all vault files)"
|
|
5843
|
-
}
|
|
5844
|
-
},
|
|
5845
|
-
required: []
|
|
5846
|
-
},
|
|
5847
|
-
async execute(_id, params) {
|
|
5848
|
-
const ops = createOps();
|
|
5849
|
-
const files = Array.isArray(params.files) ? params.files : undefined;
|
|
5850
|
-
const result = await diff({ ops, config, files });
|
|
5851
|
-
if (!result.ok) {
|
|
5852
|
-
return {
|
|
5853
|
-
content: [{ type: "text", text: `Diff failed: ${result.error.kind}` }]
|
|
5854
|
-
};
|
|
5855
|
-
}
|
|
5856
|
-
if (!result.value.hasChanges) {
|
|
5857
|
-
return {
|
|
5858
|
-
content: [
|
|
5859
|
-
{ type: "text", text: "No differences — staging matches vault." }
|
|
5860
|
-
]
|
|
5861
|
-
};
|
|
5862
|
-
}
|
|
5863
|
-
const lines = result.value.files.filter((d) => d.status === "modified" && d.diff).map((d) => `--- ${d.path}
|
|
5864
|
-
${d.diff}`);
|
|
5865
|
-
let text = lines.join(`
|
|
5866
|
-
|
|
5867
|
-
`) || "No modified files.";
|
|
5868
|
-
if (result.value.approvalHash) {
|
|
5869
|
-
text += `
|
|
5870
|
-
|
|
5871
|
-
────────────────────────────────────────
|
|
5872
|
-
Approval hash: ${result.value.approvalHash}
|
|
5873
|
-
To approve: soulguard approve --hash ${result.value.approvalHash}`;
|
|
5874
|
-
}
|
|
5875
|
-
return { content: [{ type: "text", text }] };
|
|
5876
|
-
}
|
|
5877
|
-
}, { optional: true });
|
|
5878
|
-
const hookFn = api.registerHook ?? api.on;
|
|
5879
|
-
hookFn("before_tool_call", (...args) => {
|
|
4790
|
+
api.on("before_tool_call", (...args) => {
|
|
5880
4791
|
const event = args[0];
|
|
5881
4792
|
if (!event || typeof event !== "object" || !("toolName" in event)) {
|
|
5882
4793
|
return;
|
|
5883
4794
|
}
|
|
5884
4795
|
const e = event;
|
|
5885
4796
|
const result = guardToolCall(e.toolName, e.params, {
|
|
5886
|
-
|
|
4797
|
+
protectFiles,
|
|
4798
|
+
stateDir
|
|
5887
4799
|
});
|
|
5888
4800
|
if (result.blocked) {
|
|
5889
4801
|
return { block: true, blockReason: result.reason };
|
|
@@ -5893,14 +4805,28 @@ To approve: soulguard approve --hash ${result.value.approvalHash}`;
|
|
|
5893
4805
|
}
|
|
5894
4806
|
};
|
|
5895
4807
|
}
|
|
4808
|
+
// src/context.ts
|
|
4809
|
+
async function getPendingChanges(options) {
|
|
4810
|
+
const treeResult = await StateTree.build(options);
|
|
4811
|
+
if (!treeResult.ok)
|
|
4812
|
+
return { files: [] };
|
|
4813
|
+
return { files: treeResult.value.changedFiles().map((f) => f.path) };
|
|
4814
|
+
}
|
|
4815
|
+
async function buildPendingChangesContext(options) {
|
|
4816
|
+
const { files } = await getPendingChanges(options);
|
|
4817
|
+
if (files.length === 0)
|
|
4818
|
+
return;
|
|
4819
|
+
const fileList = files.join(", ");
|
|
4820
|
+
return `[Soulguard] ${files.length} protected file(s) have pending staged changes: ${fileList}. ` + `Use \`soulguard diff\` to review. Ask your owner to apply changes, ` + `or use \`soulguard reset\` to discard them.`;
|
|
4821
|
+
}
|
|
4822
|
+
|
|
5896
4823
|
// src/index.ts
|
|
5897
4824
|
var src_default = createSoulguardPlugin();
|
|
5898
4825
|
export {
|
|
5899
4826
|
templates,
|
|
5900
|
-
relaxedTemplate,
|
|
5901
|
-
paranoidTemplate,
|
|
5902
4827
|
guardToolCall,
|
|
5903
|
-
|
|
4828
|
+
getPendingChanges,
|
|
5904
4829
|
src_default as default,
|
|
5905
|
-
createSoulguardPlugin
|
|
4830
|
+
createSoulguardPlugin,
|
|
4831
|
+
buildPendingChangesContext
|
|
5906
4832
|
};
|