agentic-workflow-manager 3.12.0 → 3.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/hooks/shared.js +14 -1
- package/dist/src/commands/preflight/checks.js +63 -5
- package/dist/src/commands/preflight/index.js +6 -1
- package/dist/src/commands/registry/add.js +10 -1
- package/dist/src/commands/sensors/baseline.js +4 -3
- package/dist/src/core/atomic-file.js +24 -2
- package/dist/src/core/executor.js +40 -1
- package/dist/src/core/install-transaction.js +13 -1
- package/dist/src/core/journal/process.js +241 -9
- package/dist/tests/commands/hooks/install-symlink-fallback.test.js +25 -0
- package/dist/tests/commands/hooks/status.test.js +23 -3
- package/dist/tests/commands/job/exec-wrapper.test.js +15 -1
- package/dist/tests/commands/job/gate-reconcile.test.js +31 -5
- package/dist/tests/commands/preflight/preflight.test.js +99 -1
- package/dist/tests/commands/registry/add.test.js +27 -0
- package/dist/tests/commands/sensors/changed.test.js +13 -0
- package/dist/tests/commands/sensors/exec-windows.test.js +13 -0
- package/dist/tests/commands/sensors/exec.test.js +28 -6
- package/dist/tests/commands/sensors/formatters/ruff.test.js +22 -6
- package/dist/tests/commands/sensors/run-changed.test.js +33 -0
- package/dist/tests/commands/watch/e2e-crash.test.js +54 -18
- package/dist/tests/commands/watch/runner.test.js +32 -2
- package/dist/tests/commands/watch/supervisor-loop.test.js +24 -3
- package/dist/tests/core/artifact-state.test.js +11 -1
- package/dist/tests/core/atomic-file-durable.test.js +48 -1
- package/dist/tests/core/atomic-file.test.js +14 -3
- package/dist/tests/core/executor.test.js +58 -0
- package/dist/tests/core/install-transaction.test.js +59 -2
- package/dist/tests/core/journal/adapter.test.js +54 -0
- package/dist/tests/core/journal/fingerprint.test.js +10 -2
- package/dist/tests/core/journal/process.test.js +208 -9
- package/dist/tests/core/journal/store.test.js +8 -2
- package/dist/tests/core/no-color.test.js +23 -0
- package/dist/tests/core/registries-sync.test.js +32 -3
- package/package.json +1 -1
|
@@ -29,7 +29,20 @@ function syncExecutable(source, dest, method) {
|
|
|
29
29
|
catch { /* not exists, fine */ }
|
|
30
30
|
fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
|
|
31
31
|
if (method === 'symlink') {
|
|
32
|
-
|
|
32
|
+
try {
|
|
33
|
+
fs_1.default.symlinkSync(source, dest);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// best-effort: a FILE symlink needs SeCreateSymbolicLinkPrivilege on
|
|
37
|
+
// Windows, denied by default on unprivileged accounts (incl. GitHub
|
|
38
|
+
// Actions' windows-latest runner) — fall back to a plain copy, same
|
|
39
|
+
// as the bootstrap skill file's own fallback (hooks/claude.ts) and
|
|
40
|
+
// executor.ts's stageArtifact for file artifacts. 'awm update' will
|
|
41
|
+
// not auto-propagate for this file until re-synced, same tradeoff.
|
|
42
|
+
fs_1.default.copyFileSync(source, dest);
|
|
43
|
+
const srcMode = fs_1.default.statSync(source).mode;
|
|
44
|
+
fs_1.default.chmodSync(dest, srcMode);
|
|
45
|
+
}
|
|
33
46
|
}
|
|
34
47
|
else {
|
|
35
48
|
fs_1.default.copyFileSync(source, dest);
|
|
@@ -9,6 +9,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const status_1 = require("../sensors/status");
|
|
11
11
|
const init_1 = require("../sensors/init");
|
|
12
|
+
const baseline_1 = require("../sensors/baseline");
|
|
12
13
|
const paths_1 = require("../../core/paths");
|
|
13
14
|
const MANIFEST = path_1.default.join('.awm', 'sensors.json');
|
|
14
15
|
/**
|
|
@@ -36,6 +37,18 @@ function readManifest(cwd) {
|
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Total sensor entries and how many are enabled (`enabled !== false`, so a missing
|
|
42
|
+
* `enabled` field defaults to counted-as-enabled). Shared by `checkManifest` and
|
|
43
|
+
* `checkSensorsBaseline` — they were previously two copies of the identical
|
|
44
|
+
* predicate, which post-implementation-qa flagged: a hardening applied to one (e.g.
|
|
45
|
+
* guarding a malformed sensor entry) would silently NOT apply to the other unless
|
|
46
|
+
* someone remembered to edit both. One function, two callers, one place to harden.
|
|
47
|
+
*/
|
|
48
|
+
function countEnabledSensors(manifest) {
|
|
49
|
+
const entries = Object.values(manifest.sensors ?? {});
|
|
50
|
+
return { total: entries.length, enabled: entries.filter(s => s.enabled !== false).length };
|
|
51
|
+
}
|
|
39
52
|
/**
|
|
40
53
|
* A repo may legitimately have no sensors — but it has to SAY so, in a committed file.
|
|
41
54
|
*
|
|
@@ -62,8 +75,7 @@ function checkManifest(cwd, manifest) {
|
|
|
62
75
|
remedy: 'fix or regenerate it with `awm sensors init`',
|
|
63
76
|
};
|
|
64
77
|
}
|
|
65
|
-
const total =
|
|
66
|
-
const enabled = Object.values(manifest.sensors ?? {}).filter(s => s.enabled !== false).length;
|
|
78
|
+
const { total, enabled } = countEnabledSensors(manifest);
|
|
67
79
|
// total === 0 is NOT an opt-out: a deliberate opt-out lists every known sensor NAME
|
|
68
80
|
// explicitly with `enabled: false` (total > 0, enabled === 0). Zero entries means
|
|
69
81
|
// nothing was ever configured — most commonly because the registry had no pack.json
|
|
@@ -135,6 +147,51 @@ function checkPack(cwd, manifest) {
|
|
|
135
147
|
}
|
|
136
148
|
return { id: 'pack', ok: true, detail: `${manifest.pack} matches the detected stack` };
|
|
137
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Advisory only — `ok` is ALWAYS `true`, same contract as `checkHost` below. A team
|
|
152
|
+
* adopting AWM on a legacy repo starts with pre-existing sensor findings; the ratchet
|
|
153
|
+
* (`awm sensors baseline`, `.awm/sensors.baseline.json`) exists precisely to snapshot
|
|
154
|
+
* those as accepted debt so the gate only fails on genuinely NEW findings. But nothing
|
|
155
|
+
* today surfaces that the mechanism exists — the team discovers it only after hitting a
|
|
156
|
+
* wall of red findings and going looking. This nudges them toward it before that
|
|
157
|
+
* happens. It never blocks preflight: a repo can legitimately have zero debt to
|
|
158
|
+
* snapshot (sensors enabled from day one), and "no baseline yet" is not itself a
|
|
159
|
+
* failure — only the operator's lack of awareness that baselining is an option is the
|
|
160
|
+
* problem this addresses.
|
|
161
|
+
*
|
|
162
|
+
* Only called when a manifest exists (see the conditional spread in `preflight()`) —
|
|
163
|
+
* there is nothing to baseline without sensors configured in the first place, so this
|
|
164
|
+
* mirrors how `checkTools`/`checkPack` are skipped entirely rather than reported on a
|
|
165
|
+
* repo that was never set up.
|
|
166
|
+
*
|
|
167
|
+
* Also requires at least one ENABLED sensor, via the same `countEnabledSensors` helper
|
|
168
|
+
* `checkManifest` uses — a deliberate opt-out (every sensor `enabled: false`) or an
|
|
169
|
+
* unparseable/empty manifest has nothing to baseline either, and nudging "run `awm
|
|
170
|
+
* sensors baseline`" there would be actively misleading rather than merely unnecessary.
|
|
171
|
+
*
|
|
172
|
+
* Presence is checked via `readBaseline` (same function `partition()` uses at gate time
|
|
173
|
+
* to decide suppression), not a raw `fs.existsSync` — a baseline PATH that exists but
|
|
174
|
+
* isn't a readable JSON file (e.g. a stray directory at that path) is treated by the
|
|
175
|
+
* real gate as "no baseline, nothing suppressed"; `existsSync` alone would have reported
|
|
176
|
+
* "baseline present" for that same case, reassuring the operator that debt is being
|
|
177
|
+
* suppressed when it silently is not.
|
|
178
|
+
*/
|
|
179
|
+
function checkSensorsBaseline(cwd, manifest) {
|
|
180
|
+
const enabled = manifest ? countEnabledSensors(manifest).enabled : 0;
|
|
181
|
+
if (enabled === 0) {
|
|
182
|
+
return { id: 'sensors-baseline', ok: true, detail: 'no enabled sensors — nothing to baseline' };
|
|
183
|
+
}
|
|
184
|
+
if ((0, baseline_1.readBaseline)(cwd) !== null) {
|
|
185
|
+
return { id: 'sensors-baseline', ok: true, detail: 'baseline present' };
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
id: 'sensors-baseline',
|
|
189
|
+
ok: true,
|
|
190
|
+
detail: 'sensors configured, no baseline yet — awm sensors baseline',
|
|
191
|
+
remedy: 'run `awm sensors baseline` to snapshot pre-existing findings as accepted debt, '
|
|
192
|
+
+ 'so the gate only chases new problems',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
138
195
|
/**
|
|
139
196
|
* Extract just the hostname portion of a git remote URL — never match against the
|
|
140
197
|
* full URL string. A bare substring check against the whole remote (`remote.includes
|
|
@@ -224,9 +281,10 @@ function preflight(cwd = process.cwd()) {
|
|
|
224
281
|
const checks = [
|
|
225
282
|
checkContext(cwd),
|
|
226
283
|
checkManifest(cwd, manifest),
|
|
227
|
-
// Skipped when there is no manifest: reporting "tools broken"
|
|
228
|
-
//
|
|
229
|
-
|
|
284
|
+
// Skipped when there is no manifest: reporting "tools broken" (or nudging toward
|
|
285
|
+
// a baseline that has nothing to snapshot) on a repo that was never set up
|
|
286
|
+
// buries the one thing the operator needs to read.
|
|
287
|
+
...(manifestExists ? [checkTools(cwd), checkPack(cwd, manifest), checkSensorsBaseline(cwd, manifest)] : []),
|
|
230
288
|
// Runs unconditionally — orthogonal to sensor configuration entirely, this is
|
|
231
289
|
// about PR/MR tooling, not sensors.
|
|
232
290
|
checkHost(cwd),
|
|
@@ -20,7 +20,12 @@ function exitCodeFor(report) {
|
|
|
20
20
|
return report.status === 'ready' ? 0 : 1;
|
|
21
21
|
}
|
|
22
22
|
function formatReport(report) {
|
|
23
|
-
|
|
23
|
+
// Computed from the actual ids present, not hardcoded to the widest id THIS
|
|
24
|
+
// report happens to have — a fixed literal here silently misaligns the moment a
|
|
25
|
+
// longer `PreflightCheck['id']` is added (confirmed: 'sensors-baseline', at 16
|
|
26
|
+
// chars, broke a hardcoded 9-char pad).
|
|
27
|
+
const idWidth = Math.max(0, ...report.checks.map(c => c.id.length));
|
|
28
|
+
const lines = report.checks.map(c => ` ${c.ok ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘')} ${c.id.padEnd(idWidth)} ${c.detail}`
|
|
24
29
|
+ (c.remedy ? `\n ${picocolors_1.default.dim('→ ' + c.remedy)}` : ''));
|
|
25
30
|
if (report.status === 'ready') {
|
|
26
31
|
return `${picocolors_1.default.green('✔')} Harness ready — this project can be gated.\n${lines.join('\n')}\n`;
|
|
@@ -13,7 +13,16 @@ const registries_1 = require("../../core/registries");
|
|
|
13
13
|
const discovery_1 = require("../../core/discovery");
|
|
14
14
|
const bundles_1 = require("../../core/bundles");
|
|
15
15
|
function deriveRegistryName(remote) {
|
|
16
|
-
|
|
16
|
+
// Split on '/', '\' and ':' — not just '/' and ':'. A git remote URL
|
|
17
|
+
// (https://…/repo.git, git@host:org/repo.git) only ever uses the first
|
|
18
|
+
// two, but `remote` here can also be a plain local filesystem path (e.g.
|
|
19
|
+
// a `awm registry add <local-repo>` clone source, or this repo's own
|
|
20
|
+
// tests), and on native Windows that path is backslash-separated
|
|
21
|
+
// (`C:\Users\...\src-alpha`). Splitting on `[/:]` alone leaves the whole
|
|
22
|
+
// backslash-joined tail as one segment (`\Users\...\src-alpha`), which
|
|
23
|
+
// then fails the caller's `/[/\\]/.test(name)` invalid-name check and
|
|
24
|
+
// makes every local-path `addRegistry` call fail on Windows.
|
|
25
|
+
const base = remote.replace(/[/\\]+$/, '').split(/[/\\:]/).pop() ?? '';
|
|
17
26
|
return base.replace(/\.git$/, '');
|
|
18
27
|
}
|
|
19
28
|
async function addRegistry(remote, nameOverride) {
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BASELINE_FILE = void 0;
|
|
6
7
|
exports.fingerprint = fingerprint;
|
|
7
8
|
exports.readBaseline = readBaseline;
|
|
8
9
|
exports.writeBaseline = writeBaseline;
|
|
@@ -11,7 +12,7 @@ exports.buildBaseline = buildBaseline;
|
|
|
11
12
|
const crypto_1 = __importDefault(require("crypto"));
|
|
12
13
|
const fs_1 = __importDefault(require("fs"));
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
14
|
-
|
|
15
|
+
exports.BASELINE_FILE = path_1.default.join('.awm', 'sensors.baseline.json');
|
|
15
16
|
/**
|
|
16
17
|
* Mask runs of digits so location noise embedded in messages (e.g. the tsc
|
|
17
18
|
* formatter writes "... line 199 ...") doesn't change the fingerprint when code
|
|
@@ -43,7 +44,7 @@ function fingerprint(sensor, e) {
|
|
|
43
44
|
return crypto_1.default.createHash('sha1').update(basis).digest('hex');
|
|
44
45
|
}
|
|
45
46
|
function readBaseline(cwd) {
|
|
46
|
-
const p = path_1.default.join(cwd, BASELINE_FILE);
|
|
47
|
+
const p = path_1.default.join(cwd, exports.BASELINE_FILE);
|
|
47
48
|
if (!fs_1.default.existsSync(p))
|
|
48
49
|
return null;
|
|
49
50
|
try {
|
|
@@ -55,7 +56,7 @@ function readBaseline(cwd) {
|
|
|
55
56
|
}
|
|
56
57
|
function writeBaseline(cwd, baseline) {
|
|
57
58
|
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
|
|
58
|
-
fs_1.default.writeFileSync(path_1.default.join(cwd, BASELINE_FILE), JSON.stringify(baseline, null, 2), 'utf-8');
|
|
59
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, exports.BASELINE_FILE), JSON.stringify(baseline, null, 2), 'utf-8');
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
61
62
|
* Split a sensor's findings into new vs baseline-suppressed. With no accepted
|
|
@@ -75,12 +75,34 @@ function writeFileAtomic(file, content, mode = 0o644) {
|
|
|
75
75
|
/** fsync del directorio contenedor: garantiza que una ENTRADA creada/renombrada
|
|
76
76
|
* sobrevive un crash del OS. Falla LANZANDO — la durabilidad de la transición
|
|
77
77
|
* es parte del contrato, nunca un best-effort silencioso (design R1.2,
|
|
78
|
-
* bloqueador 4 de la review del plan)
|
|
78
|
+
* bloqueador 4 de la review del plan) — EXCEPTO en Windows, donde fsync-ear un
|
|
79
|
+
* file descriptor de directorio no es una operacion que el SO soporte en
|
|
80
|
+
* absoluto (no es una falla de durabilidad real: no existe el mecanismo
|
|
81
|
+
* POSIX que esta funcion intenta invocar). Confirmado via R6 CI (2026-08-08,
|
|
82
|
+
* primera corrida real en windows-latest): `fs.openSync(dir, 'r')` tiene
|
|
83
|
+
* exito, pero el `fsyncSync` subsiguiente sobre ese fd siempre falla con
|
|
84
|
+
* EPERM — libuv mapea asi el resultado de `FlushFileBuffers` sobre un handle
|
|
85
|
+
* de directorio en Win32, que Windows rechaza categoricamente (no es un
|
|
86
|
+
* fallo intermitente de hardware/permisos, es ausencia de la capacidad).
|
|
87
|
+
* NTFS ya registra los renames/creates en su propio journal transaccional,
|
|
88
|
+
* asi que la garantia de durabilidad de la ENTRADA sigue sostenida por el
|
|
89
|
+
* filesystem — solo el mecanismo explicito para pedirla no existe ahi.
|
|
90
|
+
* Por eso, y solo para esta combinacion exacta (win32 + EPERM en el fsync,
|
|
91
|
+
* nunca en el open), esta funcion degrada a no-op en vez de lanzar; culquier
|
|
92
|
+
* otra plataforma, o cualquier otro código de error incluso en Windows
|
|
93
|
+
* (el open fallando, ENOENT, EACCES por permisos reales), sigue lanzando. */
|
|
79
94
|
function fsyncDirSync(dir) {
|
|
80
95
|
let dirFd;
|
|
81
96
|
try {
|
|
82
97
|
dirFd = fs_1.default.openSync(dir, 'r');
|
|
83
|
-
|
|
98
|
+
try {
|
|
99
|
+
fs_1.default.fsyncSync(dirFd);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (process.platform === 'win32' && error.code === 'EPERM')
|
|
103
|
+
return;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
84
106
|
}
|
|
85
107
|
catch (error) {
|
|
86
108
|
throw new Error(`fsync de directorio fallo para ${dir}: ${error.message}`);
|
|
@@ -10,6 +10,7 @@ exports.installArtifact = installArtifact;
|
|
|
10
10
|
// src/core/executor.ts
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const paths_1 = require("./paths");
|
|
13
14
|
function removeArtifact(targetPath) {
|
|
14
15
|
let exists = false;
|
|
15
16
|
try {
|
|
@@ -39,7 +40,45 @@ function stageArtifact(sourcePath, targetPath, method) {
|
|
|
39
40
|
const staged = path_1.default.join(parent, `.${path_1.default.basename(targetPath)}.${process.pid}.staged`);
|
|
40
41
|
fs_1.default.rmSync(staged, { recursive: true, force: true });
|
|
41
42
|
if (method === 'symlink') {
|
|
42
|
-
fs_1.default.
|
|
43
|
+
const sourceIsDirectory = fs_1.default.statSync(sourcePath).isDirectory();
|
|
44
|
+
if (sourceIsDirectory) {
|
|
45
|
+
// A directory *symlink* ('dir') needs SeCreateSymbolicLinkPrivilege on
|
|
46
|
+
// Windows — denied by default on unprivileged accounts, including
|
|
47
|
+
// GitHub Actions' windows-latest runner, so every install here would
|
|
48
|
+
// throw EPERM. A *junction* is a different NTFS reparse-point kind
|
|
49
|
+
// that Windows lets any account create, and Node/libuv report it the
|
|
50
|
+
// same way a symlink is reported (`lstat().isSymbolicLink()` is true,
|
|
51
|
+
// `readlinkSync()` resolves it) — so every downstream consumer
|
|
52
|
+
// (verify(), R19's provider-facts hashing, doctor's symlink checks)
|
|
53
|
+
// keeps working unmodified. Junctions require an absolute target,
|
|
54
|
+
// which `sourcePath` always is here (registry content roots are
|
|
55
|
+
// resolved under `awmHome()`). POSIX platforms are unaffected: 'dir'
|
|
56
|
+
// is a plain no-op hint there.
|
|
57
|
+
fs_1.default.symlinkSync(sourcePath, staged, (0, paths_1.isWindowsNative)() ? 'junction' : 'dir');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// A junction is an NTFS *directory* reparse point — it has no
|
|
61
|
+
// equivalent for an individual FILE artifact (agent .md, workflow
|
|
62
|
+
// .md, ...), so a file source must never be passed to it (it was,
|
|
63
|
+
// before this branch existed, and that silently produced a
|
|
64
|
+
// reparse point that could not resolve back to the file — see
|
|
65
|
+
// tests/core/bundle-install.test.ts's "claude-code agents" case).
|
|
66
|
+
// A 'file'-type symlink is the correct primitive here, but it
|
|
67
|
+
// needs the same SeCreateSymbolicLinkPrivilege a directory symlink
|
|
68
|
+
// does, and Windows has no privilege-free substitute for files the
|
|
69
|
+
// way junctions are for directories. So: attempt the real symlink
|
|
70
|
+
// (works, and keeps `awm update` propagation, whenever the
|
|
71
|
+
// privilege IS available — e.g. Developer Mode) and fall back to a
|
|
72
|
+
// plain copy otherwise, mirroring the established fallback already
|
|
73
|
+
// used for the bootstrap skill file (hooks/claude.ts,
|
|
74
|
+
// tests/commands/hooks/install-symlink-fallback.test.ts).
|
|
75
|
+
try {
|
|
76
|
+
fs_1.default.symlinkSync(sourcePath, staged, 'file');
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
fs_1.default.copyFileSync(sourcePath, staged);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
43
82
|
}
|
|
44
83
|
else {
|
|
45
84
|
fs_1.default.cpSync(sourcePath, staged, { recursive: true });
|
|
@@ -339,7 +339,19 @@ function defaultTransactionDeps() {
|
|
|
339
339
|
return;
|
|
340
340
|
}
|
|
341
341
|
if (op.method === 'symlink' && !stat.isSymbolicLink()) {
|
|
342
|
-
|
|
342
|
+
// executor.ts's stageArtifact falls back to a plain copy for a
|
|
343
|
+
// FILE source when the real 'file'-type symlink throws (no
|
|
344
|
+
// privilege-free equivalent to a directory junction exists for
|
|
345
|
+
// individual files on Windows) — accept that fallback here
|
|
346
|
+
// rather than failing verification for an install that landed
|
|
347
|
+
// correctly, just not as a symlink. A directory source always
|
|
348
|
+
// gets a privilege-free junction and should never legitimately
|
|
349
|
+
// reach this branch, so this stays scoped to files only.
|
|
350
|
+
const sourceIsDirectory = fs_1.default.existsSync(op.sourcePath) && fs_1.default.statSync(op.sourcePath).isDirectory();
|
|
351
|
+
const acceptableFileFallback = !sourceIsDirectory && stat.isFile();
|
|
352
|
+
if (!acceptableFileFallback) {
|
|
353
|
+
throw new Error(`verification failed: ${op.targetPath} is not a symlink`);
|
|
354
|
+
}
|
|
343
355
|
}
|
|
344
356
|
if (op.method === 'copy' && stat.isSymbolicLink()) {
|
|
345
357
|
throw new Error(`verification failed: ${op.targetPath} is unexpectedly a symlink`);
|