soulguard 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/bin/soulguard.js +21 -0
- package/dist/index.js +1816 -823
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
var __create = Object.create;
|
|
3
2
|
var __getProtoOf = Object.getPrototypeOf;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
@@ -25,7 +24,6 @@ var __export = (target, all) => {
|
|
|
25
24
|
set: (newValue) => all[name] = () => newValue
|
|
26
25
|
});
|
|
27
26
|
};
|
|
28
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
29
27
|
|
|
30
28
|
// ../../node_modules/.bun/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
31
29
|
var require_picocolors = __commonJS((exports, module) => {
|
|
@@ -97,15 +95,14 @@ var require_picocolors = __commonJS((exports, module) => {
|
|
|
97
95
|
module.exports.createColors = createColors;
|
|
98
96
|
});
|
|
99
97
|
|
|
100
|
-
// ../core/src/result.ts
|
|
98
|
+
// ../core/src/util/result.ts
|
|
101
99
|
function ok(value) {
|
|
102
100
|
return { ok: true, value };
|
|
103
101
|
}
|
|
104
102
|
function err(error) {
|
|
105
103
|
return { ok: false, error };
|
|
106
104
|
}
|
|
107
|
-
|
|
108
|
-
// ../core/src/types.ts
|
|
105
|
+
// ../core/src/util/types.ts
|
|
109
106
|
function formatIssue(issue) {
|
|
110
107
|
switch (issue.kind) {
|
|
111
108
|
case "wrong_owner":
|
|
@@ -4091,82 +4088,29 @@ var coerce = {
|
|
|
4091
4088
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
4092
4089
|
};
|
|
4093
4090
|
var NEVER = INVALID;
|
|
4094
|
-
// ../core/src/schema.ts
|
|
4091
|
+
// ../core/src/sdk/schema.ts
|
|
4092
|
+
var tierSchema = exports_external.enum(["protect", "watch"]);
|
|
4093
|
+
var ownershipSchema = exports_external.object({
|
|
4094
|
+
user: exports_external.string(),
|
|
4095
|
+
group: exports_external.string(),
|
|
4096
|
+
mode: exports_external.string()
|
|
4097
|
+
});
|
|
4098
|
+
var daemonConfigSchema = exports_external.object({
|
|
4099
|
+
channel: exports_external.string()
|
|
4100
|
+
}).passthrough();
|
|
4095
4101
|
var soulguardConfigSchema = exports_external.object({
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4102
|
+
version: exports_external.literal(1),
|
|
4103
|
+
guardian: exports_external.string(),
|
|
4104
|
+
files: exports_external.record(exports_external.string(), tierSchema),
|
|
4105
|
+
git: exports_external.boolean().optional(),
|
|
4106
|
+
defaultOwnership: ownershipSchema.optional(),
|
|
4107
|
+
daemon: daemonConfigSchema.optional()
|
|
4099
4108
|
});
|
|
4100
4109
|
function parseConfig(raw) {
|
|
4101
4110
|
return soulguardConfigSchema.parse(raw);
|
|
4102
4111
|
}
|
|
4103
|
-
// ../core/src/system-ops.ts
|
|
4104
|
-
async function getFileInfo(path, ops) {
|
|
4105
|
-
const statResult = await ops.stat(path);
|
|
4106
|
-
if (!statResult.ok)
|
|
4107
|
-
return statResult;
|
|
4108
|
-
const hashResult = await ops.hashFile(path);
|
|
4109
|
-
if (!hashResult.ok)
|
|
4110
|
-
return hashResult;
|
|
4111
|
-
return ok({
|
|
4112
|
-
path,
|
|
4113
|
-
ownership: statResult.value.ownership,
|
|
4114
|
-
hash: hashResult.value
|
|
4115
|
-
});
|
|
4116
|
-
}
|
|
4117
|
-
// ../core/src/system-ops-mock.ts
|
|
4112
|
+
// ../core/src/util/system-ops-mock.ts
|
|
4118
4113
|
import { resolve } from "node:path";
|
|
4119
|
-
|
|
4120
|
-
// ../core/src/glob.ts
|
|
4121
|
-
function isGlob(path) {
|
|
4122
|
-
return path.includes("*");
|
|
4123
|
-
}
|
|
4124
|
-
function createGlobMatcher(pattern) {
|
|
4125
|
-
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4126
|
-
let normalized = pattern;
|
|
4127
|
-
normalized = normalized.replace(/\/\*\*\//g, "<GLOBSTAR>");
|
|
4128
|
-
normalized = normalized.replace(/^\*\*\//, "<GLOBSTAR_PREFIX>");
|
|
4129
|
-
normalized = normalized.replace(/\/\*\*$/, "<GLOBSTAR_SUFFIX>");
|
|
4130
|
-
normalized = normalized.replace(/^\*\*$/, "<GLOBSTAR_ALL>");
|
|
4131
|
-
const regexStr = normalized.split(/(<GLOBSTAR(?:_PREFIX|_SUFFIX|_ALL)?>)/).map((part) => {
|
|
4132
|
-
if (part === "<GLOBSTAR>")
|
|
4133
|
-
return "/(?:.+/)?";
|
|
4134
|
-
if (part === "<GLOBSTAR_PREFIX>")
|
|
4135
|
-
return "(?:.+/)?";
|
|
4136
|
-
if (part === "<GLOBSTAR_SUFFIX>")
|
|
4137
|
-
return "(?:/.*)?";
|
|
4138
|
-
if (part === "<GLOBSTAR_ALL>")
|
|
4139
|
-
return ".*";
|
|
4140
|
-
return part.split("*").map(escape).join("[^/]*");
|
|
4141
|
-
}).join("");
|
|
4142
|
-
const regex = new RegExp(`^${regexStr}$`);
|
|
4143
|
-
return (path) => regex.test(path);
|
|
4144
|
-
}
|
|
4145
|
-
function matchGlob(pattern, path) {
|
|
4146
|
-
return createGlobMatcher(pattern)(path);
|
|
4147
|
-
}
|
|
4148
|
-
async function resolvePatterns(ops, patterns) {
|
|
4149
|
-
const files = new Set;
|
|
4150
|
-
for (const pattern of patterns) {
|
|
4151
|
-
if (isGlob(pattern)) {
|
|
4152
|
-
const result = await ops.glob(pattern);
|
|
4153
|
-
if (!result.ok) {
|
|
4154
|
-
return err(result.error);
|
|
4155
|
-
}
|
|
4156
|
-
for (const match of result.value) {
|
|
4157
|
-
const statResult = await ops.stat(match);
|
|
4158
|
-
if (statResult.ok && !statResult.value.isDirectory) {
|
|
4159
|
-
files.add(match);
|
|
4160
|
-
}
|
|
4161
|
-
}
|
|
4162
|
-
} else {
|
|
4163
|
-
files.add(pattern);
|
|
4164
|
-
}
|
|
4165
|
-
}
|
|
4166
|
-
return ok([...files].sort());
|
|
4167
|
-
}
|
|
4168
|
-
|
|
4169
|
-
// ../core/src/system-ops-mock.ts
|
|
4170
4114
|
class MockSystemOps {
|
|
4171
4115
|
workspace;
|
|
4172
4116
|
files = new Map;
|
|
@@ -4175,10 +4119,21 @@ class MockSystemOps {
|
|
|
4175
4119
|
ops = [];
|
|
4176
4120
|
failingExecs = new Set;
|
|
4177
4121
|
failingDeletes = new Set;
|
|
4122
|
+
failingStats = new Set;
|
|
4123
|
+
failingHashes = new Set;
|
|
4124
|
+
failingListDirs = new Set;
|
|
4125
|
+
failingReads = new Set;
|
|
4178
4126
|
execCallCounts = new Map;
|
|
4179
4127
|
execFailOnCall = new Map;
|
|
4180
4128
|
constructor(workspace) {
|
|
4181
4129
|
this.workspace = workspace;
|
|
4130
|
+
this.files.set(workspace, {
|
|
4131
|
+
content: "",
|
|
4132
|
+
owner: "root",
|
|
4133
|
+
group: "root",
|
|
4134
|
+
mode: "755",
|
|
4135
|
+
isDirectory: true
|
|
4136
|
+
});
|
|
4182
4137
|
}
|
|
4183
4138
|
resolve(path) {
|
|
4184
4139
|
return resolve(this.workspace, path);
|
|
@@ -4191,6 +4146,15 @@ class MockSystemOps {
|
|
|
4191
4146
|
mode: opts.mode ?? "644"
|
|
4192
4147
|
});
|
|
4193
4148
|
}
|
|
4149
|
+
addDirectory(path, opts = {}) {
|
|
4150
|
+
this.files.set(this.resolve(path), {
|
|
4151
|
+
content: "",
|
|
4152
|
+
owner: opts.owner ?? "unknown",
|
|
4153
|
+
group: opts.group ?? "unknown",
|
|
4154
|
+
mode: opts.mode ?? "755",
|
|
4155
|
+
isDirectory: true
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4194
4158
|
addUser(name) {
|
|
4195
4159
|
this.users.add(name);
|
|
4196
4160
|
}
|
|
@@ -4204,6 +4168,9 @@ class MockSystemOps {
|
|
|
4204
4168
|
return ok(this.groups.has(name));
|
|
4205
4169
|
}
|
|
4206
4170
|
async stat(path) {
|
|
4171
|
+
if (this.failingStats.has(path)) {
|
|
4172
|
+
return err({ kind: "permission_denied", path, operation: "stat" });
|
|
4173
|
+
}
|
|
4207
4174
|
const full = this.resolve(path);
|
|
4208
4175
|
const file = this.files.get(full);
|
|
4209
4176
|
if (!file)
|
|
@@ -4211,7 +4178,7 @@ class MockSystemOps {
|
|
|
4211
4178
|
return ok({
|
|
4212
4179
|
path,
|
|
4213
4180
|
ownership: { user: file.owner, group: file.group, mode: file.mode },
|
|
4214
|
-
isDirectory: false
|
|
4181
|
+
isDirectory: file.isDirectory ?? false
|
|
4215
4182
|
});
|
|
4216
4183
|
}
|
|
4217
4184
|
async chown(path, owner) {
|
|
@@ -4224,6 +4191,23 @@ class MockSystemOps {
|
|
|
4224
4191
|
file.group = owner.group;
|
|
4225
4192
|
return ok(undefined);
|
|
4226
4193
|
}
|
|
4194
|
+
async chownRecursive(path, owner) {
|
|
4195
|
+
const full = this.resolve(path);
|
|
4196
|
+
const file = this.files.get(full);
|
|
4197
|
+
if (!file)
|
|
4198
|
+
return err({ kind: "not_found", path });
|
|
4199
|
+
this.ops.push({ kind: "chown", path, owner });
|
|
4200
|
+
file.owner = owner.user;
|
|
4201
|
+
file.group = owner.group;
|
|
4202
|
+
const prefix = full + "/";
|
|
4203
|
+
for (const [childPath, childFile] of this.files) {
|
|
4204
|
+
if (childPath.startsWith(prefix)) {
|
|
4205
|
+
childFile.owner = owner.user;
|
|
4206
|
+
childFile.group = owner.group;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
return ok(undefined);
|
|
4210
|
+
}
|
|
4227
4211
|
async chmod(path, mode) {
|
|
4228
4212
|
const full = this.resolve(path);
|
|
4229
4213
|
const file = this.files.get(full);
|
|
@@ -4233,7 +4217,25 @@ class MockSystemOps {
|
|
|
4233
4217
|
file.mode = mode;
|
|
4234
4218
|
return ok(undefined);
|
|
4235
4219
|
}
|
|
4220
|
+
async chmodRecursive(path, mode) {
|
|
4221
|
+
const full = this.resolve(path);
|
|
4222
|
+
const file = this.files.get(full);
|
|
4223
|
+
if (!file)
|
|
4224
|
+
return err({ kind: "not_found", path });
|
|
4225
|
+
this.ops.push({ kind: "chmod", path, mode });
|
|
4226
|
+
file.mode = mode;
|
|
4227
|
+
const prefix = full + "/";
|
|
4228
|
+
for (const [childPath, childFile] of this.files) {
|
|
4229
|
+
if (childPath.startsWith(prefix)) {
|
|
4230
|
+
childFile.mode = mode;
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
return ok(undefined);
|
|
4234
|
+
}
|
|
4236
4235
|
async readFile(path) {
|
|
4236
|
+
if (this.failingReads.has(path)) {
|
|
4237
|
+
return err({ kind: "permission_denied", path, operation: "read" });
|
|
4238
|
+
}
|
|
4237
4239
|
const full = this.resolve(path);
|
|
4238
4240
|
const file = this.files.get(full);
|
|
4239
4241
|
if (!file)
|
|
@@ -4256,14 +4258,26 @@ class MockSystemOps {
|
|
|
4256
4258
|
async mkdir(path) {
|
|
4257
4259
|
const full = this.resolve(path);
|
|
4258
4260
|
if (!this.files.has(full)) {
|
|
4259
|
-
this.files.set(full, {
|
|
4261
|
+
this.files.set(full, {
|
|
4262
|
+
content: "",
|
|
4263
|
+
owner: "root",
|
|
4264
|
+
group: "root",
|
|
4265
|
+
mode: "755",
|
|
4266
|
+
isDirectory: true
|
|
4267
|
+
});
|
|
4260
4268
|
}
|
|
4261
4269
|
const parts = path.split("/");
|
|
4262
4270
|
for (let i = 1;i < parts.length; i++) {
|
|
4263
4271
|
const parent = parts.slice(0, i).join("/");
|
|
4264
4272
|
const parentFull = this.resolve(parent);
|
|
4265
4273
|
if (!this.files.has(parentFull)) {
|
|
4266
|
-
this.files.set(parentFull, {
|
|
4274
|
+
this.files.set(parentFull, {
|
|
4275
|
+
content: "",
|
|
4276
|
+
owner: "root",
|
|
4277
|
+
group: "root",
|
|
4278
|
+
mode: "755",
|
|
4279
|
+
isDirectory: true
|
|
4280
|
+
});
|
|
4267
4281
|
}
|
|
4268
4282
|
}
|
|
4269
4283
|
return ok(undefined);
|
|
@@ -4292,6 +4306,9 @@ class MockSystemOps {
|
|
|
4292
4306
|
return ok(undefined);
|
|
4293
4307
|
}
|
|
4294
4308
|
async hashFile(path) {
|
|
4309
|
+
if (this.failingHashes.has(path)) {
|
|
4310
|
+
return err({ kind: "io_error", path, message: "simulated hash failure" });
|
|
4311
|
+
}
|
|
4295
4312
|
const full = this.resolve(path);
|
|
4296
4313
|
const file = this.files.get(full);
|
|
4297
4314
|
if (!file)
|
|
@@ -4300,18 +4317,37 @@ class MockSystemOps {
|
|
|
4300
4317
|
hash.update(file.content);
|
|
4301
4318
|
return ok(hash.digest("hex"));
|
|
4302
4319
|
}
|
|
4303
|
-
async
|
|
4304
|
-
const
|
|
4305
|
-
const
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4320
|
+
async chmodDirectoryTree(path, modes) {
|
|
4321
|
+
const full = this.resolve(path);
|
|
4322
|
+
const dir = this.files.get(full);
|
|
4323
|
+
if (!dir)
|
|
4324
|
+
return err({ kind: "not_found", path });
|
|
4325
|
+
this.ops.push({ kind: "chmod", path, mode: modes.dirMode });
|
|
4326
|
+
dir.mode = modes.dirMode;
|
|
4327
|
+
const prefix = full + "/";
|
|
4328
|
+
for (const [childPath, childFile] of this.files) {
|
|
4329
|
+
if (childPath.startsWith(prefix)) {
|
|
4330
|
+
childFile.mode = childFile.isDirectory ? modes.dirMode : modes.fileMode;
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
return ok(undefined);
|
|
4334
|
+
}
|
|
4335
|
+
async listDir(path) {
|
|
4336
|
+
if (this.failingListDirs.has(path)) {
|
|
4337
|
+
return err({ kind: "permission_denied", path, operation: "listDir" });
|
|
4338
|
+
}
|
|
4339
|
+
const full = this.resolve(path);
|
|
4340
|
+
const dir = this.files.get(full);
|
|
4341
|
+
if (!dir)
|
|
4342
|
+
return err({ kind: "not_found", path });
|
|
4343
|
+
const prefix = full + "/";
|
|
4344
|
+
const files = [];
|
|
4345
|
+
for (const [childPath, childFile] of this.files) {
|
|
4346
|
+
if (childPath.startsWith(prefix) && !childFile.isDirectory) {
|
|
4347
|
+
files.push(childPath.slice((this.workspace + "/").length));
|
|
4312
4348
|
}
|
|
4313
4349
|
}
|
|
4314
|
-
return ok(
|
|
4350
|
+
return ok(files.sort());
|
|
4315
4351
|
}
|
|
4316
4352
|
async exec(command, args) {
|
|
4317
4353
|
this.ops.push({ kind: "exec", command, args });
|
|
@@ -4327,8 +4363,17 @@ class MockSystemOps {
|
|
|
4327
4363
|
}
|
|
4328
4364
|
return ok(undefined);
|
|
4329
4365
|
}
|
|
4366
|
+
execCaptureResults = new Map;
|
|
4367
|
+
async execCapture(command, args) {
|
|
4368
|
+
this.ops.push({ kind: "exec", command, args });
|
|
4369
|
+
const key = [command, ...args].join(" ");
|
|
4370
|
+
if (this.failingExecs.has(key)) {
|
|
4371
|
+
return err({ kind: "io_error", path: "", message: `${key} failed` });
|
|
4372
|
+
}
|
|
4373
|
+
return ok(this.execCaptureResults.get(key) ?? "");
|
|
4374
|
+
}
|
|
4330
4375
|
}
|
|
4331
|
-
// ../core/src/system-ops-node.ts
|
|
4376
|
+
// ../core/src/util/system-ops-node.ts
|
|
4332
4377
|
import { resolve as resolve2, relative, dirname } from "node:path";
|
|
4333
4378
|
import {
|
|
4334
4379
|
stat as fsStat,
|
|
@@ -4696,34 +4741,24 @@ class NodeSystemOps {
|
|
|
4696
4741
|
stream.on("error", (e) => resolve3(err(mapError(e, path, "hashFile"))));
|
|
4697
4742
|
});
|
|
4698
4743
|
}
|
|
4699
|
-
async
|
|
4744
|
+
async exec(command, args) {
|
|
4745
|
+
const execFileAsync2 = promisify(execFile);
|
|
4700
4746
|
try {
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
const globFn = fs.glob;
|
|
4704
|
-
if (typeof globFn !== "function") {
|
|
4705
|
-
return err({
|
|
4706
|
-
kind: "io_error",
|
|
4707
|
-
path: pattern,
|
|
4708
|
-
message: "fs.glob requires Node.js 22+"
|
|
4709
|
-
});
|
|
4710
|
-
}
|
|
4711
|
-
const globAsync = promisify2(globFn);
|
|
4712
|
-
const matches = await globAsync(pattern, { cwd: this.workspace });
|
|
4713
|
-
return ok([...matches].sort());
|
|
4747
|
+
await execFileAsync2(command, args, { cwd: this.workspace });
|
|
4748
|
+
return ok(undefined);
|
|
4714
4749
|
} catch (e) {
|
|
4715
4750
|
return err({
|
|
4716
4751
|
kind: "io_error",
|
|
4717
|
-
path:
|
|
4718
|
-
message: `
|
|
4752
|
+
path: this.workspace,
|
|
4753
|
+
message: `exec ${command}: ${e instanceof Error ? e.message : String(e)}`
|
|
4719
4754
|
});
|
|
4720
4755
|
}
|
|
4721
4756
|
}
|
|
4722
|
-
async
|
|
4757
|
+
async execCapture(command, args) {
|
|
4723
4758
|
const execFileAsync2 = promisify(execFile);
|
|
4724
4759
|
try {
|
|
4725
|
-
await execFileAsync2(command, args, { cwd: this.workspace });
|
|
4726
|
-
return ok(
|
|
4760
|
+
const { stdout } = await execFileAsync2(command, args, { cwd: this.workspace });
|
|
4761
|
+
return ok(stdout);
|
|
4727
4762
|
} catch (e) {
|
|
4728
4763
|
return err({
|
|
4729
4764
|
kind: "io_error",
|
|
@@ -4732,66 +4767,449 @@ class NodeSystemOps {
|
|
|
4732
4767
|
});
|
|
4733
4768
|
}
|
|
4734
4769
|
}
|
|
4770
|
+
async chownRecursive(path, owner) {
|
|
4771
|
+
const resolved = this.resolvePath(path);
|
|
4772
|
+
if (!resolved.ok)
|
|
4773
|
+
return resolved;
|
|
4774
|
+
try {
|
|
4775
|
+
await execFileAsync("chown", ["-R", `${owner.user}:${owner.group}`, resolved.value]);
|
|
4776
|
+
return ok(undefined);
|
|
4777
|
+
} catch (e) {
|
|
4778
|
+
return err(mapError(e, path, "chownRecursive"));
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
async chmodRecursive(path, mode) {
|
|
4782
|
+
const resolved = this.resolvePath(path);
|
|
4783
|
+
if (!resolved.ok)
|
|
4784
|
+
return resolved;
|
|
4785
|
+
try {
|
|
4786
|
+
await execFileAsync("chmod", ["-R", mode, resolved.value]);
|
|
4787
|
+
return ok(undefined);
|
|
4788
|
+
} catch (e) {
|
|
4789
|
+
return err(mapError(e, path, "chmodRecursive"));
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
async chmodDirectoryTree(path, modes) {
|
|
4793
|
+
const resolved = this.resolvePath(path);
|
|
4794
|
+
if (!resolved.ok)
|
|
4795
|
+
return resolved;
|
|
4796
|
+
try {
|
|
4797
|
+
await execFileAsync("find", [
|
|
4798
|
+
resolved.value,
|
|
4799
|
+
"-type",
|
|
4800
|
+
"f",
|
|
4801
|
+
"-exec",
|
|
4802
|
+
"chmod",
|
|
4803
|
+
modes.fileMode,
|
|
4804
|
+
"{}",
|
|
4805
|
+
"+"
|
|
4806
|
+
]);
|
|
4807
|
+
await execFileAsync("find", [
|
|
4808
|
+
resolved.value,
|
|
4809
|
+
"-type",
|
|
4810
|
+
"d",
|
|
4811
|
+
"-exec",
|
|
4812
|
+
"chmod",
|
|
4813
|
+
modes.dirMode,
|
|
4814
|
+
"{}",
|
|
4815
|
+
"+"
|
|
4816
|
+
]);
|
|
4817
|
+
return ok(undefined);
|
|
4818
|
+
} catch (e) {
|
|
4819
|
+
return err(mapError(e, path, "chmodDirectoryTree"));
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
async listDir(path) {
|
|
4823
|
+
const resolved = this.resolvePath(path);
|
|
4824
|
+
if (!resolved.ok)
|
|
4825
|
+
return resolved;
|
|
4826
|
+
try {
|
|
4827
|
+
const { stdout } = await execFileAsync("find", [resolved.value, "-type", "f"]);
|
|
4828
|
+
const files = stdout.trim().split(`
|
|
4829
|
+
`).filter((f) => f.length > 0).map((f) => relative(this.workspace, f)).sort();
|
|
4830
|
+
return ok(files);
|
|
4831
|
+
} catch (e) {
|
|
4832
|
+
return err(mapError(e, path, "listDir"));
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4735
4835
|
}
|
|
4736
|
-
// ../core/src/
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
}
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4836
|
+
// ../core/src/sdk/state.ts
|
|
4837
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4838
|
+
|
|
4839
|
+
// ../core/src/sdk/staging.ts
|
|
4840
|
+
import { join } from "node:path";
|
|
4841
|
+
var STAGING_DIR = ".soulguard-staging";
|
|
4842
|
+
function stagingPath(filePath) {
|
|
4843
|
+
return join(STAGING_DIR, filePath);
|
|
4844
|
+
}
|
|
4845
|
+
function isStagingPath(filePath) {
|
|
4846
|
+
return filePath === STAGING_DIR || filePath.startsWith(STAGING_DIR + "/");
|
|
4847
|
+
}
|
|
4848
|
+
var DELETE_SENTINEL = { __soulguard_delete_sentinel__: true };
|
|
4849
|
+
function isDeleteSentinel(content) {
|
|
4850
|
+
try {
|
|
4851
|
+
const parsed = JSON.parse(content);
|
|
4852
|
+
return parsed != null && parsed.__soulguard_delete_sentinel__ === true;
|
|
4853
|
+
} catch {
|
|
4854
|
+
return false;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
// ../core/src/util/constants.ts
|
|
4859
|
+
var SOULGUARD_GROUP = "soulguard";
|
|
4860
|
+
function getProtectOwnership(guardian) {
|
|
4861
|
+
return { user: guardian, group: SOULGUARD_GROUP, mode: "444" };
|
|
4862
|
+
}
|
|
4863
|
+
function guardianName(agentUser) {
|
|
4864
|
+
return `soulguardian_${agentUser}`;
|
|
4865
|
+
}
|
|
4866
|
+
function makeDefaultConfig(guardian) {
|
|
4867
|
+
return {
|
|
4868
|
+
version: 1,
|
|
4869
|
+
guardian,
|
|
4870
|
+
files: {
|
|
4871
|
+
"soulguard.json": "protect"
|
|
4872
|
+
}
|
|
4873
|
+
};
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
// ../core/src/sdk/state.ts
|
|
4877
|
+
var PROTECT_DIR_MODE = "555";
|
|
4878
|
+
|
|
4879
|
+
class StateTreeBuildError extends Error {
|
|
4880
|
+
constructor(error) {
|
|
4881
|
+
super(`Failed to build state tree: ${error.message}`);
|
|
4882
|
+
this.name = "StateTreeBuildError";
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4886
|
+
class StateTree {
|
|
4887
|
+
entities;
|
|
4888
|
+
config;
|
|
4889
|
+
protectOwnership;
|
|
4890
|
+
constructor(entities, config, protectOwnership) {
|
|
4891
|
+
this.entities = entities;
|
|
4892
|
+
this.config = config;
|
|
4893
|
+
this.protectOwnership = protectOwnership;
|
|
4894
|
+
}
|
|
4895
|
+
static async buildOrThrow(options) {
|
|
4896
|
+
const result = await StateTree.build(options);
|
|
4897
|
+
if (!result.ok)
|
|
4898
|
+
throw new StateTreeBuildError(result.error);
|
|
4899
|
+
return result.value;
|
|
4900
|
+
}
|
|
4901
|
+
static async build(options) {
|
|
4902
|
+
const { ops, config } = options;
|
|
4903
|
+
const protectOwnership = getProtectOwnership(config.guardian);
|
|
4904
|
+
const entities = [];
|
|
4905
|
+
for (const [key, tier] of Object.entries(config.files)) {
|
|
4906
|
+
let isDir = key.endsWith("/");
|
|
4907
|
+
const path = isDir ? key.slice(0, -1) : key;
|
|
4908
|
+
if (!isDir) {
|
|
4909
|
+
const statResult = await ops.stat(path);
|
|
4910
|
+
if (statResult.ok && statResult.value.isDirectory) {
|
|
4911
|
+
isDir = true;
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
if (isDir) {
|
|
4915
|
+
const result = await buildDirectory(ops, path, tier);
|
|
4916
|
+
if (!result.ok)
|
|
4917
|
+
return result;
|
|
4918
|
+
if (result.value)
|
|
4919
|
+
entities.push(result.value);
|
|
4920
|
+
} else {
|
|
4921
|
+
const result = await buildFile(ops, key, tier);
|
|
4922
|
+
if (!result.ok)
|
|
4923
|
+
return result;
|
|
4924
|
+
if (result.value)
|
|
4925
|
+
entities.push(result.value);
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
return ok(new StateTree(entities, config, protectOwnership));
|
|
4929
|
+
}
|
|
4930
|
+
flatFiles() {
|
|
4931
|
+
return collectFiles(this.entities);
|
|
4932
|
+
}
|
|
4933
|
+
get approvalHash() {
|
|
4934
|
+
const changed = this.changedFiles();
|
|
4935
|
+
if (changed.length === 0)
|
|
4936
|
+
return null;
|
|
4937
|
+
const sorted = [...changed].sort((a, b) => a.path.localeCompare(b.path));
|
|
4938
|
+
const hasher = createHash2("sha256");
|
|
4939
|
+
for (const file of sorted) {
|
|
4940
|
+
hasher.update(file.path);
|
|
4941
|
+
hasher.update(file.status);
|
|
4942
|
+
hasher.update(file.canonicalHash ?? "null");
|
|
4943
|
+
hasher.update(file.stagedHash ?? "null");
|
|
4944
|
+
}
|
|
4945
|
+
return hasher.digest("hex");
|
|
4946
|
+
}
|
|
4947
|
+
changedFiles() {
|
|
4948
|
+
return this.flatFiles().filter((f) => f.status !== "unchanged");
|
|
4949
|
+
}
|
|
4950
|
+
stagedFiles() {
|
|
4951
|
+
return this.flatFiles().filter((f) => f.stagedHash !== null || f.status === "deleted");
|
|
4952
|
+
}
|
|
4953
|
+
driftedEntities() {
|
|
4954
|
+
return collectDrifts(this.entities, this.protectOwnership);
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
function collectFiles(entities) {
|
|
4958
|
+
const files = [];
|
|
4959
|
+
for (const entity of entities) {
|
|
4960
|
+
if (entity.kind === "file") {
|
|
4961
|
+
files.push(entity);
|
|
4962
|
+
} else {
|
|
4963
|
+
files.push(...collectFiles(entity.children));
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
return files;
|
|
4967
|
+
}
|
|
4968
|
+
function collectDrifts(entities, protectOwnership) {
|
|
4969
|
+
const drifts = [];
|
|
4970
|
+
for (const entity of entities) {
|
|
4971
|
+
if (entity.configTier === "protect" && entity.ownership) {
|
|
4972
|
+
const details = [];
|
|
4973
|
+
const expectedMode = entity.kind === "directory" ? PROTECT_DIR_MODE : protectOwnership.mode;
|
|
4974
|
+
if (entity.ownership.user !== protectOwnership.user) {
|
|
4975
|
+
details.push({
|
|
4976
|
+
kind: "wrong_owner",
|
|
4977
|
+
expected: protectOwnership.user,
|
|
4978
|
+
actual: entity.ownership.user
|
|
4979
|
+
});
|
|
4980
|
+
}
|
|
4981
|
+
if (entity.ownership.group !== protectOwnership.group) {
|
|
4982
|
+
details.push({
|
|
4983
|
+
kind: "wrong_group",
|
|
4984
|
+
expected: protectOwnership.group,
|
|
4985
|
+
actual: entity.ownership.group
|
|
4986
|
+
});
|
|
4987
|
+
}
|
|
4988
|
+
if (entity.ownership.mode !== expectedMode) {
|
|
4989
|
+
details.push({ kind: "wrong_mode", expected: expectedMode, actual: entity.ownership.mode });
|
|
4990
|
+
}
|
|
4991
|
+
if (details.length > 0) {
|
|
4992
|
+
drifts.push({ entity, details });
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
if (entity.kind === "directory") {
|
|
4996
|
+
drifts.push(...collectDrifts(entity.children, protectOwnership));
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
return drifts;
|
|
5000
|
+
}
|
|
5001
|
+
async function buildFile(ops, path, tier) {
|
|
5002
|
+
const statResult = await ops.stat(path);
|
|
5003
|
+
let diskExists = false;
|
|
5004
|
+
let ownership = null;
|
|
5005
|
+
if (statResult.ok) {
|
|
5006
|
+
diskExists = true;
|
|
5007
|
+
ownership = statResult.value.ownership;
|
|
5008
|
+
} else if (statResult.error.kind !== "not_found") {
|
|
5009
|
+
return err({
|
|
5010
|
+
kind: "build_failed",
|
|
5011
|
+
message: `stat failed for ${path}: ${statResult.error.kind}`
|
|
5012
|
+
});
|
|
5013
|
+
}
|
|
5014
|
+
let canonicalHash = null;
|
|
5015
|
+
if (diskExists) {
|
|
5016
|
+
const hashResult = await ops.hashFile(path);
|
|
5017
|
+
if (!hashResult.ok) {
|
|
5018
|
+
return err({
|
|
5019
|
+
kind: "build_failed",
|
|
5020
|
+
message: `hash failed for ${path}: ${hashResult.error.kind}`
|
|
5021
|
+
});
|
|
5022
|
+
}
|
|
5023
|
+
canonicalHash = hashResult.value;
|
|
5024
|
+
}
|
|
5025
|
+
const staged = stagingPath(path);
|
|
5026
|
+
const stagedExists = await ops.exists(staged);
|
|
5027
|
+
if (!stagedExists.ok) {
|
|
5028
|
+
return err({
|
|
5029
|
+
kind: "build_failed",
|
|
5030
|
+
message: `exists check failed for ${staged}: ${stagedExists.error.kind}`
|
|
4771
5031
|
});
|
|
4772
5032
|
}
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
5033
|
+
let stagedHash = null;
|
|
5034
|
+
let isDelete = false;
|
|
5035
|
+
if (stagedExists.value) {
|
|
5036
|
+
const content = await ops.readFile(staged);
|
|
5037
|
+
if (!content.ok) {
|
|
5038
|
+
return err({
|
|
5039
|
+
kind: "build_failed",
|
|
5040
|
+
message: `read failed for ${staged}: ${content.error.kind}`
|
|
5041
|
+
});
|
|
5042
|
+
}
|
|
5043
|
+
if (isDeleteSentinel(content.value)) {
|
|
5044
|
+
isDelete = true;
|
|
5045
|
+
} else {
|
|
5046
|
+
const hashResult = await ops.hashFile(staged);
|
|
5047
|
+
if (!hashResult.ok) {
|
|
5048
|
+
return err({
|
|
5049
|
+
kind: "build_failed",
|
|
5050
|
+
message: `hash failed for ${staged}: ${hashResult.error.kind}`
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
stagedHash = hashResult.value;
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
if (!diskExists && !stagedExists.value) {
|
|
5057
|
+
return ok(null);
|
|
5058
|
+
}
|
|
5059
|
+
let status;
|
|
5060
|
+
if (isDelete) {
|
|
5061
|
+
status = "deleted";
|
|
5062
|
+
} else if (!diskExists) {
|
|
5063
|
+
status = "created";
|
|
5064
|
+
} else if (stagedHash === null) {
|
|
5065
|
+
status = "unchanged";
|
|
5066
|
+
} else if (stagedHash === canonicalHash) {
|
|
5067
|
+
status = "unchanged";
|
|
5068
|
+
} else {
|
|
5069
|
+
status = "modified";
|
|
5070
|
+
}
|
|
5071
|
+
return ok({
|
|
5072
|
+
kind: "file",
|
|
5073
|
+
path,
|
|
5074
|
+
configTier: tier,
|
|
5075
|
+
ownership: ownership ? { user: ownership.user, group: ownership.group, mode: ownership.mode } : null,
|
|
5076
|
+
canonicalHash,
|
|
5077
|
+
stagedHash,
|
|
5078
|
+
status
|
|
5079
|
+
});
|
|
5080
|
+
}
|
|
5081
|
+
async function buildDirectory(ops, dirPath, tier) {
|
|
5082
|
+
const statResult = await ops.stat(dirPath);
|
|
5083
|
+
let diskExists = false;
|
|
5084
|
+
let ownership = null;
|
|
5085
|
+
if (statResult.ok) {
|
|
5086
|
+
if (statResult.value.isDirectory) {
|
|
5087
|
+
diskExists = true;
|
|
5088
|
+
ownership = statResult.value.ownership;
|
|
5089
|
+
}
|
|
5090
|
+
} else if (statResult.error.kind !== "not_found") {
|
|
5091
|
+
return err({
|
|
5092
|
+
kind: "build_failed",
|
|
5093
|
+
message: `stat failed for ${dirPath}: ${statResult.error.kind}`
|
|
4778
5094
|
});
|
|
4779
5095
|
}
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
5096
|
+
const staged = stagingPath(dirPath);
|
|
5097
|
+
const stagedStat = await ops.stat(staged);
|
|
5098
|
+
let dirDelete = false;
|
|
5099
|
+
if (stagedStat.ok && !stagedStat.value.isDirectory) {
|
|
5100
|
+
const content = await ops.readFile(staged);
|
|
5101
|
+
if (!content.ok) {
|
|
5102
|
+
return err({
|
|
5103
|
+
kind: "build_failed",
|
|
5104
|
+
message: `read failed for ${staged}: ${content.error.kind}`
|
|
5105
|
+
});
|
|
5106
|
+
}
|
|
5107
|
+
if (isDeleteSentinel(content.value)) {
|
|
5108
|
+
dirDelete = true;
|
|
5109
|
+
}
|
|
5110
|
+
} else if (!stagedStat.ok && stagedStat.error.kind !== "not_found") {
|
|
5111
|
+
return err({
|
|
5112
|
+
kind: "build_failed",
|
|
5113
|
+
message: `stat failed for ${staged}: ${stagedStat.error.kind}`
|
|
4785
5114
|
});
|
|
4786
5115
|
}
|
|
4787
|
-
if (
|
|
4788
|
-
|
|
5116
|
+
if (!diskExists && !dirDelete) {
|
|
5117
|
+
const stagedDirExists = stagedStat.ok && stagedStat.value.isDirectory;
|
|
5118
|
+
if (!stagedDirExists)
|
|
5119
|
+
return ok(null);
|
|
5120
|
+
}
|
|
5121
|
+
const children = [];
|
|
5122
|
+
const diskChildren = new Set;
|
|
5123
|
+
if (diskExists) {
|
|
5124
|
+
const listResult = await ops.listDir(dirPath);
|
|
5125
|
+
if (!listResult.ok) {
|
|
5126
|
+
return err({
|
|
5127
|
+
kind: "build_failed",
|
|
5128
|
+
message: `listDir failed for ${dirPath}: ${listResult.error.kind}`
|
|
5129
|
+
});
|
|
5130
|
+
}
|
|
5131
|
+
for (const childPath of listResult.value) {
|
|
5132
|
+
diskChildren.add(childPath);
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
const stagedChildren = new Set;
|
|
5136
|
+
if (!dirDelete) {
|
|
5137
|
+
if (stagedStat.ok && stagedStat.value.isDirectory) {
|
|
5138
|
+
const stagedList = await ops.listDir(staged);
|
|
5139
|
+
if (!stagedList.ok) {
|
|
5140
|
+
return err({
|
|
5141
|
+
kind: "build_failed",
|
|
5142
|
+
message: `listDir failed for ${staged}: ${stagedList.error.kind}`
|
|
5143
|
+
});
|
|
5144
|
+
}
|
|
5145
|
+
for (const stagedChildPath of stagedList.value) {
|
|
5146
|
+
const canonical = stagedChildPath.slice(STAGING_DIR.length + 1);
|
|
5147
|
+
stagedChildren.add(canonical);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
const allChildren = new Set([...diskChildren, ...stagedChildren]);
|
|
5152
|
+
for (const childPath of [...allChildren].sort()) {
|
|
5153
|
+
if (dirDelete) {
|
|
5154
|
+
const childStat = await ops.stat(childPath);
|
|
5155
|
+
let childOwnership = null;
|
|
5156
|
+
let childHash = null;
|
|
5157
|
+
if (childStat.ok) {
|
|
5158
|
+
childOwnership = {
|
|
5159
|
+
user: childStat.value.ownership.user,
|
|
5160
|
+
group: childStat.value.ownership.group,
|
|
5161
|
+
mode: childStat.value.ownership.mode
|
|
5162
|
+
};
|
|
5163
|
+
const hashResult = await ops.hashFile(childPath);
|
|
5164
|
+
if (!hashResult.ok) {
|
|
5165
|
+
return err({
|
|
5166
|
+
kind: "build_failed",
|
|
5167
|
+
message: `hash failed for ${childPath}: ${hashResult.error.kind}`
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
5170
|
+
childHash = hashResult.value;
|
|
5171
|
+
} else if (childStat.error.kind !== "not_found") {
|
|
5172
|
+
return err({
|
|
5173
|
+
kind: "build_failed",
|
|
5174
|
+
message: `stat failed for ${childPath}: ${childStat.error.kind}`
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
children.push({
|
|
5178
|
+
kind: "file",
|
|
5179
|
+
path: childPath,
|
|
5180
|
+
configTier: tier,
|
|
5181
|
+
ownership: childOwnership,
|
|
5182
|
+
canonicalHash: childHash,
|
|
5183
|
+
stagedHash: null,
|
|
5184
|
+
status: "deleted"
|
|
5185
|
+
});
|
|
5186
|
+
} else {
|
|
5187
|
+
const childResult = await buildFile(ops, childPath, tier);
|
|
5188
|
+
if (!childResult.ok)
|
|
5189
|
+
return childResult;
|
|
5190
|
+
if (childResult.value)
|
|
5191
|
+
children.push(childResult.value);
|
|
5192
|
+
}
|
|
4789
5193
|
}
|
|
4790
|
-
|
|
5194
|
+
if (!diskExists && children.length === 0)
|
|
5195
|
+
return ok(null);
|
|
5196
|
+
return ok({
|
|
5197
|
+
kind: "directory",
|
|
5198
|
+
path: dirPath,
|
|
5199
|
+
configTier: tier,
|
|
5200
|
+
ownership: ownership ? { user: ownership.user, group: ownership.group, mode: ownership.mode } : null,
|
|
5201
|
+
deleted: dirDelete,
|
|
5202
|
+
children
|
|
5203
|
+
});
|
|
5204
|
+
}
|
|
5205
|
+
// ../core/src/sdk/status.ts
|
|
5206
|
+
async function status(opts) {
|
|
5207
|
+
const { tree } = opts;
|
|
5208
|
+
return ok({
|
|
5209
|
+
changed: tree.changedFiles(),
|
|
5210
|
+
drifts: tree.driftedEntities()
|
|
5211
|
+
});
|
|
4791
5212
|
}
|
|
4792
|
-
// ../core/src/diff.ts
|
|
4793
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
4794
|
-
|
|
4795
5213
|
// ../../node_modules/.bun/diff@8.0.3/node_modules/diff/libesm/diff/base.js
|
|
4796
5214
|
class Diff {
|
|
4797
5215
|
diff(oldStr, newStr, options = {}) {
|
|
@@ -5600,148 +6018,118 @@ function splitLines(text) {
|
|
|
5600
6018
|
return result;
|
|
5601
6019
|
}
|
|
5602
6020
|
|
|
5603
|
-
// ../core/src/diff.ts
|
|
5604
|
-
var STAGING_DIR = ".soulguard/staging";
|
|
6021
|
+
// ../core/src/sdk/diff.ts
|
|
5605
6022
|
async function diff(options) {
|
|
5606
|
-
const {
|
|
5607
|
-
|
|
5608
|
-
if (!stagingExists.ok) {
|
|
5609
|
-
return err({ kind: "read_failed", path: STAGING_DIR, message: stagingExists.error.message });
|
|
5610
|
-
}
|
|
5611
|
-
if (!stagingExists.value) {
|
|
5612
|
-
return err({ kind: "no_staging" });
|
|
5613
|
-
}
|
|
5614
|
-
const resolved = await resolvePatterns(ops, config.vault);
|
|
5615
|
-
if (!resolved.ok) {
|
|
5616
|
-
return err({ kind: "read_failed", path: "glob", message: resolved.error.message });
|
|
5617
|
-
}
|
|
5618
|
-
let vaultFiles = resolved.value;
|
|
6023
|
+
const { tree, ops, files: filterFiles } = options;
|
|
6024
|
+
let changed = tree.changedFiles();
|
|
5619
6025
|
if (filterFiles && filterFiles.length > 0) {
|
|
5620
6026
|
const filterSet = new Set(filterFiles);
|
|
5621
|
-
|
|
5622
|
-
}
|
|
5623
|
-
const fileDiffs = [];
|
|
5624
|
-
for (const path of vaultFiles) {
|
|
5625
|
-
const stagingPath = `${STAGING_DIR}/${path}`;
|
|
5626
|
-
const [vaultExists, stagingFileExists] = await Promise.all([
|
|
5627
|
-
ops.exists(path),
|
|
5628
|
-
ops.exists(stagingPath)
|
|
5629
|
-
]);
|
|
5630
|
-
if (!vaultExists.ok) {
|
|
5631
|
-
return err({ kind: "read_failed", path, message: vaultExists.error.message });
|
|
5632
|
-
}
|
|
5633
|
-
if (!stagingFileExists.ok) {
|
|
5634
|
-
return err({
|
|
5635
|
-
kind: "read_failed",
|
|
5636
|
-
path: stagingPath,
|
|
5637
|
-
message: stagingFileExists.error.message
|
|
5638
|
-
});
|
|
5639
|
-
}
|
|
5640
|
-
if (vaultExists.value && !stagingFileExists.value) {
|
|
5641
|
-
const vaultHash2 = await ops.hashFile(path);
|
|
5642
|
-
fileDiffs.push({
|
|
5643
|
-
path,
|
|
5644
|
-
status: "deleted",
|
|
5645
|
-
protectedHash: vaultHash2.ok ? vaultHash2.value : undefined
|
|
5646
|
-
});
|
|
5647
|
-
continue;
|
|
5648
|
-
}
|
|
5649
|
-
if (!vaultExists.value && stagingFileExists.value) {
|
|
5650
|
-
const newHash = await ops.hashFile(stagingPath);
|
|
5651
|
-
fileDiffs.push({
|
|
5652
|
-
path,
|
|
5653
|
-
status: "vault_missing",
|
|
5654
|
-
stagedHash: newHash.ok ? newHash.value : undefined
|
|
5655
|
-
});
|
|
5656
|
-
continue;
|
|
5657
|
-
}
|
|
5658
|
-
if (!vaultExists.value && !stagingFileExists.value) {
|
|
5659
|
-
fileDiffs.push({ path, status: "staging_missing" });
|
|
5660
|
-
continue;
|
|
5661
|
-
}
|
|
5662
|
-
const [vaultHash, stagingHash] = await Promise.all([
|
|
5663
|
-
ops.hashFile(path),
|
|
5664
|
-
ops.hashFile(stagingPath)
|
|
5665
|
-
]);
|
|
5666
|
-
if (!vaultHash.ok) {
|
|
5667
|
-
return err({ kind: "read_failed", path, message: "hash failed" });
|
|
5668
|
-
}
|
|
5669
|
-
if (!stagingHash.ok) {
|
|
5670
|
-
return err({ kind: "read_failed", path: stagingPath, message: "hash failed" });
|
|
5671
|
-
}
|
|
5672
|
-
if (vaultHash.value === stagingHash.value) {
|
|
5673
|
-
fileDiffs.push({
|
|
5674
|
-
path,
|
|
5675
|
-
status: "unchanged",
|
|
5676
|
-
protectedHash: vaultHash.value,
|
|
5677
|
-
stagedHash: stagingHash.value
|
|
5678
|
-
});
|
|
5679
|
-
continue;
|
|
5680
|
-
}
|
|
5681
|
-
const [vaultContent, stagingContent] = await Promise.all([
|
|
5682
|
-
ops.readFile(path),
|
|
5683
|
-
ops.readFile(stagingPath)
|
|
5684
|
-
]);
|
|
5685
|
-
if (!vaultContent.ok) {
|
|
5686
|
-
return err({ kind: "read_failed", path, message: "read failed" });
|
|
5687
|
-
}
|
|
5688
|
-
if (!stagingContent.ok) {
|
|
5689
|
-
return err({ kind: "read_failed", path: stagingPath, message: "read failed" });
|
|
5690
|
-
}
|
|
5691
|
-
const unifiedDiff = createTwoFilesPatch(`a/${path}`, `b/${path}`, vaultContent.value, stagingContent.value);
|
|
5692
|
-
fileDiffs.push({
|
|
5693
|
-
path,
|
|
5694
|
-
status: "modified",
|
|
5695
|
-
diff: unifiedDiff,
|
|
5696
|
-
protectedHash: vaultHash.value,
|
|
5697
|
-
stagedHash: stagingHash.value
|
|
5698
|
-
});
|
|
6027
|
+
changed = changed.filter((f) => filterSet.has(f.path));
|
|
5699
6028
|
}
|
|
5700
|
-
const
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
6029
|
+
const files = [];
|
|
6030
|
+
for (const sf of changed) {
|
|
6031
|
+
const diffText = await generateDiff(ops, sf);
|
|
6032
|
+
if (!diffText.ok)
|
|
6033
|
+
return diffText;
|
|
6034
|
+
files.push({ file: sf, diff: diffText.value });
|
|
5704
6035
|
}
|
|
5705
|
-
return ok({
|
|
6036
|
+
return ok({
|
|
6037
|
+
files,
|
|
6038
|
+
hasChanges: files.length > 0,
|
|
6039
|
+
approvalHash: tree.approvalHash ?? undefined
|
|
6040
|
+
});
|
|
5706
6041
|
}
|
|
5707
|
-
function
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
6042
|
+
async function generateDiff(ops, sf) {
|
|
6043
|
+
let oldContent = "";
|
|
6044
|
+
let newContent = "";
|
|
6045
|
+
if (sf.status !== "created") {
|
|
6046
|
+
const result = await ops.readFile(sf.path);
|
|
6047
|
+
if (!result.ok) {
|
|
6048
|
+
return err({ kind: "build_failed", message: `read failed for ${sf.path}` });
|
|
6049
|
+
}
|
|
6050
|
+
oldContent = result.value;
|
|
6051
|
+
}
|
|
6052
|
+
if (sf.status !== "deleted") {
|
|
6053
|
+
const result = await ops.readFile(stagingPath(sf.path));
|
|
6054
|
+
if (!result.ok) {
|
|
6055
|
+
return err({ kind: "build_failed", message: `read failed for ${stagingPath(sf.path)}` });
|
|
5715
6056
|
}
|
|
6057
|
+
newContent = result.value;
|
|
6058
|
+
}
|
|
6059
|
+
const oldName = sf.status === "created" ? "/dev/null" : `a/${sf.path}`;
|
|
6060
|
+
const newName = sf.status === "deleted" ? "/dev/null" : `b/${sf.path}`;
|
|
6061
|
+
return ok(createTwoFilesPatch(oldName, newName, oldContent, newContent));
|
|
6062
|
+
}
|
|
6063
|
+
// ../core/src/sdk/config.ts
|
|
6064
|
+
function patternsForTier(config, tier) {
|
|
6065
|
+
return Object.entries(config.files).filter(([, t]) => t === tier).map(([pattern]) => pattern);
|
|
6066
|
+
}
|
|
6067
|
+
function protectPatterns(config) {
|
|
6068
|
+
return patternsForTier(config, "protect");
|
|
6069
|
+
}
|
|
6070
|
+
function watchPatterns(config) {
|
|
6071
|
+
return patternsForTier(config, "watch");
|
|
6072
|
+
}
|
|
6073
|
+
async function readConfig(ops) {
|
|
6074
|
+
const exists = await ops.exists("soulguard.json");
|
|
6075
|
+
if (!exists.ok) {
|
|
6076
|
+
return err({ kind: "io_error", message: exists.error.message });
|
|
6077
|
+
}
|
|
6078
|
+
if (!exists.value) {
|
|
6079
|
+
return err({ kind: "not_found" });
|
|
6080
|
+
}
|
|
6081
|
+
const raw = await ops.readFile("soulguard.json");
|
|
6082
|
+
if (!raw.ok) {
|
|
6083
|
+
return err({ kind: "io_error", message: raw.error.kind });
|
|
5716
6084
|
}
|
|
5717
|
-
|
|
6085
|
+
try {
|
|
6086
|
+
return ok(parseConfig(JSON.parse(raw.value)));
|
|
6087
|
+
} catch (e) {
|
|
6088
|
+
return err({
|
|
6089
|
+
kind: "parse_failed",
|
|
6090
|
+
message: e instanceof Error ? e.message : String(e)
|
|
6091
|
+
});
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
async function writeConfig(ops, config) {
|
|
6095
|
+
const content = JSON.stringify(config, null, 2) + `
|
|
6096
|
+
`;
|
|
6097
|
+
const result = await ops.writeFile("soulguard.json", content);
|
|
6098
|
+
if (!result.ok) {
|
|
6099
|
+
return err({ kind: "config_write_failed", message: result.error.kind });
|
|
6100
|
+
}
|
|
6101
|
+
return ok(undefined);
|
|
5718
6102
|
}
|
|
5719
|
-
|
|
6103
|
+
|
|
6104
|
+
// ../core/src/util/git.ts
|
|
6105
|
+
var GIT_DIR = ".soulguard/.git";
|
|
6106
|
+
var GIT_ARGS = ["--git-dir", GIT_DIR, "--work-tree", "."];
|
|
5720
6107
|
async function isGitEnabled(ops, config) {
|
|
5721
6108
|
if (config.git === false)
|
|
5722
6109
|
return false;
|
|
5723
|
-
const gitExists = await ops.exists(
|
|
6110
|
+
const gitExists = await ops.exists(GIT_DIR);
|
|
5724
6111
|
return gitExists.ok && gitExists.value;
|
|
5725
6112
|
}
|
|
5726
6113
|
async function gitCommit(ops, files, message) {
|
|
5727
6114
|
if (files.length === 0) {
|
|
5728
6115
|
return ok({ committed: false, reason: "no_files" });
|
|
5729
6116
|
}
|
|
5730
|
-
const preCheck = await ops.exec("git", ["diff", "--cached", "--quiet"]);
|
|
6117
|
+
const preCheck = await ops.exec("git", [...GIT_ARGS, "diff", "--cached", "--quiet"]);
|
|
5731
6118
|
if (!preCheck.ok) {
|
|
5732
6119
|
return ok({ committed: false, reason: "dirty_staging" });
|
|
5733
6120
|
}
|
|
5734
6121
|
for (const file of files) {
|
|
5735
|
-
const result = await ops.exec("git", ["add", "--", file]);
|
|
6122
|
+
const result = await ops.exec("git", [...GIT_ARGS, "add", "--", file]);
|
|
5736
6123
|
if (!result.ok) {
|
|
5737
6124
|
return err({ kind: "git_error", message: `git add ${file}: ${result.error.message}` });
|
|
5738
6125
|
}
|
|
5739
6126
|
}
|
|
5740
|
-
const diffResult = await ops.exec("git", ["diff", "--cached", "--quiet"]);
|
|
6127
|
+
const diffResult = await ops.exec("git", [...GIT_ARGS, "diff", "--cached", "--quiet"]);
|
|
5741
6128
|
if (diffResult.ok) {
|
|
5742
6129
|
return ok({ committed: false, reason: "nothing_staged" });
|
|
5743
6130
|
}
|
|
5744
6131
|
const commitResult = await ops.exec("git", [
|
|
6132
|
+
...GIT_ARGS,
|
|
5745
6133
|
"commit",
|
|
5746
6134
|
"--author",
|
|
5747
6135
|
"SoulGuardian <soulguardian@soulguard.ai>",
|
|
@@ -5753,9 +6141,9 @@ async function gitCommit(ops, files, message) {
|
|
|
5753
6141
|
}
|
|
5754
6142
|
return ok({ committed: true, message, files });
|
|
5755
6143
|
}
|
|
5756
|
-
function
|
|
6144
|
+
function protectCommitMessage(files, approvalMessage) {
|
|
5757
6145
|
const fileList = files.join(", ");
|
|
5758
|
-
const base = `soulguard:
|
|
6146
|
+
const base = `soulguard: protect update — ${fileList}`;
|
|
5759
6147
|
if (approvalMessage) {
|
|
5760
6148
|
return `${base}
|
|
5761
6149
|
|
|
@@ -5763,41 +6151,23 @@ ${approvalMessage}`;
|
|
|
5763
6151
|
}
|
|
5764
6152
|
return base;
|
|
5765
6153
|
}
|
|
5766
|
-
function ledgerCommitMessage() {
|
|
5767
|
-
return "soulguard: ledger sync";
|
|
5768
|
-
}
|
|
5769
|
-
async function commitLedgerFiles(ops, config) {
|
|
5770
|
-
if (!await isGitEnabled(ops, config)) {
|
|
5771
|
-
return ok({ committed: false, reason: "git_disabled" });
|
|
5772
|
-
}
|
|
5773
|
-
const resolved = await resolvePatterns(ops, config.ledger);
|
|
5774
|
-
if (!resolved.ok) {
|
|
5775
|
-
return err({ kind: "git_error", message: `glob failed: ${resolved.error.message}` });
|
|
5776
|
-
}
|
|
5777
|
-
const ledgerFiles = resolved.value;
|
|
5778
|
-
if (ledgerFiles.length === 0) {
|
|
5779
|
-
return ok({ committed: false, reason: "no_files" });
|
|
5780
|
-
}
|
|
5781
|
-
return gitCommit(ops, ledgerFiles, ledgerCommitMessage());
|
|
5782
|
-
}
|
|
5783
6154
|
|
|
5784
|
-
// ../core/src/sync.ts
|
|
6155
|
+
// ../core/src/sdk/sync.ts
|
|
5785
6156
|
async function sync(options) {
|
|
5786
|
-
const { ops } = options;
|
|
5787
|
-
const
|
|
5788
|
-
if (!beforeResult.ok)
|
|
5789
|
-
return beforeResult;
|
|
5790
|
-
const before = beforeResult.value;
|
|
6157
|
+
const { tree, ops, config } = options;
|
|
6158
|
+
const drifts = tree.driftedEntities();
|
|
5791
6159
|
const errors2 = [];
|
|
5792
|
-
for (const
|
|
5793
|
-
if (
|
|
6160
|
+
for (const drift of drifts) {
|
|
6161
|
+
if (drift.entity.configTier !== "protect")
|
|
5794
6162
|
continue;
|
|
5795
|
-
const
|
|
5796
|
-
const
|
|
5797
|
-
const
|
|
5798
|
-
const needsChmod = issue.issues.some((i) => i.kind === "wrong_mode");
|
|
6163
|
+
const path = drift.entity.path;
|
|
6164
|
+
const needsChown = drift.details.some((i) => i.kind === "wrong_owner" || i.kind === "wrong_group");
|
|
6165
|
+
const needsChmod = drift.details.some((i) => i.kind === "wrong_mode");
|
|
5799
6166
|
if (needsChown) {
|
|
5800
|
-
const
|
|
6167
|
+
const ownerIssue = drift.details.find((i) => i.kind === "wrong_owner");
|
|
6168
|
+
const groupIssue = drift.details.find((i) => i.kind === "wrong_group");
|
|
6169
|
+
const user = ownerIssue ? ownerIssue.expected : drift.entity.ownership.user;
|
|
6170
|
+
const group = groupIssue ? groupIssue.expected : drift.entity.ownership.group;
|
|
5801
6171
|
const result = await ops.chown(path, { user, group });
|
|
5802
6172
|
if (!result.ok) {
|
|
5803
6173
|
errors2.push({ path, operation: "chown", error: result.error });
|
|
@@ -5805,27 +6175,19 @@ async function sync(options) {
|
|
|
5805
6175
|
}
|
|
5806
6176
|
}
|
|
5807
6177
|
if (needsChmod) {
|
|
5808
|
-
const
|
|
6178
|
+
const modeIssue = drift.details.find((i) => i.kind === "wrong_mode");
|
|
6179
|
+
const result = await ops.chmod(path, modeIssue.expected);
|
|
5809
6180
|
if (!result.ok) {
|
|
5810
6181
|
errors2.push({ path, operation: "chmod", error: result.error });
|
|
5811
|
-
continue;
|
|
5812
6182
|
}
|
|
5813
6183
|
}
|
|
5814
6184
|
}
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
const after = afterResult.value;
|
|
6185
|
+
if (errors2.length > 0) {
|
|
6186
|
+
return ok({ drifts, errors: errors2, git: undefined });
|
|
6187
|
+
}
|
|
5819
6188
|
let git;
|
|
5820
|
-
if (await isGitEnabled(ops,
|
|
5821
|
-
const
|
|
5822
|
-
resolvePatterns(ops, options.config.vault),
|
|
5823
|
-
resolvePatterns(ops, options.config.ledger)
|
|
5824
|
-
]);
|
|
5825
|
-
const allFiles = [
|
|
5826
|
-
...vaultResolved.ok ? vaultResolved.value : [],
|
|
5827
|
-
...ledgerResolved.ok ? ledgerResolved.value : []
|
|
5828
|
-
];
|
|
6189
|
+
if (await isGitEnabled(ops, config)) {
|
|
6190
|
+
const allFiles = [...protectPatterns(config), ...watchPatterns(config)];
|
|
5829
6191
|
if (allFiles.length > 0) {
|
|
5830
6192
|
const gitResult = await gitCommit(ops, allFiles, "soulguard: sync");
|
|
5831
6193
|
if (gitResult.ok) {
|
|
@@ -5833,20 +6195,9 @@ async function sync(options) {
|
|
|
5833
6195
|
}
|
|
5834
6196
|
}
|
|
5835
6197
|
}
|
|
5836
|
-
return ok({
|
|
6198
|
+
return ok({ drifts, errors: errors2, git });
|
|
5837
6199
|
}
|
|
5838
|
-
// ../core/src/
|
|
5839
|
-
var IDENTITY = { user: "soulguardian", group: "soulguard" };
|
|
5840
|
-
var VAULT_OWNERSHIP = {
|
|
5841
|
-
user: IDENTITY.user,
|
|
5842
|
-
group: IDENTITY.group,
|
|
5843
|
-
mode: "444"
|
|
5844
|
-
};
|
|
5845
|
-
var DEFAULT_CONFIG = {
|
|
5846
|
-
vault: ["soulguard.json"],
|
|
5847
|
-
ledger: []
|
|
5848
|
-
};
|
|
5849
|
-
// ../core/src/console-live.ts
|
|
6200
|
+
// ../core/src/util/console-live.ts
|
|
5850
6201
|
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
5851
6202
|
|
|
5852
6203
|
class LiveConsoleOutput {
|
|
@@ -5869,7 +6220,7 @@ class LiveConsoleOutput {
|
|
|
5869
6220
|
console.log(import_picocolors.default.bold(text));
|
|
5870
6221
|
}
|
|
5871
6222
|
}
|
|
5872
|
-
// ../core/src/policy.ts
|
|
6223
|
+
// ../core/src/sdk/policy.ts
|
|
5873
6224
|
function validatePolicies(policies) {
|
|
5874
6225
|
const seen = new Set;
|
|
5875
6226
|
const duplicates = [];
|
|
@@ -5897,7 +6248,7 @@ async function evaluatePolicies(policies, ctx) {
|
|
|
5897
6248
|
}
|
|
5898
6249
|
return ok(undefined);
|
|
5899
6250
|
}
|
|
5900
|
-
// ../core/src/self-protection.ts
|
|
6251
|
+
// ../core/src/sdk/self-protection.ts
|
|
5901
6252
|
function validateSelfProtection(pendingContents, deletedFiles = []) {
|
|
5902
6253
|
if (deletedFiles.some((f) => f.path === "soulguard.json")) {
|
|
5903
6254
|
return err({
|
|
@@ -5928,64 +6279,54 @@ function validateSelfProtection(pendingContents, deletedFiles = []) {
|
|
|
5928
6279
|
}
|
|
5929
6280
|
return ok(undefined);
|
|
5930
6281
|
}
|
|
5931
|
-
// ../core/src/
|
|
5932
|
-
|
|
5933
|
-
|
|
6282
|
+
// ../core/src/sdk/apply.ts
|
|
6283
|
+
import { dirname as dirname2 } from "node:path";
|
|
6284
|
+
function collectParentDirs(prefix, paths) {
|
|
6285
|
+
const dirs = new Set;
|
|
6286
|
+
for (const p of paths) {
|
|
6287
|
+
const parent = dirname2(`${prefix}/${p}`);
|
|
6288
|
+
if (parent !== prefix) {
|
|
6289
|
+
dirs.add(parent);
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
6292
|
+
return [...dirs].sort();
|
|
6293
|
+
}
|
|
6294
|
+
async function apply(options) {
|
|
6295
|
+
const { ops, tree, hash, policies } = options;
|
|
6296
|
+
const protectOwnership = tree.protectOwnership;
|
|
5934
6297
|
if (policies && policies.length > 0) {
|
|
5935
6298
|
const validation = validatePolicies(policies);
|
|
5936
6299
|
if (!validation.ok) {
|
|
5937
6300
|
return err(validation.error);
|
|
5938
6301
|
}
|
|
5939
6302
|
}
|
|
5940
|
-
const
|
|
5941
|
-
if (
|
|
5942
|
-
return
|
|
6303
|
+
const changedFiles = tree.changedFiles();
|
|
6304
|
+
if (changedFiles.length === 0) {
|
|
6305
|
+
return ok({ appliedFiles: [] });
|
|
5943
6306
|
}
|
|
5944
|
-
if (
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
const copyResult = await ops.copyFile(`.soulguard/staging/${file.path}`, `.soulguard/pending/${file.path}`);
|
|
5951
|
-
if (!copyResult.ok) {
|
|
5952
|
-
await cleanupPending(ops, changedFiles);
|
|
5953
|
-
return err({ kind: "apply_failed", message: `Cannot copy staging/${file.path} to pending` });
|
|
6307
|
+
if (hash) {
|
|
6308
|
+
if (tree.approvalHash !== hash) {
|
|
6309
|
+
return err({
|
|
6310
|
+
kind: "hash_mismatch",
|
|
6311
|
+
message: `Expected hash ${hash} but got hash ${tree.approvalHash}`
|
|
6312
|
+
});
|
|
5954
6313
|
}
|
|
5955
6314
|
}
|
|
5956
|
-
const
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
}
|
|
5961
|
-
const pendingHash = await computePendingHash(ops, changedFiles);
|
|
5962
|
-
if (!pendingHash.ok) {
|
|
5963
|
-
await cleanupPending(ops, changedFiles);
|
|
5964
|
-
return err({ kind: "apply_failed", message: pendingHash.error });
|
|
5965
|
-
}
|
|
5966
|
-
if (pendingHash.value !== hash) {
|
|
5967
|
-
await cleanupPending(ops, changedFiles);
|
|
5968
|
-
return err({
|
|
5969
|
-
kind: "hash_mismatch",
|
|
5970
|
-
message: `Expected hash ${hash} but got hash ${pendingHash.value}`
|
|
5971
|
-
});
|
|
5972
|
-
}
|
|
5973
|
-
const pendingContents = new Map;
|
|
5974
|
-
for (const file of changedFiles.filter((f) => f.status !== "deleted")) {
|
|
5975
|
-
const content = await ops.readFile(`.soulguard/pending/${file.path}`);
|
|
6315
|
+
const nonDeletedFiles = changedFiles.filter((f) => f.status !== "deleted");
|
|
6316
|
+
const stagingContents = new Map;
|
|
6317
|
+
for (const file of nonDeletedFiles) {
|
|
6318
|
+
const content = await ops.readFile(stagingPath(file.path));
|
|
5976
6319
|
if (!content.ok) {
|
|
5977
|
-
await cleanupPending(ops, changedFiles);
|
|
5978
6320
|
return err({
|
|
5979
6321
|
kind: "apply_failed",
|
|
5980
|
-
message: `Cannot read
|
|
6322
|
+
message: `Cannot read staging/${file.path}`
|
|
5981
6323
|
});
|
|
5982
6324
|
}
|
|
5983
|
-
|
|
6325
|
+
stagingContents.set(file.path, content.value);
|
|
5984
6326
|
}
|
|
5985
6327
|
{
|
|
5986
|
-
const selfCheck = validateSelfProtection(
|
|
6328
|
+
const selfCheck = validateSelfProtection(stagingContents, changedFiles.filter((f) => f.status === "deleted"));
|
|
5987
6329
|
if (!selfCheck.ok) {
|
|
5988
|
-
await cleanupPending(ops, changedFiles);
|
|
5989
6330
|
return err(selfCheck.error);
|
|
5990
6331
|
}
|
|
5991
6332
|
}
|
|
@@ -5993,103 +6334,118 @@ async function approve(options) {
|
|
|
5993
6334
|
const ctx = new Map;
|
|
5994
6335
|
for (const file of changedFiles) {
|
|
5995
6336
|
if (file.status === "deleted") {
|
|
5996
|
-
const
|
|
6337
|
+
const protectContent = await ops.readFile(file.path);
|
|
5997
6338
|
ctx.set(file.path, {
|
|
5998
6339
|
final: "",
|
|
5999
6340
|
diff: `File deleted: ${file.path}`,
|
|
6000
|
-
previous:
|
|
6341
|
+
previous: protectContent.ok ? protectContent.value : ""
|
|
6001
6342
|
});
|
|
6002
6343
|
} else {
|
|
6003
6344
|
let previous = "";
|
|
6004
6345
|
if (file.status === "modified") {
|
|
6005
|
-
const
|
|
6006
|
-
if (
|
|
6007
|
-
previous =
|
|
6346
|
+
const protectContent = await ops.readFile(file.path);
|
|
6347
|
+
if (protectContent.ok) {
|
|
6348
|
+
previous = protectContent.value;
|
|
6008
6349
|
}
|
|
6009
6350
|
}
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
previous
|
|
6014
|
-
});
|
|
6351
|
+
const final = stagingContents.get(file.path);
|
|
6352
|
+
const diffText = file.status === "modified" ? createTwoFilesPatch(`a/${file.path}`, `b/${file.path}`, previous, final) : "";
|
|
6353
|
+
ctx.set(file.path, { final, diff: diffText, previous });
|
|
6015
6354
|
}
|
|
6016
6355
|
}
|
|
6017
6356
|
const policyResult = await evaluatePolicies(policies, ctx);
|
|
6018
6357
|
if (!policyResult.ok) {
|
|
6019
|
-
await cleanupPending(ops, changedFiles);
|
|
6020
6358
|
return err(policyResult.error);
|
|
6021
6359
|
}
|
|
6022
6360
|
}
|
|
6023
|
-
const backedUpFiles = [];
|
|
6024
6361
|
await ops.mkdir(".soulguard/backup");
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6362
|
+
const filesToBackup = changedFiles.filter((f) => f.status !== "created");
|
|
6363
|
+
const backupParents = collectParentDirs(".soulguard/backup", filesToBackup.map((f) => f.path));
|
|
6364
|
+
for (const dir of backupParents) {
|
|
6365
|
+
await ops.mkdir(dir);
|
|
6366
|
+
}
|
|
6367
|
+
for (const file of filesToBackup) {
|
|
6028
6368
|
const backupResult = await ops.copyFile(file.path, `.soulguard/backup/${file.path}`);
|
|
6029
6369
|
if (!backupResult.ok) {
|
|
6030
|
-
await cleanupBackup(ops
|
|
6031
|
-
await cleanupPending(ops, changedFiles);
|
|
6370
|
+
await cleanupBackup(ops);
|
|
6032
6371
|
return err({ kind: "apply_failed", message: `Backup of ${file.path} failed` });
|
|
6033
6372
|
}
|
|
6034
|
-
backedUpFiles.push(file.path);
|
|
6035
6373
|
}
|
|
6036
6374
|
const appliedFiles = [];
|
|
6037
6375
|
for (const file of changedFiles) {
|
|
6038
6376
|
if (file.status === "deleted") {
|
|
6039
6377
|
const deleteResult = await ops.deleteFile(file.path);
|
|
6040
6378
|
if (!deleteResult.ok) {
|
|
6041
|
-
await rollback(ops,
|
|
6379
|
+
await rollback(ops, appliedFiles, protectOwnership);
|
|
6042
6380
|
return err({
|
|
6043
6381
|
kind: "apply_failed",
|
|
6044
6382
|
message: `Cannot delete ${file.path}: ${deleteResult.error.kind}`
|
|
6045
6383
|
});
|
|
6046
6384
|
}
|
|
6047
6385
|
} else {
|
|
6048
|
-
|
|
6386
|
+
if (file.status === "created") {
|
|
6387
|
+
const parentDir = dirname2(file.path);
|
|
6388
|
+
if (parentDir !== ".") {
|
|
6389
|
+
await ops.mkdir(parentDir);
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
const content = await ops.readFile(stagingPath(file.path));
|
|
6049
6393
|
if (!content.ok) {
|
|
6050
|
-
await rollback(ops,
|
|
6051
|
-
return err({ kind: "apply_failed", message: `Cannot read
|
|
6394
|
+
await rollback(ops, appliedFiles, protectOwnership);
|
|
6395
|
+
return err({ kind: "apply_failed", message: `Cannot read staging/${file.path}` });
|
|
6052
6396
|
}
|
|
6053
6397
|
const writeResult = await ops.writeFile(file.path, content.value);
|
|
6054
6398
|
if (!writeResult.ok) {
|
|
6055
|
-
await rollback(ops,
|
|
6399
|
+
await rollback(ops, appliedFiles, protectOwnership);
|
|
6056
6400
|
return err({
|
|
6057
6401
|
kind: "apply_failed",
|
|
6058
6402
|
message: `Cannot write ${file.path}: ${writeResult.error.kind}`
|
|
6059
6403
|
});
|
|
6060
6404
|
}
|
|
6061
|
-
const chownResult = await ops.chown(file.path,
|
|
6405
|
+
const chownResult = await ops.chown(file.path, protectOwnership);
|
|
6062
6406
|
if (!chownResult.ok) {
|
|
6063
|
-
await rollback(ops,
|
|
6407
|
+
await rollback(ops, appliedFiles, protectOwnership);
|
|
6064
6408
|
return err({
|
|
6065
6409
|
kind: "apply_failed",
|
|
6066
6410
|
message: `Cannot chown ${file.path}: ${chownResult.error.kind}`
|
|
6067
6411
|
});
|
|
6068
6412
|
}
|
|
6069
|
-
const chmodResult = await ops.chmod(file.path,
|
|
6413
|
+
const chmodResult = await ops.chmod(file.path, protectOwnership.mode);
|
|
6070
6414
|
if (!chmodResult.ok) {
|
|
6071
|
-
await rollback(ops,
|
|
6415
|
+
await rollback(ops, appliedFiles, protectOwnership);
|
|
6072
6416
|
return err({
|
|
6073
6417
|
kind: "apply_failed",
|
|
6074
6418
|
message: `Cannot chmod ${file.path}: ${chmodResult.error.kind}`
|
|
6075
6419
|
});
|
|
6076
6420
|
}
|
|
6421
|
+
const writtenHash = await ops.hashFile(file.path);
|
|
6422
|
+
if (!writtenHash.ok || writtenHash.value !== file.stagedHash) {
|
|
6423
|
+
await rollback(ops, appliedFiles, protectOwnership);
|
|
6424
|
+
return err({
|
|
6425
|
+
kind: "hash_mismatch",
|
|
6426
|
+
message: `Staging tampered: ${file.path} content after write does not match snapshot`
|
|
6427
|
+
});
|
|
6428
|
+
}
|
|
6077
6429
|
}
|
|
6078
6430
|
appliedFiles.push(file.path);
|
|
6079
6431
|
}
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
await ops.
|
|
6432
|
+
const defaultOwnership = tree.config.defaultOwnership;
|
|
6433
|
+
for (const file of nonDeletedFiles) {
|
|
6434
|
+
const stagePath = stagingPath(file.path);
|
|
6435
|
+
const stageParent = dirname2(stagePath);
|
|
6436
|
+
if (stageParent !== STAGING_DIR) {
|
|
6437
|
+
await ops.mkdir(stageParent);
|
|
6438
|
+
}
|
|
6439
|
+
await ops.copyFile(file.path, stagePath);
|
|
6440
|
+
if (defaultOwnership) {
|
|
6441
|
+
await ops.chown(stagePath, defaultOwnership);
|
|
6442
|
+
await ops.chmod(stagePath, defaultOwnership.mode);
|
|
6086
6443
|
}
|
|
6087
6444
|
}
|
|
6088
|
-
await cleanupBackup(ops
|
|
6089
|
-
await cleanupPending(ops, changedFiles);
|
|
6445
|
+
await cleanupBackup(ops);
|
|
6090
6446
|
let gitResult;
|
|
6091
|
-
if (await isGitEnabled(ops, config)) {
|
|
6092
|
-
const message =
|
|
6447
|
+
if (await isGitEnabled(ops, tree.config)) {
|
|
6448
|
+
const message = protectCommitMessage(appliedFiles);
|
|
6093
6449
|
const result = await gitCommit(ops, appliedFiles, message);
|
|
6094
6450
|
if (result.ok) {
|
|
6095
6451
|
gitResult = result.value;
|
|
@@ -6097,77 +6453,57 @@ async function approve(options) {
|
|
|
6097
6453
|
}
|
|
6098
6454
|
return ok({ appliedFiles, gitResult });
|
|
6099
6455
|
}
|
|
6100
|
-
async function
|
|
6101
|
-
|
|
6102
|
-
for (const f of changedFiles) {
|
|
6103
|
-
if (f.status === "deleted") {
|
|
6104
|
-
withHashes.push(f);
|
|
6105
|
-
} else {
|
|
6106
|
-
const fileHash = await ops.hashFile(`.soulguard/pending/${f.path}`);
|
|
6107
|
-
if (!fileHash.ok) {
|
|
6108
|
-
return err(`Cannot hash pending/${f.path}`);
|
|
6109
|
-
}
|
|
6110
|
-
withHashes.push({ ...f, stagedHash: fileHash.value });
|
|
6111
|
-
}
|
|
6112
|
-
}
|
|
6113
|
-
return ok(computeApprovalHash(withHashes));
|
|
6114
|
-
}
|
|
6115
|
-
async function cleanupPending(ops, files) {
|
|
6116
|
-
for (const file of files) {
|
|
6117
|
-
await ops.deleteFile(`.soulguard/pending/${file.path}`);
|
|
6118
|
-
}
|
|
6119
|
-
}
|
|
6120
|
-
async function cleanupBackup(ops, backedUpFiles) {
|
|
6121
|
-
for (const filePath of backedUpFiles) {
|
|
6122
|
-
await ops.deleteFile(`.soulguard/backup/${filePath}`);
|
|
6123
|
-
}
|
|
6456
|
+
async function cleanupBackup(ops) {
|
|
6457
|
+
await ops.deleteFile(".soulguard/backup");
|
|
6124
6458
|
}
|
|
6125
|
-
async function rollback(ops,
|
|
6459
|
+
async function rollback(ops, appliedFiles, protectOwnership) {
|
|
6126
6460
|
for (const filePath of appliedFiles) {
|
|
6127
|
-
const
|
|
6128
|
-
if (
|
|
6129
|
-
await ops.
|
|
6130
|
-
|
|
6131
|
-
|
|
6461
|
+
const backupExists = await ops.exists(`.soulguard/backup/${filePath}`);
|
|
6462
|
+
if (backupExists.ok && backupExists.value) {
|
|
6463
|
+
const backupContent = await ops.readFile(`.soulguard/backup/${filePath}`);
|
|
6464
|
+
if (backupContent.ok) {
|
|
6465
|
+
await ops.writeFile(filePath, backupContent.value);
|
|
6466
|
+
await ops.chown(filePath, protectOwnership);
|
|
6467
|
+
await ops.chmod(filePath, protectOwnership.mode);
|
|
6468
|
+
}
|
|
6469
|
+
} else {
|
|
6470
|
+
await ops.deleteFile(filePath);
|
|
6132
6471
|
}
|
|
6133
6472
|
}
|
|
6134
|
-
await cleanupBackup(ops
|
|
6135
|
-
await cleanupPending(ops, changedFiles);
|
|
6473
|
+
await cleanupBackup(ops);
|
|
6136
6474
|
}
|
|
6137
|
-
// ../core/src/reset.ts
|
|
6475
|
+
// ../core/src/sdk/reset.ts
|
|
6138
6476
|
async function reset(options) {
|
|
6139
|
-
const { ops,
|
|
6140
|
-
const
|
|
6141
|
-
if (
|
|
6142
|
-
return
|
|
6477
|
+
const { tree, ops, paths, all } = options;
|
|
6478
|
+
const stagedFiles = tree.stagedFiles().map((f) => f.path);
|
|
6479
|
+
if (stagedFiles.length === 0) {
|
|
6480
|
+
return ok({ stagedFiles: [], deleted: false });
|
|
6143
6481
|
}
|
|
6144
|
-
if (!
|
|
6145
|
-
return ok({
|
|
6482
|
+
if (!paths?.length && !all) {
|
|
6483
|
+
return ok({ stagedFiles, deleted: false });
|
|
6146
6484
|
}
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
const stagingPath = `.soulguard/staging/${file.path}`;
|
|
6151
|
-
const copyResult = await ops.copyFile(file.path, stagingPath);
|
|
6152
|
-
if (copyResult.ok) {
|
|
6153
|
-
if (stagingOwnership) {
|
|
6154
|
-
await ops.chown(stagingPath, stagingOwnership);
|
|
6155
|
-
await ops.chmod(stagingPath, stagingOwnership.mode);
|
|
6156
|
-
}
|
|
6157
|
-
resetFiles.push(file.path);
|
|
6485
|
+
if (all) {
|
|
6486
|
+
for (const f of stagedFiles) {
|
|
6487
|
+
await ops.deleteFile(stagingPath(f));
|
|
6158
6488
|
}
|
|
6489
|
+
return ok({ stagedFiles, deleted: true });
|
|
6159
6490
|
}
|
|
6160
|
-
|
|
6491
|
+
const affected = [];
|
|
6492
|
+
for (const p of paths) {
|
|
6493
|
+
const matching = stagedFiles.filter((f) => f === p || f.startsWith(p + "/"));
|
|
6494
|
+
for (const f of matching) {
|
|
6495
|
+
await ops.deleteFile(stagingPath(f));
|
|
6496
|
+
affected.push(f);
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
return ok({ stagedFiles: affected, deleted: true });
|
|
6161
6500
|
}
|
|
6162
|
-
// ../core/src/
|
|
6163
|
-
function
|
|
6501
|
+
// ../core/src/sdk/protect-check.ts
|
|
6502
|
+
function isProtectedFile(protectFiles, filePath) {
|
|
6164
6503
|
const norm = normalizePath(filePath);
|
|
6165
|
-
return
|
|
6166
|
-
const
|
|
6167
|
-
|
|
6168
|
-
return matchGlob(normPattern, norm);
|
|
6169
|
-
}
|
|
6170
|
-
return norm === normPattern;
|
|
6504
|
+
return protectFiles.some((entry) => {
|
|
6505
|
+
const normEntry = normalizePath(entry);
|
|
6506
|
+
return norm === normEntry || norm.startsWith(normEntry + "/");
|
|
6171
6507
|
});
|
|
6172
6508
|
}
|
|
6173
6509
|
function normalizePath(p) {
|
|
@@ -6176,9 +6512,61 @@ function normalizePath(p) {
|
|
|
6176
6512
|
s = s.slice(2);
|
|
6177
6513
|
if (s.startsWith("/"))
|
|
6178
6514
|
s = s.slice(1);
|
|
6515
|
+
if (s.endsWith("/"))
|
|
6516
|
+
s = s.slice(0, -1);
|
|
6179
6517
|
return s;
|
|
6180
6518
|
}
|
|
6519
|
+
// ../core/src/sdk/tier.ts
|
|
6520
|
+
function setTier(config, files, tier) {
|
|
6521
|
+
const added = [];
|
|
6522
|
+
const moved = [];
|
|
6523
|
+
const alreadyInTier = [];
|
|
6524
|
+
const newFiles = { ...config.files };
|
|
6525
|
+
for (const file of files) {
|
|
6526
|
+
const currentTier = newFiles[file];
|
|
6527
|
+
if (currentTier === tier) {
|
|
6528
|
+
alreadyInTier.push(file);
|
|
6529
|
+
} else if (currentTier !== undefined) {
|
|
6530
|
+
moved.push(file);
|
|
6531
|
+
newFiles[file] = tier;
|
|
6532
|
+
} else {
|
|
6533
|
+
added.push(file);
|
|
6534
|
+
newFiles[file] = tier;
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
return {
|
|
6538
|
+
added,
|
|
6539
|
+
moved,
|
|
6540
|
+
alreadyInTier,
|
|
6541
|
+
config: { ...config, files: newFiles }
|
|
6542
|
+
};
|
|
6543
|
+
}
|
|
6544
|
+
function release(config, files) {
|
|
6545
|
+
const released = [];
|
|
6546
|
+
const notTracked = [];
|
|
6547
|
+
const newFiles = { ...config.files };
|
|
6548
|
+
for (const file of files) {
|
|
6549
|
+
if (newFiles[file] !== undefined) {
|
|
6550
|
+
released.push(file);
|
|
6551
|
+
delete newFiles[file];
|
|
6552
|
+
} else {
|
|
6553
|
+
notTracked.push(file);
|
|
6554
|
+
}
|
|
6555
|
+
}
|
|
6556
|
+
return {
|
|
6557
|
+
released,
|
|
6558
|
+
notTracked,
|
|
6559
|
+
config: { ...config, files: newFiles }
|
|
6560
|
+
};
|
|
6561
|
+
}
|
|
6181
6562
|
// ../core/src/cli/status-command.ts
|
|
6563
|
+
var STAGED_LABELS = {
|
|
6564
|
+
modified: "staged",
|
|
6565
|
+
created: "staged (new)",
|
|
6566
|
+
deleted: "staged (delete)",
|
|
6567
|
+
unchanged: "unchanged"
|
|
6568
|
+
};
|
|
6569
|
+
|
|
6182
6570
|
class StatusCommand {
|
|
6183
6571
|
opts;
|
|
6184
6572
|
out;
|
|
@@ -6187,62 +6575,30 @@ class StatusCommand {
|
|
|
6187
6575
|
this.out = out;
|
|
6188
6576
|
}
|
|
6189
6577
|
async execute() {
|
|
6190
|
-
const result = await status(this.opts);
|
|
6578
|
+
const result = await status({ tree: this.opts.tree });
|
|
6191
6579
|
if (!result.ok)
|
|
6192
6580
|
return 1;
|
|
6193
|
-
const {
|
|
6581
|
+
const { changed, drifts } = result.value;
|
|
6194
6582
|
this.out.heading(`Soulguard Status — ${this.opts.ops.workspace}`);
|
|
6195
6583
|
this.out.write("");
|
|
6196
|
-
|
|
6197
|
-
this.out.
|
|
6198
|
-
for (const
|
|
6199
|
-
this.
|
|
6584
|
+
for (const drift of drifts) {
|
|
6585
|
+
this.out.warn(` ⚠️ ${drift.entity.path} (${drift.entity.configTier})`);
|
|
6586
|
+
for (const issue of drift.details) {
|
|
6587
|
+
this.out.warn(` ${formatIssue(issue)}`);
|
|
6200
6588
|
}
|
|
6201
|
-
this.out.write("");
|
|
6202
6589
|
}
|
|
6203
|
-
|
|
6204
|
-
this.out.
|
|
6205
|
-
for (const f of ledger) {
|
|
6206
|
-
this.printFile(f);
|
|
6207
|
-
}
|
|
6208
|
-
this.out.write("");
|
|
6590
|
+
for (const file of changed) {
|
|
6591
|
+
this.out.info(` ${STAGED_LABELS[file.status]} ${file.path}`);
|
|
6209
6592
|
}
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
return issues.length > 0 ? 1 : 0;
|
|
6213
|
-
}
|
|
6214
|
-
printFile(f) {
|
|
6215
|
-
switch (f.status) {
|
|
6216
|
-
case "ok":
|
|
6217
|
-
this.out.success(` ✅ ${f.file.path}`);
|
|
6218
|
-
break;
|
|
6219
|
-
case "drifted":
|
|
6220
|
-
this.out.warn(` ⚠️ ${f.file.path}`);
|
|
6221
|
-
for (const issue of f.issues) {
|
|
6222
|
-
this.out.warn(` ${formatIssue(issue)}`);
|
|
6223
|
-
}
|
|
6224
|
-
break;
|
|
6225
|
-
case "missing":
|
|
6226
|
-
this.out.error(` ❌ ${f.path}`);
|
|
6227
|
-
break;
|
|
6228
|
-
case "error":
|
|
6229
|
-
this.out.error(` ❌ ${f.path} (${f.error.kind})`);
|
|
6230
|
-
break;
|
|
6593
|
+
if (drifts.length === 0 && changed.length === 0) {
|
|
6594
|
+
this.out.success("All files ok.");
|
|
6231
6595
|
}
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
let missing = 0;
|
|
6237
|
-
for (const f of files) {
|
|
6238
|
-
if (f.status === "ok")
|
|
6239
|
-
okCount++;
|
|
6240
|
-
else if (f.status === "drifted")
|
|
6241
|
-
drifted++;
|
|
6242
|
-
else if (f.status === "missing")
|
|
6243
|
-
missing++;
|
|
6596
|
+
this.out.write("");
|
|
6597
|
+
if (drifts.length > 0) {
|
|
6598
|
+
this.out.info(`${drifts.length} drifted`);
|
|
6599
|
+
return 1;
|
|
6244
6600
|
}
|
|
6245
|
-
return
|
|
6601
|
+
return 0;
|
|
6246
6602
|
}
|
|
6247
6603
|
}
|
|
6248
6604
|
// ../core/src/cli/sync-command.ts
|
|
@@ -6257,24 +6613,20 @@ class SyncCommand {
|
|
|
6257
6613
|
const result = await sync(this.opts);
|
|
6258
6614
|
if (!result.ok)
|
|
6259
6615
|
return 1;
|
|
6260
|
-
const {
|
|
6616
|
+
const { drifts, errors: errors2, git } = result.value;
|
|
6261
6617
|
this.out.heading(`Soulguard Sync — ${this.opts.ops.workspace}`);
|
|
6262
6618
|
this.out.write("");
|
|
6263
|
-
if (
|
|
6619
|
+
if (drifts.length === 0 && errors2.length === 0) {
|
|
6264
6620
|
this.out.success("Nothing to fix — all files ok.");
|
|
6265
6621
|
this.reportGit(git);
|
|
6266
6622
|
return 0;
|
|
6267
6623
|
}
|
|
6268
|
-
|
|
6269
|
-
const fixed = before.issues.filter((f) => !afterPaths.has(this.issuePath(f)));
|
|
6270
|
-
if (fixed.length > 0) {
|
|
6624
|
+
if (drifts.length > 0 && errors2.length === 0) {
|
|
6271
6625
|
this.out.heading("Fixed:");
|
|
6272
|
-
for (const
|
|
6273
|
-
this.out.success(` \uD83D\uDD27 ${
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
this.out.info(` ${formatIssue(issue)}`);
|
|
6277
|
-
}
|
|
6626
|
+
for (const d of drifts) {
|
|
6627
|
+
this.out.success(` \uD83D\uDD27 ${d.entity.path}`);
|
|
6628
|
+
for (const issue of d.details) {
|
|
6629
|
+
this.out.info(` ${formatIssue(issue)}`);
|
|
6278
6630
|
}
|
|
6279
6631
|
}
|
|
6280
6632
|
this.out.write("");
|
|
@@ -6285,47 +6637,17 @@ class SyncCommand {
|
|
|
6285
6637
|
this.out.error(` ❌ ${e.path}: ${e.operation} failed (${e.error.kind})`);
|
|
6286
6638
|
}
|
|
6287
6639
|
this.out.write("");
|
|
6640
|
+
return 1;
|
|
6288
6641
|
}
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
return 0;
|
|
6293
|
-
}
|
|
6294
|
-
this.out.warn(`${after.issues.length} issue(s) remaining after sync:`);
|
|
6295
|
-
for (const f of after.issues) {
|
|
6296
|
-
this.printFile(f);
|
|
6297
|
-
}
|
|
6298
|
-
return 1;
|
|
6642
|
+
this.out.success("All files now ok.");
|
|
6643
|
+
this.reportGit(git);
|
|
6644
|
+
return 0;
|
|
6299
6645
|
}
|
|
6300
6646
|
reportGit(git) {
|
|
6301
6647
|
if (git?.committed) {
|
|
6302
6648
|
this.out.success(` \uD83D\uDCDD Committed ${git.files.length} file(s) to git`);
|
|
6303
6649
|
}
|
|
6304
6650
|
}
|
|
6305
|
-
issuePath(f) {
|
|
6306
|
-
switch (f.status) {
|
|
6307
|
-
case "ok":
|
|
6308
|
-
return f.file.path;
|
|
6309
|
-
case "drifted":
|
|
6310
|
-
return f.file.path;
|
|
6311
|
-
case "missing":
|
|
6312
|
-
case "error":
|
|
6313
|
-
return f.path;
|
|
6314
|
-
}
|
|
6315
|
-
}
|
|
6316
|
-
printFile(f) {
|
|
6317
|
-
switch (f.status) {
|
|
6318
|
-
case "drifted":
|
|
6319
|
-
this.out.warn(` ⚠️ ${f.file.path}`);
|
|
6320
|
-
break;
|
|
6321
|
-
case "missing":
|
|
6322
|
-
this.out.error(` ❌ ${f.path}`);
|
|
6323
|
-
break;
|
|
6324
|
-
case "error":
|
|
6325
|
-
this.out.error(` ❌ ${f.path} (${f.error.kind})`);
|
|
6326
|
-
break;
|
|
6327
|
-
}
|
|
6328
|
-
}
|
|
6329
6651
|
}
|
|
6330
6652
|
// ../core/src/cli/diff-command.ts
|
|
6331
6653
|
class DiffCommand {
|
|
@@ -6338,56 +6660,34 @@ class DiffCommand {
|
|
|
6338
6660
|
async execute() {
|
|
6339
6661
|
const result = await diff(this.options);
|
|
6340
6662
|
if (!result.ok) {
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
this.out.error("No staging directory found. Run `soulguard init` first.");
|
|
6344
|
-
return 1;
|
|
6345
|
-
case "no_config":
|
|
6346
|
-
this.out.error("No soulguard.json found.");
|
|
6347
|
-
return 1;
|
|
6348
|
-
case "read_failed":
|
|
6349
|
-
this.out.error(`Failed to read ${result.error.path}: ${result.error.message}`);
|
|
6350
|
-
return 1;
|
|
6351
|
-
}
|
|
6663
|
+
this.out.error(`Failed: ${result.error.message}`);
|
|
6664
|
+
return 1;
|
|
6352
6665
|
}
|
|
6353
6666
|
const { files, hasChanges } = result.value;
|
|
6354
6667
|
this.out.heading(`Soulguard Diff — ${this.options.ops.workspace}`);
|
|
6355
6668
|
this.out.write("");
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
switch (file.status) {
|
|
6359
|
-
case "unchanged":
|
|
6360
|
-
this.out.info(` ✅ ${file.path} (no changes)`);
|
|
6361
|
-
break;
|
|
6669
|
+
for (const entry of files) {
|
|
6670
|
+
switch (entry.file.status) {
|
|
6362
6671
|
case "modified":
|
|
6363
|
-
|
|
6364
|
-
this.out.warn(` \uD83D\uDCDD ${file.path}`);
|
|
6365
|
-
if (file.diff) {
|
|
6366
|
-
for (const line of file.diff.split(`
|
|
6367
|
-
`)) {
|
|
6368
|
-
this.out.write(` ${line}`);
|
|
6369
|
-
}
|
|
6370
|
-
}
|
|
6672
|
+
this.out.warn(` \uD83D\uDCDD ${entry.file.path}`);
|
|
6371
6673
|
break;
|
|
6372
|
-
case "
|
|
6373
|
-
|
|
6374
|
-
this.out.warn(` ⚠️ ${file.path} (no staging copy)`);
|
|
6375
|
-
break;
|
|
6376
|
-
case "vault_missing":
|
|
6377
|
-
changeCount++;
|
|
6378
|
-
this.out.warn(` ⚠️ ${file.path} (vault file missing — new file)`);
|
|
6674
|
+
case "created":
|
|
6675
|
+
this.out.warn(` ⚠️ ${entry.file.path} (new file)`);
|
|
6379
6676
|
break;
|
|
6380
6677
|
case "deleted":
|
|
6381
|
-
|
|
6382
|
-
this.out.warn(` \uD83D\uDDD1️ ${file.path} (staged for deletion)`);
|
|
6678
|
+
this.out.warn(` \uD83D\uDDD1️ ${entry.file.path} (staged for deletion)`);
|
|
6383
6679
|
break;
|
|
6384
6680
|
}
|
|
6681
|
+
for (const line of entry.diff.split(`
|
|
6682
|
+
`)) {
|
|
6683
|
+
this.out.write(` ${line}`);
|
|
6684
|
+
}
|
|
6385
6685
|
}
|
|
6386
6686
|
this.out.write("");
|
|
6387
6687
|
if (hasChanges) {
|
|
6388
|
-
this.out.info(`${
|
|
6688
|
+
this.out.info(`${files.length} file(s) changed`);
|
|
6389
6689
|
if (result.value.approvalHash) {
|
|
6390
|
-
this.out.info(`
|
|
6690
|
+
this.out.info(`Apply hash: ${result.value.approvalHash}`);
|
|
6391
6691
|
}
|
|
6392
6692
|
} else {
|
|
6393
6693
|
this.out.info("No changes");
|
|
@@ -6395,8 +6695,8 @@ class DiffCommand {
|
|
|
6395
6695
|
return hasChanges ? 1 : 0;
|
|
6396
6696
|
}
|
|
6397
6697
|
}
|
|
6398
|
-
// ../core/src/cli/
|
|
6399
|
-
class
|
|
6698
|
+
// ../core/src/cli/apply-command.ts
|
|
6699
|
+
class ApplyCommand {
|
|
6400
6700
|
opts;
|
|
6401
6701
|
out;
|
|
6402
6702
|
constructor(opts, out) {
|
|
@@ -6405,25 +6705,32 @@ class ApproveCommand {
|
|
|
6405
6705
|
}
|
|
6406
6706
|
async execute() {
|
|
6407
6707
|
let hash = this.opts.hash;
|
|
6408
|
-
if (
|
|
6409
|
-
|
|
6708
|
+
if (this.opts.skipHashVerification) {
|
|
6709
|
+
if (hash) {
|
|
6710
|
+
this.out.error("Cannot use both --yes and --hash flags");
|
|
6711
|
+
return 1;
|
|
6712
|
+
}
|
|
6713
|
+
hash = undefined;
|
|
6714
|
+
} else if (!hash) {
|
|
6715
|
+
const diffResult = await diff({
|
|
6716
|
+
tree: this.opts.tree,
|
|
6717
|
+
ops: this.opts.ops
|
|
6718
|
+
});
|
|
6410
6719
|
if (!diffResult.ok) {
|
|
6411
|
-
this.out.error(`Diff failed: ${diffResult.error.
|
|
6720
|
+
this.out.error(`Diff failed: ${diffResult.error.message}`);
|
|
6412
6721
|
return 1;
|
|
6413
6722
|
}
|
|
6414
6723
|
if (!diffResult.value.hasChanges) {
|
|
6415
|
-
this.out.info("No changes to
|
|
6724
|
+
this.out.info("No changes to apply — staging matches protected files.");
|
|
6416
6725
|
return 0;
|
|
6417
6726
|
}
|
|
6418
|
-
this.out.heading(`Soulguard
|
|
6727
|
+
this.out.heading(`Soulguard Apply — ${this.opts.ops.workspace}`);
|
|
6419
6728
|
this.out.write("");
|
|
6420
|
-
for (const
|
|
6421
|
-
|
|
6422
|
-
this.out.write(file.diff);
|
|
6423
|
-
}
|
|
6729
|
+
for (const entry of diffResult.value.files) {
|
|
6730
|
+
this.out.write(entry.diff);
|
|
6424
6731
|
}
|
|
6425
6732
|
this.out.write("");
|
|
6426
|
-
this.out.info(`
|
|
6733
|
+
this.out.info(`Apply hash: ${diffResult.value.approvalHash}`);
|
|
6427
6734
|
this.out.write("");
|
|
6428
6735
|
if (this.opts.prompt) {
|
|
6429
6736
|
const confirmed = await this.opts.prompt();
|
|
@@ -6434,12 +6741,14 @@ class ApproveCommand {
|
|
|
6434
6741
|
}
|
|
6435
6742
|
hash = diffResult.value.approvalHash;
|
|
6436
6743
|
}
|
|
6437
|
-
const result = await
|
|
6744
|
+
const result = await apply({
|
|
6745
|
+
ops: this.opts.ops,
|
|
6746
|
+
tree: this.opts.tree,
|
|
6747
|
+
hash,
|
|
6748
|
+
policies: this.opts.policies
|
|
6749
|
+
});
|
|
6438
6750
|
if (!result.ok) {
|
|
6439
6751
|
switch (result.error.kind) {
|
|
6440
|
-
case "no_changes":
|
|
6441
|
-
this.out.info("No changes to approve — staging matches vault.");
|
|
6442
|
-
return 0;
|
|
6443
6752
|
case "hash_mismatch":
|
|
6444
6753
|
this.out.error(result.error.message);
|
|
6445
6754
|
this.out.info("Please run `soulguard diff` again and re-review.");
|
|
@@ -6459,18 +6768,15 @@ class ApproveCommand {
|
|
|
6459
6768
|
case "apply_failed":
|
|
6460
6769
|
this.out.error(`Apply failed: ${result.error.message}`);
|
|
6461
6770
|
return 1;
|
|
6462
|
-
case "diff_failed":
|
|
6463
|
-
this.out.error(`Diff failed: ${result.error.message}`);
|
|
6464
|
-
return 1;
|
|
6465
6771
|
}
|
|
6466
6772
|
}
|
|
6467
6773
|
this.out.write("");
|
|
6468
|
-
this.out.success(`
|
|
6774
|
+
this.out.success(`Applied ${result.value.appliedFiles.length} file(s):`);
|
|
6469
6775
|
for (const file of result.value.appliedFiles) {
|
|
6470
6776
|
this.out.success(` ✅ ${file}`);
|
|
6471
6777
|
}
|
|
6472
6778
|
this.out.write("");
|
|
6473
|
-
this.out.info("
|
|
6779
|
+
this.out.info("Protected files updated. Staging synced.");
|
|
6474
6780
|
return 0;
|
|
6475
6781
|
}
|
|
6476
6782
|
}
|
|
@@ -6488,118 +6794,852 @@ class ResetCommand {
|
|
|
6488
6794
|
this.out.error(`Reset failed: ${result.error.message}`);
|
|
6489
6795
|
return 1;
|
|
6490
6796
|
}
|
|
6491
|
-
|
|
6492
|
-
|
|
6797
|
+
const { stagedFiles, deleted } = result.value;
|
|
6798
|
+
if (stagedFiles.length === 0) {
|
|
6799
|
+
this.out.info("Nothing staged — staging tree is clean.");
|
|
6493
6800
|
return 0;
|
|
6494
6801
|
}
|
|
6495
|
-
|
|
6802
|
+
if (!deleted) {
|
|
6803
|
+
this.out.write("Staged changes:");
|
|
6804
|
+
for (const f of stagedFiles) {
|
|
6805
|
+
this.out.write(` ${STAGING_DIR}/${f}`);
|
|
6806
|
+
}
|
|
6807
|
+
this.out.write("");
|
|
6808
|
+
this.out.write("Use --all to reset everything, or specify paths to reset.");
|
|
6809
|
+
return 0;
|
|
6810
|
+
}
|
|
6811
|
+
this.out.success(`Reset ${stagedFiles.length} staged file(s):`);
|
|
6812
|
+
for (const f of stagedFiles) {
|
|
6813
|
+
this.out.write(` ${STAGING_DIR}/${f}`);
|
|
6814
|
+
}
|
|
6815
|
+
return 0;
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6818
|
+
// ../core/src/cli/tier-command.ts
|
|
6819
|
+
function formatSummary(paths) {
|
|
6820
|
+
const dirs = paths.filter((p) => p.endsWith("/")).length;
|
|
6821
|
+
const files = paths.length - dirs;
|
|
6822
|
+
const parts = [];
|
|
6823
|
+
if (files > 0)
|
|
6824
|
+
parts.push(`${files} ${files === 1 ? "file" : "files"}`);
|
|
6825
|
+
if (dirs > 0)
|
|
6826
|
+
parts.push(`${dirs} ${dirs === 1 ? "directory" : "directories"}`);
|
|
6827
|
+
return parts.join(" and ");
|
|
6828
|
+
}
|
|
6829
|
+
function formatChange(action, from) {
|
|
6830
|
+
if (action.kind === "release") {
|
|
6831
|
+
return { prefix: "-", suffix: "(released)" };
|
|
6832
|
+
}
|
|
6833
|
+
const tier = action.tier;
|
|
6834
|
+
if (from === undefined) {
|
|
6835
|
+
return { prefix: "+", suffix: `→ ${tier}` };
|
|
6836
|
+
}
|
|
6837
|
+
const arrow = from === "watch" ? "↑" : "↓";
|
|
6838
|
+
return { prefix: arrow, suffix: `→ ${tier} (was ${from})` };
|
|
6839
|
+
}
|
|
6840
|
+
async function isDirectory(ops, path) {
|
|
6841
|
+
const stat = await ops.stat(path);
|
|
6842
|
+
return stat.ok && stat.value.isDirectory;
|
|
6843
|
+
}
|
|
6844
|
+
async function enforceProtect(ops, path, ownership) {
|
|
6845
|
+
const isDir = await isDirectory(ops, path);
|
|
6846
|
+
if (isDir) {
|
|
6847
|
+
const chown = await ops.chownRecursive(path, { user: ownership.user, group: ownership.group });
|
|
6848
|
+
if (!chown.ok)
|
|
6849
|
+
return { ok: false, error: `chown ${path}: ${chown.error.kind}` };
|
|
6850
|
+
const chmod = await ops.chmodDirectoryTree(path, { fileMode: ownership.mode, dirMode: "555" });
|
|
6851
|
+
if (!chmod.ok)
|
|
6852
|
+
return { ok: false, error: `chmod ${path}: ${chmod.error.kind}` };
|
|
6853
|
+
} else {
|
|
6854
|
+
const chown = await ops.chown(path, { user: ownership.user, group: ownership.group });
|
|
6855
|
+
if (!chown.ok)
|
|
6856
|
+
return { ok: false, error: `chown ${path}: ${chown.error.kind}` };
|
|
6857
|
+
const chmod = await ops.chmod(path, ownership.mode);
|
|
6858
|
+
if (!chmod.ok)
|
|
6859
|
+
return { ok: false, error: `chmod ${path}: ${chmod.error.kind}` };
|
|
6860
|
+
}
|
|
6861
|
+
return { ok: true };
|
|
6862
|
+
}
|
|
6863
|
+
async function restoreOwnership(ops, path, ownership) {
|
|
6864
|
+
const isDir = await isDirectory(ops, path);
|
|
6865
|
+
if (isDir) {
|
|
6866
|
+
await ops.chownRecursive(path, { user: ownership.user, group: ownership.group });
|
|
6867
|
+
await ops.chmodRecursive(path, ownership.mode);
|
|
6868
|
+
} else {
|
|
6869
|
+
await ops.chown(path, { user: ownership.user, group: ownership.group });
|
|
6870
|
+
await ops.chmod(path, ownership.mode);
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
|
|
6874
|
+
class TierCommand {
|
|
6875
|
+
opts;
|
|
6876
|
+
out;
|
|
6877
|
+
constructor(opts, out) {
|
|
6878
|
+
this.opts = opts;
|
|
6879
|
+
this.out = out;
|
|
6880
|
+
}
|
|
6881
|
+
async execute() {
|
|
6882
|
+
const { ops, files, action } = this.opts;
|
|
6883
|
+
if (files.length === 0) {
|
|
6884
|
+
this.out.error("No files specified.");
|
|
6885
|
+
return 1;
|
|
6886
|
+
}
|
|
6887
|
+
const configResult = await readConfig(ops);
|
|
6888
|
+
if (!configResult.ok) {
|
|
6889
|
+
const e = configResult.error;
|
|
6890
|
+
if (e.kind === "not_found") {
|
|
6891
|
+
this.out.error("Failed to read config: soulguard.json not found");
|
|
6892
|
+
} else {
|
|
6893
|
+
this.out.error(`Failed to read config: ${e.message}`);
|
|
6894
|
+
}
|
|
6895
|
+
return 1;
|
|
6896
|
+
}
|
|
6897
|
+
let config;
|
|
6898
|
+
let changedPaths;
|
|
6899
|
+
const createdPaths = new Set;
|
|
6900
|
+
if (action.kind === "set") {
|
|
6901
|
+
for (const file of files) {
|
|
6902
|
+
const exists = await ops.exists(file);
|
|
6903
|
+
if (exists.ok && exists.value)
|
|
6904
|
+
continue;
|
|
6905
|
+
if (file.endsWith("/")) {
|
|
6906
|
+
const mk = await ops.exec("mkdir", ["-p", file]);
|
|
6907
|
+
if (!mk.ok) {
|
|
6908
|
+
this.out.error(`Failed to create directory ${file}: ${mk.error.message}`);
|
|
6909
|
+
return 1;
|
|
6910
|
+
}
|
|
6911
|
+
} else {
|
|
6912
|
+
const parent = file.includes("/") ? file.slice(0, file.lastIndexOf("/")) : null;
|
|
6913
|
+
if (parent) {
|
|
6914
|
+
await ops.exec("mkdir", ["-p", parent]);
|
|
6915
|
+
}
|
|
6916
|
+
const wr = await ops.writeFile(file, "");
|
|
6917
|
+
if (!wr.ok) {
|
|
6918
|
+
this.out.error(`Failed to create file ${file}: ${wr.error.kind}`);
|
|
6919
|
+
return 1;
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
if (action.tier === "watch") {
|
|
6923
|
+
const defaultOwnership = configResult.value.defaultOwnership;
|
|
6924
|
+
if (defaultOwnership) {
|
|
6925
|
+
const owner = { user: defaultOwnership.user, group: defaultOwnership.group };
|
|
6926
|
+
if (file.endsWith("/")) {
|
|
6927
|
+
await ops.chownRecursive(file, owner);
|
|
6928
|
+
await ops.chmodRecursive(file, "755");
|
|
6929
|
+
} else {
|
|
6930
|
+
await ops.chown(file, owner);
|
|
6931
|
+
await ops.chmod(file, defaultOwnership.mode);
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
}
|
|
6935
|
+
createdPaths.add(file);
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
if (action.kind === "set") {
|
|
6939
|
+
const result = setTier(configResult.value, files, action.tier);
|
|
6940
|
+
config = result.config;
|
|
6941
|
+
changedPaths = [...result.added, ...result.moved];
|
|
6942
|
+
for (const f of result.added) {
|
|
6943
|
+
const fmt = formatChange(action);
|
|
6944
|
+
const created = createdPaths.has(f) ? " (created)" : "";
|
|
6945
|
+
this.out.success(` ${fmt.prefix} ${f} ${fmt.suffix}${created}`);
|
|
6946
|
+
}
|
|
6947
|
+
for (const f of result.moved) {
|
|
6948
|
+
const oldTier = configResult.value.files[f];
|
|
6949
|
+
const fmt = formatChange(action, oldTier);
|
|
6950
|
+
this.out.info(` ${fmt.prefix} ${f} ${fmt.suffix}`);
|
|
6951
|
+
}
|
|
6952
|
+
for (const f of result.alreadyInTier) {
|
|
6953
|
+
this.out.info(` · ${f} (already ${action.tier})`);
|
|
6954
|
+
}
|
|
6955
|
+
} else {
|
|
6956
|
+
const result = release(configResult.value, files);
|
|
6957
|
+
config = result.config;
|
|
6958
|
+
changedPaths = result.released;
|
|
6959
|
+
for (const f of result.released) {
|
|
6960
|
+
this.out.success(` - ${f} (released)`);
|
|
6961
|
+
}
|
|
6962
|
+
for (const f of result.notTracked) {
|
|
6963
|
+
this.out.info(` · ${f} (not tracked)`);
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
if (changedPaths.length === 0) {
|
|
6967
|
+
this.out.info("Nothing to change.");
|
|
6968
|
+
return 0;
|
|
6969
|
+
}
|
|
6970
|
+
const writeResult = await writeConfig(ops, config);
|
|
6971
|
+
if (!writeResult.ok) {
|
|
6972
|
+
this.out.error(`Failed to write config: ${writeResult.error.message}`);
|
|
6973
|
+
return 1;
|
|
6974
|
+
}
|
|
6975
|
+
if (action.kind === "set" && action.tier === "protect") {
|
|
6976
|
+
const expectedProtectOwnership = getProtectOwnership(configResult.value.guardian);
|
|
6977
|
+
for (const file of changedPaths) {
|
|
6978
|
+
const result = await enforceProtect(ops, file, expectedProtectOwnership);
|
|
6979
|
+
if (!result.ok) {
|
|
6980
|
+
this.out.error(`Failed to enforce: ${result.error}`);
|
|
6981
|
+
return 1;
|
|
6982
|
+
}
|
|
6983
|
+
}
|
|
6984
|
+
} else if (action.kind === "set" && action.tier === "watch") {
|
|
6985
|
+
const defaultOwnership = configResult.value.defaultOwnership;
|
|
6986
|
+
for (const file of changedPaths) {
|
|
6987
|
+
const wasTier = configResult.value.files[file];
|
|
6988
|
+
if (wasTier === "protect" && defaultOwnership) {
|
|
6989
|
+
await restoreOwnership(ops, file, defaultOwnership);
|
|
6990
|
+
}
|
|
6991
|
+
}
|
|
6992
|
+
} else if (action.kind === "release") {
|
|
6993
|
+
const defaultOwnership = configResult.value.defaultOwnership;
|
|
6994
|
+
for (const file of changedPaths) {
|
|
6995
|
+
const wasTier = configResult.value.files[file];
|
|
6996
|
+
if (wasTier === "protect" && defaultOwnership) {
|
|
6997
|
+
await restoreOwnership(ops, file, defaultOwnership);
|
|
6998
|
+
}
|
|
6999
|
+
const sibling = stagingPath(file);
|
|
7000
|
+
const siblingExists = await ops.exists(sibling);
|
|
7001
|
+
if (siblingExists.ok && siblingExists.value) {
|
|
7002
|
+
await ops.deleteFile(sibling);
|
|
7003
|
+
}
|
|
7004
|
+
}
|
|
7005
|
+
}
|
|
7006
|
+
if (await isGitEnabled(ops, config)) {
|
|
7007
|
+
const gitFiles = ["soulguard.json", ...changedPaths];
|
|
7008
|
+
const verb = action.kind === "set" ? action.tier : "release";
|
|
7009
|
+
await gitCommit(ops, gitFiles, `soulguard: ${verb} ${changedPaths.join(", ")}`);
|
|
7010
|
+
}
|
|
6496
7011
|
this.out.write("");
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
this.out.
|
|
7012
|
+
if (action.kind === "set") {
|
|
7013
|
+
const label = action.tier === "protect" ? "protected" : "watched";
|
|
7014
|
+
this.out.success(`Updated. ${formatSummary(changedPaths)} now ${label}.`);
|
|
7015
|
+
} else {
|
|
7016
|
+
this.out.success(`Released. ${formatSummary(changedPaths)} untracked.`);
|
|
7017
|
+
}
|
|
7018
|
+
return 0;
|
|
7019
|
+
}
|
|
7020
|
+
}
|
|
7021
|
+
// ../core/src/sdk/stage.ts
|
|
7022
|
+
import { dirname as dirname3 } from "node:path";
|
|
7023
|
+
function isInProtectTier(path, protectPatterns2) {
|
|
7024
|
+
for (const pattern of protectPatterns2) {
|
|
7025
|
+
const normalized = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
|
|
7026
|
+
if (path === normalized || path === pattern)
|
|
7027
|
+
return true;
|
|
7028
|
+
if (path.startsWith(normalized + "/"))
|
|
7029
|
+
return true;
|
|
7030
|
+
}
|
|
7031
|
+
return false;
|
|
7032
|
+
}
|
|
7033
|
+
async function ensureParentDir(ops, path) {
|
|
7034
|
+
const parentDir = dirname3(path);
|
|
7035
|
+
if (parentDir === "." || parentDir === "/") {
|
|
7036
|
+
return ok(undefined);
|
|
7037
|
+
}
|
|
7038
|
+
const mkdirResult = await ops.mkdir(parentDir);
|
|
7039
|
+
if (!mkdirResult.ok) {
|
|
7040
|
+
return err(`Cannot create parent directory ${parentDir}: ${mkdirResult.error.kind}`);
|
|
7041
|
+
}
|
|
7042
|
+
return ok(undefined);
|
|
7043
|
+
}
|
|
7044
|
+
async function stage(options) {
|
|
7045
|
+
const { ops, config, path, delete: isDelete } = options;
|
|
7046
|
+
const protectFiles = protectPatterns(config);
|
|
7047
|
+
if (!isInProtectTier(path, protectFiles)) {
|
|
7048
|
+
return err({ kind: "not_in_protect_tier", path });
|
|
7049
|
+
}
|
|
7050
|
+
const stagedFiles = [];
|
|
7051
|
+
const pathStat = await ops.stat(path);
|
|
7052
|
+
if (!pathStat.ok && pathStat.error.kind !== "not_found") {
|
|
7053
|
+
return err({
|
|
7054
|
+
kind: "stage_failed",
|
|
7055
|
+
path,
|
|
7056
|
+
message: `Cannot stat path: ${pathStat.error.kind}`
|
|
7057
|
+
});
|
|
7058
|
+
}
|
|
7059
|
+
const isDirectory2 = pathStat.ok && pathStat.value.isDirectory;
|
|
7060
|
+
if (isDirectory2 && isDelete) {
|
|
7061
|
+
const stagePath = stagingPath(path);
|
|
7062
|
+
const existsResult = await ops.exists(stagePath);
|
|
7063
|
+
if (existsResult.ok && existsResult.value) {
|
|
7064
|
+
const readResult = await ops.readFile(stagePath);
|
|
7065
|
+
if (readResult.ok && isDeleteSentinel(readResult.value)) {
|
|
7066
|
+
return ok({ stagedFiles: [] });
|
|
7067
|
+
}
|
|
7068
|
+
}
|
|
7069
|
+
const parentResult = await ensureParentDir(ops, stagePath);
|
|
7070
|
+
if (!parentResult.ok) {
|
|
7071
|
+
return err({ kind: "stage_failed", path, message: parentResult.error });
|
|
7072
|
+
}
|
|
7073
|
+
const writeResult = await ops.writeFile(stagePath, JSON.stringify(DELETE_SENTINEL, null, 2));
|
|
7074
|
+
if (!writeResult.ok) {
|
|
7075
|
+
return err({
|
|
7076
|
+
kind: "stage_failed",
|
|
7077
|
+
path,
|
|
7078
|
+
message: `Cannot write delete sentinel: ${writeResult.error.kind}`
|
|
7079
|
+
});
|
|
7080
|
+
}
|
|
7081
|
+
stagedFiles.push({ path, action: "delete" });
|
|
7082
|
+
} else if (isDirectory2 && !isDelete) {
|
|
7083
|
+
const listResult = await ops.listDir(path);
|
|
7084
|
+
if (!listResult.ok) {
|
|
7085
|
+
return err({
|
|
7086
|
+
kind: "stage_failed",
|
|
7087
|
+
path,
|
|
7088
|
+
message: `Cannot list directory: ${listResult.error.kind}`
|
|
7089
|
+
});
|
|
7090
|
+
}
|
|
7091
|
+
for (const filePath of listResult.value) {
|
|
7092
|
+
const result = await stage({ ops, config, path: filePath, delete: isDelete });
|
|
7093
|
+
if (!result.ok) {
|
|
7094
|
+
return result;
|
|
7095
|
+
}
|
|
7096
|
+
stagedFiles.push(...result.value.stagedFiles);
|
|
7097
|
+
}
|
|
7098
|
+
} else if (!isDirectory2 && isDelete) {
|
|
7099
|
+
const stagePath = stagingPath(path);
|
|
7100
|
+
const existsResult = await ops.exists(stagePath);
|
|
7101
|
+
if (existsResult.ok && existsResult.value) {
|
|
7102
|
+
const readResult = await ops.readFile(stagePath);
|
|
7103
|
+
if (readResult.ok && isDeleteSentinel(readResult.value)) {
|
|
7104
|
+
return ok({ stagedFiles: [] });
|
|
7105
|
+
}
|
|
7106
|
+
}
|
|
7107
|
+
const parentResult = await ensureParentDir(ops, stagePath);
|
|
7108
|
+
if (!parentResult.ok) {
|
|
7109
|
+
return err({ kind: "stage_failed", path, message: parentResult.error });
|
|
7110
|
+
}
|
|
7111
|
+
const writeResult = await ops.writeFile(stagePath, JSON.stringify(DELETE_SENTINEL, null, 2));
|
|
7112
|
+
if (!writeResult.ok) {
|
|
7113
|
+
return err({
|
|
7114
|
+
kind: "stage_failed",
|
|
7115
|
+
path,
|
|
7116
|
+
message: `Cannot write delete sentinel: ${writeResult.error.kind}`
|
|
7117
|
+
});
|
|
7118
|
+
}
|
|
7119
|
+
stagedFiles.push({ path, action: "delete" });
|
|
7120
|
+
} else if (!isDirectory2 && !isDelete) {
|
|
7121
|
+
const stagePath = stagingPath(path);
|
|
7122
|
+
const existsResult = await ops.exists(stagePath);
|
|
7123
|
+
if (existsResult.ok && existsResult.value) {
|
|
7124
|
+
const readResult = await ops.readFile(stagePath);
|
|
7125
|
+
if (readResult.ok && isDeleteSentinel(readResult.value)) {} else {
|
|
7126
|
+
return ok({ stagedFiles: [] });
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
const parentResult = await ensureParentDir(ops, stagePath);
|
|
7130
|
+
if (!parentResult.ok) {
|
|
7131
|
+
return err({ kind: "stage_failed", path, message: parentResult.error });
|
|
7132
|
+
}
|
|
7133
|
+
const sourceExists = await ops.exists(path);
|
|
7134
|
+
if (sourceExists.ok && sourceExists.value) {
|
|
7135
|
+
const readResult = await ops.readFile(path);
|
|
7136
|
+
if (!readResult.ok) {
|
|
7137
|
+
return err({
|
|
7138
|
+
kind: "stage_failed",
|
|
7139
|
+
path,
|
|
7140
|
+
message: `Cannot read file: ${readResult.error.kind}`
|
|
7141
|
+
});
|
|
7142
|
+
}
|
|
7143
|
+
const writeResult = await ops.writeFile(stagePath, readResult.value);
|
|
7144
|
+
if (!writeResult.ok) {
|
|
7145
|
+
return err({
|
|
7146
|
+
kind: "stage_failed",
|
|
7147
|
+
path,
|
|
7148
|
+
message: `Cannot write staging copy: ${writeResult.error.kind}`
|
|
7149
|
+
});
|
|
7150
|
+
}
|
|
7151
|
+
} else {
|
|
7152
|
+
const writeResult = await ops.writeFile(stagePath, "");
|
|
7153
|
+
if (!writeResult.ok) {
|
|
7154
|
+
return err({
|
|
7155
|
+
kind: "stage_failed",
|
|
7156
|
+
path,
|
|
7157
|
+
message: `Cannot create empty staging file: ${writeResult.error.kind}`
|
|
7158
|
+
});
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
stagedFiles.push({ path, action: "edit" });
|
|
7162
|
+
}
|
|
7163
|
+
return ok({ stagedFiles });
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
// ../core/src/cli/stage-command.ts
|
|
7167
|
+
class StageCommand {
|
|
7168
|
+
opts;
|
|
7169
|
+
out;
|
|
7170
|
+
constructor(opts, out) {
|
|
7171
|
+
this.opts = opts;
|
|
7172
|
+
this.out = out;
|
|
7173
|
+
}
|
|
7174
|
+
async execute() {
|
|
7175
|
+
const { ops, config, paths, delete: isDelete } = this.opts;
|
|
7176
|
+
if (paths.length === 0) {
|
|
7177
|
+
this.out.error("No paths specified.");
|
|
7178
|
+
return 1;
|
|
7179
|
+
}
|
|
7180
|
+
const allStagedFiles = [];
|
|
7181
|
+
const alreadyStaged = [];
|
|
7182
|
+
for (const path of paths) {
|
|
7183
|
+
const result = await stage({ ops, config, path, delete: isDelete });
|
|
7184
|
+
if (!result.ok) {
|
|
7185
|
+
switch (result.error.kind) {
|
|
7186
|
+
case "not_in_protect_tier":
|
|
7187
|
+
this.out.error(`${result.error.path} is not in the protect tier.`);
|
|
7188
|
+
return 1;
|
|
7189
|
+
case "stage_failed":
|
|
7190
|
+
this.out.error(`Failed to stage ${result.error.path}: ${result.error.message}`);
|
|
7191
|
+
return 1;
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
if (result.value.stagedFiles.length === 0) {
|
|
7195
|
+
alreadyStaged.push(path);
|
|
7196
|
+
} else {
|
|
7197
|
+
allStagedFiles.push(...result.value.stagedFiles);
|
|
7198
|
+
}
|
|
7199
|
+
}
|
|
7200
|
+
for (const file of alreadyStaged) {
|
|
7201
|
+
this.out.info(` · ${file} (already staged)`);
|
|
7202
|
+
}
|
|
7203
|
+
for (const { path, action } of allStagedFiles) {
|
|
7204
|
+
if (action === "delete") {
|
|
7205
|
+
this.out.success(` \uD83D\uDDD1️ ${path} (staged for deletion)`);
|
|
7206
|
+
} else {
|
|
7207
|
+
this.out.success(` \uD83D\uDCDD ${path} (staged for editing)`);
|
|
7208
|
+
}
|
|
7209
|
+
}
|
|
7210
|
+
if (allStagedFiles.length === 0) {
|
|
7211
|
+
this.out.info("Nothing to stage.");
|
|
7212
|
+
} else {
|
|
7213
|
+
this.out.write("");
|
|
7214
|
+
this.out.success(`Staged ${allStagedFiles.length} file(s).`);
|
|
6500
7215
|
}
|
|
6501
7216
|
return 0;
|
|
6502
7217
|
}
|
|
6503
7218
|
}
|
|
7219
|
+
// ../core/src/daemon/proposal-manager.ts
|
|
7220
|
+
import { EventEmitter } from "node:events";
|
|
7221
|
+
class ProposalManager extends EventEmitter {
|
|
7222
|
+
_ops;
|
|
7223
|
+
_config;
|
|
7224
|
+
_channel;
|
|
7225
|
+
_pollIntervalMs;
|
|
7226
|
+
_activeProposal = null;
|
|
7227
|
+
_abortController = null;
|
|
7228
|
+
_pendingFlow = null;
|
|
7229
|
+
_running = false;
|
|
7230
|
+
_pollTimer = null;
|
|
7231
|
+
_pendingProposalHash = null;
|
|
7232
|
+
constructor(options) {
|
|
7233
|
+
super();
|
|
7234
|
+
this._ops = options.ops;
|
|
7235
|
+
this._config = options.config;
|
|
7236
|
+
this._channel = options.channel;
|
|
7237
|
+
this._pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
7238
|
+
}
|
|
7239
|
+
get activeProposal() {
|
|
7240
|
+
return this._activeProposal;
|
|
7241
|
+
}
|
|
7242
|
+
get running() {
|
|
7243
|
+
return this._running;
|
|
7244
|
+
}
|
|
7245
|
+
start() {
|
|
7246
|
+
if (this._running)
|
|
7247
|
+
return;
|
|
7248
|
+
this._running = true;
|
|
7249
|
+
this._pendingProposalHash = null;
|
|
7250
|
+
this._poll();
|
|
7251
|
+
this._pollTimer = setInterval(() => this._poll(), this._pollIntervalMs);
|
|
7252
|
+
}
|
|
7253
|
+
async stop() {
|
|
7254
|
+
if (!this._running)
|
|
7255
|
+
return;
|
|
7256
|
+
this._running = false;
|
|
7257
|
+
if (this._pollTimer) {
|
|
7258
|
+
clearInterval(this._pollTimer);
|
|
7259
|
+
this._pollTimer = null;
|
|
7260
|
+
}
|
|
7261
|
+
await this._abortPending();
|
|
7262
|
+
}
|
|
7263
|
+
async onStagingReady() {
|
|
7264
|
+
this._pendingFlow = this._propose();
|
|
7265
|
+
await this._pendingFlow;
|
|
7266
|
+
}
|
|
7267
|
+
async _poll() {
|
|
7268
|
+
try {
|
|
7269
|
+
const treeResult = await StateTree.build({
|
|
7270
|
+
ops: this._ops,
|
|
7271
|
+
config: this._config
|
|
7272
|
+
});
|
|
7273
|
+
if (!treeResult.ok) {
|
|
7274
|
+
console.log(`[poll] StateTree.build failed: ${treeResult.error.message}`);
|
|
7275
|
+
return;
|
|
7276
|
+
}
|
|
7277
|
+
const currentHash = treeResult.value.approvalHash;
|
|
7278
|
+
if (currentHash === this._pendingProposalHash)
|
|
7279
|
+
return;
|
|
7280
|
+
console.log(`[poll] hash changed: ${this._pendingProposalHash?.slice(0, 12) ?? "null"} → ${currentHash?.slice(0, 12) ?? "null"}`);
|
|
7281
|
+
this._pendingProposalHash = currentHash;
|
|
7282
|
+
this._pendingFlow = this._propose().catch((err3) => {
|
|
7283
|
+
this.emit("error", err3 instanceof Error ? err3 : new Error(String(err3)), "propose");
|
|
7284
|
+
});
|
|
7285
|
+
} catch (e) {
|
|
7286
|
+
this.emit("error", e instanceof Error ? e : new Error(String(e)), "poll");
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
async _propose() {
|
|
7290
|
+
await this._supersedePending();
|
|
7291
|
+
const treeResult = await StateTree.build({ ops: this._ops, config: this._config });
|
|
7292
|
+
if (!treeResult.ok) {
|
|
7293
|
+
this.emit("error", new Error(`Failed to build state tree: ${treeResult.error.message}`), "propose:tree");
|
|
7294
|
+
return;
|
|
7295
|
+
}
|
|
7296
|
+
const diffResult = await diff({ tree: treeResult.value, ops: this._ops });
|
|
7297
|
+
if (!diffResult.ok) {
|
|
7298
|
+
this.emit("error", new Error(`Failed to build diff: ${diffResult.error.message}`), "propose:diff");
|
|
7299
|
+
return;
|
|
7300
|
+
}
|
|
7301
|
+
if (!diffResult.value.hasChanges)
|
|
7302
|
+
return;
|
|
7303
|
+
const files = diffResult.value.files.map((df) => ({
|
|
7304
|
+
path: df.file.path,
|
|
7305
|
+
status: df.file.status,
|
|
7306
|
+
diff: df.diff
|
|
7307
|
+
}));
|
|
7308
|
+
const hash = diffResult.value.approvalHash;
|
|
7309
|
+
const payload = { files, hash };
|
|
7310
|
+
const postResult = await this._channel.postProposal(payload);
|
|
7311
|
+
const proposal = {
|
|
7312
|
+
channel: postResult.channel,
|
|
7313
|
+
externalId: postResult.proposalId,
|
|
7314
|
+
payload,
|
|
7315
|
+
state: "pending",
|
|
7316
|
+
createdAt: new Date().toISOString()
|
|
7317
|
+
};
|
|
7318
|
+
this._activeProposal = proposal;
|
|
7319
|
+
const ac = new AbortController;
|
|
7320
|
+
this._abortController = ac;
|
|
7321
|
+
this.emit("proposed", proposal);
|
|
7322
|
+
await this._runApprovalFlow(proposal, ac.signal);
|
|
7323
|
+
this._pendingFlow = null;
|
|
7324
|
+
}
|
|
7325
|
+
async _runApprovalFlow(proposal, signal) {
|
|
7326
|
+
try {
|
|
7327
|
+
const approvalResult = await this._channel.waitForApproval(proposal.externalId, signal);
|
|
7328
|
+
if (approvalResult.approved) {
|
|
7329
|
+
const freshTree = await StateTree.build({ ops: this._ops, config: this._config });
|
|
7330
|
+
if (!freshTree.ok) {
|
|
7331
|
+
const error = new Error(`Failed to build fresh state tree: ${freshTree.error.message}`);
|
|
7332
|
+
this.emit("error", error, "approval:verification");
|
|
7333
|
+
proposal.state = "rejected";
|
|
7334
|
+
this._activeProposal = null;
|
|
7335
|
+
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7336
|
+
this.emit("rejected", proposal);
|
|
7337
|
+
return;
|
|
7338
|
+
}
|
|
7339
|
+
if (freshTree.value.approvalHash !== proposal.payload.hash) {
|
|
7340
|
+
proposal.state = "rejected";
|
|
7341
|
+
this._activeProposal = null;
|
|
7342
|
+
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7343
|
+
this.emit("rejected", proposal);
|
|
7344
|
+
return;
|
|
7345
|
+
}
|
|
7346
|
+
const applyResult = await apply({
|
|
7347
|
+
ops: this._ops,
|
|
7348
|
+
tree: freshTree.value,
|
|
7349
|
+
hash: proposal.payload.hash
|
|
7350
|
+
});
|
|
7351
|
+
if (!applyResult.ok) {
|
|
7352
|
+
const error = new Error(`Apply failed: ${applyResult.error.kind}`);
|
|
7353
|
+
this.emit("error", error, "approval:apply");
|
|
7354
|
+
proposal.state = "rejected";
|
|
7355
|
+
this._activeProposal = null;
|
|
7356
|
+
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7357
|
+
this.emit("rejected", proposal);
|
|
7358
|
+
return;
|
|
7359
|
+
}
|
|
7360
|
+
proposal.state = "approved";
|
|
7361
|
+
this._activeProposal = null;
|
|
7362
|
+
await this._channel.postResult(proposal.externalId, "applied");
|
|
7363
|
+
this.emit("applied", proposal);
|
|
7364
|
+
} else {
|
|
7365
|
+
proposal.state = "rejected";
|
|
7366
|
+
this._activeProposal = null;
|
|
7367
|
+
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7368
|
+
this.emit("rejected", proposal);
|
|
7369
|
+
}
|
|
7370
|
+
} catch (e) {
|
|
7371
|
+
if (e instanceof DOMException && e.name === "AbortError")
|
|
7372
|
+
return;
|
|
7373
|
+
if (e instanceof Error && e.name === "AbortError")
|
|
7374
|
+
return;
|
|
7375
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
7376
|
+
this.emit("error", error, "approval:wait");
|
|
7377
|
+
proposal.state = "rejected";
|
|
7378
|
+
this._activeProposal = null;
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7381
|
+
async _supersedePending() {
|
|
7382
|
+
if (this._activeProposal && this._abortController) {
|
|
7383
|
+
const oldProposal = this._activeProposal;
|
|
7384
|
+
const oldController = this._abortController;
|
|
7385
|
+
oldProposal.state = "superseded";
|
|
7386
|
+
this._activeProposal = null;
|
|
7387
|
+
this._abortController = null;
|
|
7388
|
+
oldController.abort();
|
|
7389
|
+
if (this._pendingFlow) {
|
|
7390
|
+
await this._pendingFlow.catch(() => {});
|
|
7391
|
+
this._pendingFlow = null;
|
|
7392
|
+
}
|
|
7393
|
+
await this._channel.postResult(oldProposal.externalId, "superseded");
|
|
7394
|
+
this.emit("superseded", oldProposal);
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
async _abortPending() {
|
|
7398
|
+
if (this._abortController)
|
|
7399
|
+
this._abortController.abort();
|
|
7400
|
+
if (this._pendingFlow) {
|
|
7401
|
+
await this._pendingFlow.catch(() => {});
|
|
7402
|
+
this._pendingFlow = null;
|
|
7403
|
+
}
|
|
7404
|
+
this._activeProposal = null;
|
|
7405
|
+
this._abortController = null;
|
|
7406
|
+
}
|
|
7407
|
+
}
|
|
7408
|
+
// ../core/src/daemon/channel-registry.ts
|
|
7409
|
+
var REGISTRY_KEY = "__soulguard_channel_registry__";
|
|
7410
|
+
function getRegistry() {
|
|
7411
|
+
const g = globalThis;
|
|
7412
|
+
if (!g[REGISTRY_KEY]) {
|
|
7413
|
+
g[REGISTRY_KEY] = new Map;
|
|
7414
|
+
}
|
|
7415
|
+
return g[REGISTRY_KEY];
|
|
7416
|
+
}
|
|
7417
|
+
function registerChannel(name, createFn) {
|
|
7418
|
+
getRegistry().set(name, createFn);
|
|
7419
|
+
}
|
|
7420
|
+
function getChannel(name) {
|
|
7421
|
+
return getRegistry().get(name);
|
|
7422
|
+
}
|
|
7423
|
+
|
|
7424
|
+
// ../core/src/daemon/daemon.ts
|
|
7425
|
+
class SoulguardDaemon {
|
|
7426
|
+
_ops;
|
|
7427
|
+
_config;
|
|
7428
|
+
_channel = null;
|
|
7429
|
+
_proposalManager = null;
|
|
7430
|
+
_running = false;
|
|
7431
|
+
constructor(options) {
|
|
7432
|
+
this._ops = options.ops;
|
|
7433
|
+
this._config = options.config;
|
|
7434
|
+
}
|
|
7435
|
+
get running() {
|
|
7436
|
+
return this._running;
|
|
7437
|
+
}
|
|
7438
|
+
get proposalManager() {
|
|
7439
|
+
return this._proposalManager;
|
|
7440
|
+
}
|
|
7441
|
+
async start() {
|
|
7442
|
+
if (this._running)
|
|
7443
|
+
return;
|
|
7444
|
+
const daemonConfig = this._config.daemon;
|
|
7445
|
+
if (!daemonConfig) {
|
|
7446
|
+
throw new Error("Daemon configuration missing. Add a 'daemon' section to soulguard.json.");
|
|
7447
|
+
}
|
|
7448
|
+
const channelName = daemonConfig.channel;
|
|
7449
|
+
console.log(`[daemon] channel: "${channelName}", config keys: ${JSON.stringify(Object.keys(daemonConfig))}`);
|
|
7450
|
+
const createChannelFn = getChannel(channelName);
|
|
7451
|
+
if (!createChannelFn) {
|
|
7452
|
+
throw new Error(`No channel registered for "${channelName}". Register it with registerChannel() before starting the daemon.`);
|
|
7453
|
+
}
|
|
7454
|
+
const channelConfig = daemonConfig[channelName];
|
|
7455
|
+
console.log(`[daemon] channelConfig present: ${!!channelConfig}, keys: ${channelConfig ? JSON.stringify(Object.keys(channelConfig)) : "n/a"}`);
|
|
7456
|
+
this._channel = createChannelFn(channelConfig);
|
|
7457
|
+
console.log(`[daemon] channel created: ${this._channel.name}`);
|
|
7458
|
+
this._proposalManager = new ProposalManager({
|
|
7459
|
+
ops: this._ops,
|
|
7460
|
+
config: this._config,
|
|
7461
|
+
channel: this._channel
|
|
7462
|
+
});
|
|
7463
|
+
console.log(`[daemon] starting proposal manager`);
|
|
7464
|
+
this._proposalManager.start();
|
|
7465
|
+
this._running = true;
|
|
7466
|
+
}
|
|
7467
|
+
async stop() {
|
|
7468
|
+
if (!this._running)
|
|
7469
|
+
return;
|
|
7470
|
+
this._running = false;
|
|
7471
|
+
if (this._proposalManager) {
|
|
7472
|
+
await this._proposalManager.stop();
|
|
7473
|
+
this._proposalManager = null;
|
|
7474
|
+
}
|
|
7475
|
+
if (this._channel) {
|
|
7476
|
+
await this._channel.dispose();
|
|
7477
|
+
this._channel = null;
|
|
7478
|
+
}
|
|
7479
|
+
}
|
|
7480
|
+
}
|
|
7481
|
+
// ../core/src/daemon/service.ts
|
|
7482
|
+
function generateServiceFile(options) {
|
|
7483
|
+
if (options.platform === "systemd") {
|
|
7484
|
+
return generateSystemdUnit(options);
|
|
7485
|
+
}
|
|
7486
|
+
return generateLaunchdPlist(options);
|
|
7487
|
+
}
|
|
7488
|
+
function serviceFilePath(options) {
|
|
7489
|
+
if (options.platform === "systemd") {
|
|
7490
|
+
return `/etc/systemd/system/soulguard-${options.guardianUser}.service`;
|
|
7491
|
+
}
|
|
7492
|
+
return `/Library/LaunchDaemons/com.soulguard.${options.guardianUser}.plist`;
|
|
7493
|
+
}
|
|
7494
|
+
function generateSystemdUnit(options) {
|
|
7495
|
+
return `[Unit]
|
|
7496
|
+
Description=Soulguard daemon for ${options.agentUser}
|
|
7497
|
+
After=network.target
|
|
7498
|
+
|
|
7499
|
+
[Service]
|
|
7500
|
+
Type=simple
|
|
7501
|
+
User=${options.guardianUser}
|
|
7502
|
+
ExecStart=${options.soulguardBin} daemon start
|
|
7503
|
+
WorkingDirectory=${options.workspaceRoot}
|
|
7504
|
+
Restart=on-failure
|
|
7505
|
+
RestartSec=5
|
|
7506
|
+
Environment=NODE_ENV=production
|
|
7507
|
+
|
|
7508
|
+
[Install]
|
|
7509
|
+
WantedBy=multi-user.target
|
|
7510
|
+
`;
|
|
7511
|
+
}
|
|
7512
|
+
function generateLaunchdPlist(options) {
|
|
7513
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
7514
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
7515
|
+
<plist version="1.0">
|
|
7516
|
+
<dict>
|
|
7517
|
+
<key>Label</key>
|
|
7518
|
+
<string>com.soulguard.${options.guardianUser}</string>
|
|
7519
|
+
<key>UserName</key>
|
|
7520
|
+
<string>${options.guardianUser}</string>
|
|
7521
|
+
<key>ProgramArguments</key>
|
|
7522
|
+
<array>
|
|
7523
|
+
<string>${options.soulguardBin}</string>
|
|
7524
|
+
<string>daemon</string>
|
|
7525
|
+
<string>start</string>
|
|
7526
|
+
</array>
|
|
7527
|
+
<key>WorkingDirectory</key>
|
|
7528
|
+
<string>${options.workspaceRoot}</string>
|
|
7529
|
+
<key>KeepAlive</key>
|
|
7530
|
+
<true/>
|
|
7531
|
+
<key>RunAtLoad</key>
|
|
7532
|
+
<true/>
|
|
7533
|
+
<key>StandardOutPath</key>
|
|
7534
|
+
<string>/var/log/soulguard-${options.guardianUser}.log</string>
|
|
7535
|
+
<key>StandardErrorPath</key>
|
|
7536
|
+
<string>/var/log/soulguard-${options.guardianUser}.err</string>
|
|
7537
|
+
</dict>
|
|
7538
|
+
</plist>
|
|
7539
|
+
`;
|
|
7540
|
+
}
|
|
7541
|
+
// ../core/src/cli/plugin-registry.ts
|
|
7542
|
+
var REGISTRY_KEY2 = "__soulguard_plugin_registry__";
|
|
7543
|
+
function getRegistry2() {
|
|
7544
|
+
const g = globalThis;
|
|
7545
|
+
if (!g[REGISTRY_KEY2]) {
|
|
7546
|
+
g[REGISTRY_KEY2] = new Map;
|
|
7547
|
+
}
|
|
7548
|
+
return g[REGISTRY_KEY2];
|
|
7549
|
+
}
|
|
7550
|
+
function registerPlugin(name, dir) {
|
|
7551
|
+
getRegistry2().set(name, dir);
|
|
7552
|
+
}
|
|
7553
|
+
function getPluginDir(name) {
|
|
7554
|
+
return getRegistry2().get(name);
|
|
7555
|
+
}
|
|
6504
7556
|
// ../openclaw/src/index.ts
|
|
6505
7557
|
var exports_src = {};
|
|
6506
7558
|
__export(exports_src, {
|
|
6507
7559
|
templates: () => templates,
|
|
6508
|
-
relaxedTemplate: () => relaxedTemplate,
|
|
6509
|
-
paranoidTemplate: () => paranoidTemplate,
|
|
6510
7560
|
guardToolCall: () => guardToolCall,
|
|
6511
|
-
|
|
7561
|
+
getPendingChanges: () => getPendingChanges,
|
|
6512
7562
|
default: () => src_default,
|
|
6513
|
-
createSoulguardPlugin: () => createSoulguardPlugin
|
|
7563
|
+
createSoulguardPlugin: () => createSoulguardPlugin,
|
|
7564
|
+
buildPendingChangesContext: () => buildPendingChangesContext
|
|
6514
7565
|
});
|
|
6515
7566
|
|
|
6516
7567
|
// ../openclaw/src/templates.ts
|
|
6517
|
-
var SOULGUARD_CONFIG = ["soulguard.json"];
|
|
6518
|
-
var CORE_IDENTITY = ["SOUL.md", "AGENTS.md", "IDENTITY.md", "USER.md"];
|
|
6519
|
-
var CORE_SESSION = ["TOOLS.md", "HEARTBEAT.md", "BOOTSTRAP.md"];
|
|
6520
|
-
var CORE_MEMORY = ["MEMORY.md"];
|
|
6521
|
-
var MEMORY_DIR = ["memory/**"];
|
|
6522
|
-
var SKILLS = ["skills/**"];
|
|
6523
|
-
var OPENCLAW_CONFIG = ["openclaw.json"];
|
|
6524
|
-
var CRON = ["cron/jobs.json"];
|
|
6525
|
-
var EXTENSIONS = ["extensions/**"];
|
|
6526
|
-
var SESSIONS = ["sessions/**"];
|
|
6527
|
-
var ALL_KNOWN_PATHS = [
|
|
6528
|
-
...SOULGUARD_CONFIG,
|
|
6529
|
-
...CORE_IDENTITY,
|
|
6530
|
-
...CORE_SESSION,
|
|
6531
|
-
...CORE_MEMORY,
|
|
6532
|
-
...MEMORY_DIR,
|
|
6533
|
-
...SKILLS,
|
|
6534
|
-
...OPENCLAW_CONFIG,
|
|
6535
|
-
...CRON,
|
|
6536
|
-
...EXTENSIONS,
|
|
6537
|
-
...SESSIONS
|
|
6538
|
-
];
|
|
6539
|
-
var defaultTemplate = {
|
|
6540
|
-
name: "default",
|
|
6541
|
-
description: "Core identity and config in vault, memory and skills tracked in ledger",
|
|
6542
|
-
vault: [
|
|
6543
|
-
...SOULGUARD_CONFIG,
|
|
6544
|
-
...CORE_IDENTITY,
|
|
6545
|
-
...CORE_SESSION,
|
|
6546
|
-
...OPENCLAW_CONFIG,
|
|
6547
|
-
...CRON,
|
|
6548
|
-
...EXTENSIONS
|
|
6549
|
-
],
|
|
6550
|
-
ledger: [...CORE_MEMORY, ...MEMORY_DIR, ...SKILLS],
|
|
6551
|
-
unprotected: [...SESSIONS]
|
|
6552
|
-
};
|
|
6553
|
-
var paranoidTemplate = {
|
|
6554
|
-
name: "paranoid",
|
|
6555
|
-
description: "Everything possible in vault, only skills in ledger",
|
|
6556
|
-
vault: [
|
|
6557
|
-
...SOULGUARD_CONFIG,
|
|
6558
|
-
...CORE_IDENTITY,
|
|
6559
|
-
...CORE_SESSION,
|
|
6560
|
-
...CORE_MEMORY,
|
|
6561
|
-
...MEMORY_DIR,
|
|
6562
|
-
...SKILLS,
|
|
6563
|
-
...OPENCLAW_CONFIG,
|
|
6564
|
-
...CRON,
|
|
6565
|
-
...EXTENSIONS
|
|
6566
|
-
],
|
|
6567
|
-
ledger: [...SESSIONS],
|
|
6568
|
-
unprotected: []
|
|
6569
|
-
};
|
|
6570
|
-
var relaxedTemplate = {
|
|
6571
|
-
name: "relaxed",
|
|
6572
|
-
description: "Only soulguard config locked, everything else tracked — good for initial setup",
|
|
6573
|
-
vault: [...SOULGUARD_CONFIG],
|
|
6574
|
-
ledger: [
|
|
6575
|
-
...CORE_IDENTITY,
|
|
6576
|
-
...CORE_SESSION,
|
|
6577
|
-
...CORE_MEMORY,
|
|
6578
|
-
...MEMORY_DIR,
|
|
6579
|
-
...SKILLS,
|
|
6580
|
-
...OPENCLAW_CONFIG,
|
|
6581
|
-
...CRON,
|
|
6582
|
-
...EXTENSIONS
|
|
6583
|
-
],
|
|
6584
|
-
unprotected: [...SESSIONS]
|
|
6585
|
-
};
|
|
6586
7568
|
var templates = {
|
|
6587
|
-
default:
|
|
6588
|
-
|
|
6589
|
-
|
|
7569
|
+
default: {
|
|
7570
|
+
name: "default",
|
|
7571
|
+
description: "Core identity and config protected, memory and skills watched",
|
|
7572
|
+
protect: [
|
|
7573
|
+
"workspace/SOUL.md",
|
|
7574
|
+
"workspace/AGENTS.md",
|
|
7575
|
+
"workspace/IDENTITY.md",
|
|
7576
|
+
"workspace/USER.md",
|
|
7577
|
+
"workspace/TOOLS.md",
|
|
7578
|
+
"workspace/HEARTBEAT.md",
|
|
7579
|
+
"workspace/BOOTSTRAP.md",
|
|
7580
|
+
"openclaw.json",
|
|
7581
|
+
"cron/",
|
|
7582
|
+
"extensions/"
|
|
7583
|
+
],
|
|
7584
|
+
watch: ["workspace/MEMORY.md", "workspace/memory/", "workspace/skills/"],
|
|
7585
|
+
release: ["workspace/sessions/"]
|
|
7586
|
+
},
|
|
7587
|
+
paranoid: {
|
|
7588
|
+
name: "paranoid",
|
|
7589
|
+
description: "Everything protected, only sessions watched",
|
|
7590
|
+
protect: [
|
|
7591
|
+
"workspace/SOUL.md",
|
|
7592
|
+
"workspace/AGENTS.md",
|
|
7593
|
+
"workspace/IDENTITY.md",
|
|
7594
|
+
"workspace/USER.md",
|
|
7595
|
+
"workspace/TOOLS.md",
|
|
7596
|
+
"workspace/HEARTBEAT.md",
|
|
7597
|
+
"workspace/BOOTSTRAP.md",
|
|
7598
|
+
"workspace/MEMORY.md",
|
|
7599
|
+
"workspace/memory/",
|
|
7600
|
+
"workspace/skills/",
|
|
7601
|
+
"openclaw.json",
|
|
7602
|
+
"cron/",
|
|
7603
|
+
"extensions/"
|
|
7604
|
+
],
|
|
7605
|
+
watch: ["workspace/sessions/"],
|
|
7606
|
+
release: []
|
|
7607
|
+
},
|
|
7608
|
+
relaxed: {
|
|
7609
|
+
name: "relaxed",
|
|
7610
|
+
description: "Everything watched — good for initial setup",
|
|
7611
|
+
protect: [],
|
|
7612
|
+
watch: [
|
|
7613
|
+
"workspace/SOUL.md",
|
|
7614
|
+
"workspace/AGENTS.md",
|
|
7615
|
+
"workspace/IDENTITY.md",
|
|
7616
|
+
"workspace/USER.md",
|
|
7617
|
+
"workspace/TOOLS.md",
|
|
7618
|
+
"workspace/HEARTBEAT.md",
|
|
7619
|
+
"workspace/BOOTSTRAP.md",
|
|
7620
|
+
"workspace/MEMORY.md",
|
|
7621
|
+
"workspace/memory/",
|
|
7622
|
+
"workspace/skills/",
|
|
7623
|
+
"openclaw.json",
|
|
7624
|
+
"cron/",
|
|
7625
|
+
"extensions/"
|
|
7626
|
+
],
|
|
7627
|
+
release: ["workspace/sessions/"]
|
|
7628
|
+
}
|
|
6590
7629
|
};
|
|
6591
7630
|
// ../openclaw/src/plugin.ts
|
|
6592
7631
|
import { readFileSync } from "node:fs";
|
|
6593
|
-
import
|
|
7632
|
+
import os from "node:os";
|
|
7633
|
+
import { join as join2 } from "node:path";
|
|
6594
7634
|
|
|
6595
7635
|
// ../openclaw/src/guard.ts
|
|
6596
|
-
import
|
|
6597
|
-
var WRITE_TOOLS = new Set(["
|
|
7636
|
+
import path from "node:path";
|
|
7637
|
+
var WRITE_TOOLS = new Set(["write", "edit"]);
|
|
6598
7638
|
var PATH_KEYS = ["file_path", "path", "file"];
|
|
6599
|
-
var STAGING_PREFIX = ".soulguard/staging/";
|
|
6600
7639
|
function guardToolCall(toolName, params, options) {
|
|
6601
|
-
if (!WRITE_TOOLS.has(toolName))
|
|
7640
|
+
if (!WRITE_TOOLS.has(toolName.toLowerCase())) {
|
|
6602
7641
|
return { blocked: false };
|
|
7642
|
+
}
|
|
6603
7643
|
let targetPath;
|
|
6604
7644
|
for (const key of PATH_KEYS) {
|
|
6605
7645
|
const v = params[key];
|
|
@@ -6608,138 +7648,59 @@ function guardToolCall(toolName, params, options) {
|
|
|
6608
7648
|
break;
|
|
6609
7649
|
}
|
|
6610
7650
|
}
|
|
7651
|
+
if (targetPath && path.isAbsolute(targetPath)) {
|
|
7652
|
+
targetPath = path.relative(options.stateDir, targetPath);
|
|
7653
|
+
}
|
|
6611
7654
|
if (!targetPath)
|
|
6612
7655
|
return { blocked: false };
|
|
6613
|
-
|
|
6614
|
-
if (norm.startsWith(STAGING_PREFIX))
|
|
7656
|
+
if (isStagingPath(targetPath))
|
|
6615
7657
|
return { blocked: false };
|
|
6616
|
-
if (!
|
|
7658
|
+
if (!isProtectedFile(options.protectFiles, targetPath))
|
|
6617
7659
|
return { blocked: false };
|
|
6618
|
-
const fileName = basename(targetPath);
|
|
6619
7660
|
return {
|
|
6620
7661
|
blocked: true,
|
|
6621
|
-
reason:
|
|
7662
|
+
reason: [
|
|
7663
|
+
`${targetPath} is protected by soulguard.`,
|
|
7664
|
+
`To propose changes, run \`soulguard stage ${targetPath}\` to create a working copy,`,
|
|
7665
|
+
`then edit the staged file at ${stagingPath(targetPath)}.`,
|
|
7666
|
+
`Run \`soulguard diff\` to review your changes.`,
|
|
7667
|
+
`Your owner will review and apply the changes.`
|
|
7668
|
+
].join(" ")
|
|
6622
7669
|
};
|
|
6623
7670
|
}
|
|
6624
7671
|
|
|
6625
7672
|
// ../openclaw/src/plugin.ts
|
|
6626
|
-
var
|
|
6627
|
-
vault: ["openclaw.json", "soulguard.json"],
|
|
6628
|
-
ledger: []
|
|
6629
|
-
};
|
|
7673
|
+
var PKG_VERSION = typeof SOULGUARD_VERSION !== "undefined" ? SOULGUARD_VERSION : "0.0.0-dev";
|
|
6630
7674
|
var PLUGIN_DESCRIPTION = "Identity protection for AI agents";
|
|
6631
7675
|
function createSoulguardPlugin(options) {
|
|
6632
7676
|
return {
|
|
6633
7677
|
id: "soulguard",
|
|
6634
7678
|
name: "Soulguard",
|
|
6635
7679
|
description: PLUGIN_DESCRIPTION,
|
|
6636
|
-
version:
|
|
7680
|
+
version: PKG_VERSION,
|
|
6637
7681
|
activate(api) {
|
|
6638
|
-
const
|
|
7682
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() ?? join2(os.homedir(), ".openclaw");
|
|
6639
7683
|
const configFile = options?.configPath ?? "soulguard.json";
|
|
6640
|
-
const configPath =
|
|
6641
|
-
let
|
|
6642
|
-
let vaultFiles;
|
|
7684
|
+
const configPath = join2(stateDir, configFile);
|
|
7685
|
+
let protectFiles;
|
|
6643
7686
|
try {
|
|
6644
7687
|
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
6645
|
-
|
|
6646
|
-
vaultFiles = config.vault;
|
|
7688
|
+
protectFiles = protectPatterns(parseConfig(raw));
|
|
6647
7689
|
} catch {
|
|
6648
|
-
config
|
|
6649
|
-
|
|
6650
|
-
api.logger?.warn("soulguard: no soulguard.json found — using OpenClaw defaults");
|
|
7690
|
+
api.logger?.warn(`soulguard: no config found at ${configPath} — plugin inactive`);
|
|
7691
|
+
return;
|
|
6651
7692
|
}
|
|
6652
|
-
if (
|
|
7693
|
+
if (protectFiles.length === 0)
|
|
6653
7694
|
return;
|
|
6654
|
-
|
|
6655
|
-
api.registerTool({
|
|
6656
|
-
name: "soulguard_status",
|
|
6657
|
-
description: "Check soulguard protection status of vault and ledger files",
|
|
6658
|
-
parameters: { type: "object", properties: {}, required: [] },
|
|
6659
|
-
async execute(_id, _params) {
|
|
6660
|
-
const ops = createOps();
|
|
6661
|
-
const result = await status({
|
|
6662
|
-
config,
|
|
6663
|
-
expectedVaultOwnership: { user: "soulguardian", group: "soulguard", mode: "444" },
|
|
6664
|
-
expectedLedgerOwnership: { user: "agent", group: "staff", mode: "644" },
|
|
6665
|
-
ops
|
|
6666
|
-
});
|
|
6667
|
-
if (!result.ok) {
|
|
6668
|
-
return { content: [{ type: "text", text: "Status check failed" }] };
|
|
6669
|
-
}
|
|
6670
|
-
const lines = ["Soulguard Status:", ""];
|
|
6671
|
-
for (const f of [...result.value.vault, ...result.value.ledger]) {
|
|
6672
|
-
if (f.status === "ok")
|
|
6673
|
-
lines.push(` ✅ ${f.file.path}`);
|
|
6674
|
-
else if (f.status === "drifted")
|
|
6675
|
-
lines.push(` ⚠️ ${f.file.path} — ${f.issues.map((i) => i.kind).join(", ")}`);
|
|
6676
|
-
else if (f.status === "missing")
|
|
6677
|
-
lines.push(` ❌ ${f.path} — missing`);
|
|
6678
|
-
else if (f.status === "error")
|
|
6679
|
-
lines.push(` ❌ ${f.path} — error: ${f.error.kind}`);
|
|
6680
|
-
}
|
|
6681
|
-
if (result.value.issues.length === 0)
|
|
6682
|
-
lines.push("", "All files ok.");
|
|
6683
|
-
else
|
|
6684
|
-
lines.push("", `${result.value.issues.length} issue(s) found.`);
|
|
6685
|
-
return { content: [{ type: "text", text: lines.join(`
|
|
6686
|
-
`) }] };
|
|
6687
|
-
}
|
|
6688
|
-
}, { optional: true });
|
|
6689
|
-
api.registerTool({
|
|
6690
|
-
name: "soulguard_diff",
|
|
6691
|
-
description: "Show differences between vault files and their staging copies",
|
|
6692
|
-
parameters: {
|
|
6693
|
-
type: "object",
|
|
6694
|
-
properties: {
|
|
6695
|
-
files: {
|
|
6696
|
-
type: "array",
|
|
6697
|
-
items: { type: "string" },
|
|
6698
|
-
description: "Specific files to diff (default: all vault files)"
|
|
6699
|
-
}
|
|
6700
|
-
},
|
|
6701
|
-
required: []
|
|
6702
|
-
},
|
|
6703
|
-
async execute(_id, params) {
|
|
6704
|
-
const ops = createOps();
|
|
6705
|
-
const files = Array.isArray(params.files) ? params.files : undefined;
|
|
6706
|
-
const result = await diff({ ops, config, files });
|
|
6707
|
-
if (!result.ok) {
|
|
6708
|
-
return {
|
|
6709
|
-
content: [{ type: "text", text: `Diff failed: ${result.error.kind}` }]
|
|
6710
|
-
};
|
|
6711
|
-
}
|
|
6712
|
-
if (!result.value.hasChanges) {
|
|
6713
|
-
return {
|
|
6714
|
-
content: [
|
|
6715
|
-
{ type: "text", text: "No differences — staging matches vault." }
|
|
6716
|
-
]
|
|
6717
|
-
};
|
|
6718
|
-
}
|
|
6719
|
-
const lines = result.value.files.filter((d) => d.status === "modified" && d.diff).map((d) => `--- ${d.path}
|
|
6720
|
-
${d.diff}`);
|
|
6721
|
-
let text = lines.join(`
|
|
6722
|
-
|
|
6723
|
-
`) || "No modified files.";
|
|
6724
|
-
if (result.value.approvalHash) {
|
|
6725
|
-
text += `
|
|
6726
|
-
|
|
6727
|
-
────────────────────────────────────────
|
|
6728
|
-
Approval hash: ${result.value.approvalHash}
|
|
6729
|
-
To approve: soulguard approve --hash ${result.value.approvalHash}`;
|
|
6730
|
-
}
|
|
6731
|
-
return { content: [{ type: "text", text }] };
|
|
6732
|
-
}
|
|
6733
|
-
}, { optional: true });
|
|
6734
|
-
const hookFn = api.registerHook ?? api.on;
|
|
6735
|
-
hookFn("before_tool_call", (...args) => {
|
|
7695
|
+
api.on("before_tool_call", (...args) => {
|
|
6736
7696
|
const event = args[0];
|
|
6737
7697
|
if (!event || typeof event !== "object" || !("toolName" in event)) {
|
|
6738
7698
|
return;
|
|
6739
7699
|
}
|
|
6740
7700
|
const e = event;
|
|
6741
7701
|
const result = guardToolCall(e.toolName, e.params, {
|
|
6742
|
-
|
|
7702
|
+
protectFiles,
|
|
7703
|
+
stateDir
|
|
6743
7704
|
});
|
|
6744
7705
|
if (result.blocked) {
|
|
6745
7706
|
return { block: true, blockReason: result.reason };
|
|
@@ -6749,42 +7710,74 @@ To approve: soulguard approve --hash ${result.value.approvalHash}`;
|
|
|
6749
7710
|
}
|
|
6750
7711
|
};
|
|
6751
7712
|
}
|
|
7713
|
+
// ../openclaw/src/context.ts
|
|
7714
|
+
async function getPendingChanges(options) {
|
|
7715
|
+
const treeResult = await StateTree.build(options);
|
|
7716
|
+
if (!treeResult.ok)
|
|
7717
|
+
return { files: [] };
|
|
7718
|
+
return { files: treeResult.value.changedFiles().map((f) => f.path) };
|
|
7719
|
+
}
|
|
7720
|
+
async function buildPendingChangesContext(options) {
|
|
7721
|
+
const { files } = await getPendingChanges(options);
|
|
7722
|
+
if (files.length === 0)
|
|
7723
|
+
return;
|
|
7724
|
+
const fileList = files.join(", ");
|
|
7725
|
+
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.`;
|
|
7726
|
+
}
|
|
7727
|
+
|
|
6752
7728
|
// ../openclaw/src/index.ts
|
|
6753
7729
|
var src_default = createSoulguardPlugin();
|
|
6754
7730
|
export {
|
|
6755
|
-
|
|
7731
|
+
writeConfig,
|
|
7732
|
+
watchPatterns,
|
|
6756
7733
|
validateSelfProtection,
|
|
6757
7734
|
validatePolicies,
|
|
6758
7735
|
sync,
|
|
6759
7736
|
status,
|
|
7737
|
+
stagingPath,
|
|
6760
7738
|
soulguardConfigSchema,
|
|
6761
|
-
|
|
7739
|
+
setTier,
|
|
7740
|
+
serviceFilePath,
|
|
6762
7741
|
reset,
|
|
7742
|
+
release,
|
|
7743
|
+
registerPlugin,
|
|
7744
|
+
registerChannel,
|
|
7745
|
+
readConfig,
|
|
7746
|
+
protectPatterns,
|
|
7747
|
+
patternsForTier,
|
|
6763
7748
|
parseConfig,
|
|
6764
7749
|
exports_src as openclaw,
|
|
6765
7750
|
ok,
|
|
6766
7751
|
normalizePath,
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
7752
|
+
makeDefaultConfig,
|
|
7753
|
+
isStagingPath,
|
|
7754
|
+
isProtectedFile,
|
|
7755
|
+
isDeleteSentinel,
|
|
7756
|
+
guardianName,
|
|
7757
|
+
getProtectOwnership,
|
|
7758
|
+
getPluginDir,
|
|
7759
|
+
getChannel,
|
|
7760
|
+
generateServiceFile,
|
|
6774
7761
|
formatIssue,
|
|
6775
7762
|
evaluatePolicies,
|
|
6776
7763
|
err,
|
|
6777
7764
|
diff,
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
approve,
|
|
7765
|
+
apply,
|
|
7766
|
+
TierCommand,
|
|
6781
7767
|
SyncCommand,
|
|
6782
7768
|
StatusCommand,
|
|
7769
|
+
StateTreeBuildError,
|
|
7770
|
+
StateTree,
|
|
7771
|
+
StageCommand,
|
|
7772
|
+
SoulguardDaemon,
|
|
7773
|
+
STAGING_DIR,
|
|
7774
|
+
SOULGUARD_GROUP,
|
|
6783
7775
|
ResetCommand,
|
|
7776
|
+
ProposalManager,
|
|
6784
7777
|
NodeSystemOps,
|
|
6785
7778
|
MockSystemOps,
|
|
6786
7779
|
LiveConsoleOutput,
|
|
6787
7780
|
DiffCommand,
|
|
6788
|
-
|
|
6789
|
-
|
|
7781
|
+
DELETE_SENTINEL,
|
|
7782
|
+
ApplyCommand
|
|
6790
7783
|
};
|