@anureo/code-review 0.0.0-stage → 0.3.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/LICENSE +201 -0
- package/bin/anureo-review.d.ts +2 -0
- package/bin/anureo-review.js +3 -0
- package/package.json +43 -4
- package/src/angles.d.ts +19 -0
- package/src/angles.js +217 -0
- package/src/cli.d.ts +27 -0
- package/src/cli.js +157 -0
- package/src/client.d.ts +18 -0
- package/src/client.js +54 -0
- package/src/errors.d.ts +27 -0
- package/src/errors.js +44 -0
- package/src/file-state.d.ts +13 -0
- package/src/file-state.js +24 -0
- package/src/fix-patch.d.ts +13 -0
- package/src/fix-patch.js +86 -0
- package/src/fix-tree.d.ts +25 -0
- package/src/fix-tree.js +43 -0
- package/src/fix.d.ts +37 -0
- package/src/fix.js +110 -0
- package/src/git-state.d.ts +9 -0
- package/src/git-state.js +23 -0
- package/src/git.d.ts +15 -0
- package/src/git.js +36 -0
- package/src/index.d.ts +36 -0
- package/src/index.js +30 -0
- package/src/options.d.ts +61 -0
- package/src/options.js +56 -0
- package/src/paths.d.ts +22 -0
- package/src/paths.js +46 -0
- package/src/progress.d.ts +30 -0
- package/src/progress.js +0 -0
- package/src/prompts.d.ts +42 -0
- package/src/prompts.js +104 -0
- package/src/report.d.ts +5 -0
- package/src/report.js +80 -0
- package/src/results.d.ts +69 -0
- package/src/results.js +0 -0
- package/src/review-execution.d.ts +52 -0
- package/src/review-execution.js +81 -0
- package/src/review-report.d.ts +21 -0
- package/src/review-report.js +30 -0
- package/src/review.d.ts +16 -0
- package/src/review.js +184 -0
- package/src/schema.d.ts +77 -0
- package/src/schema.js +47 -0
- package/src/validation.d.ts +21 -0
- package/src/validation.js +105 -0
- package/src/worktree.d.ts +23 -0
- package/src/worktree.js +117 -0
- package/README.md +0 -3
package/src/schema.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Internal structural schema for one analysis assignment; validatePlan checks source scope,
|
|
3
|
+
* canonical ownership and ID limits after structured output parsing. Unknown fields are rejected. */
|
|
4
|
+
export const moduleSchema = z.strictObject({
|
|
5
|
+
/** Exact selection identity; validatePlan rejects duplicates and IDs over 256 UTF-8 bytes. */
|
|
6
|
+
id: z.string().min(1),
|
|
7
|
+
name: z.string().min(1),
|
|
8
|
+
/** Module responsibilities passed to the angle agent with the source assignment. */
|
|
9
|
+
description: z.string().min(1),
|
|
10
|
+
/** Nonempty source list relative to the review directory; aliases are normalized by validatePlan. */
|
|
11
|
+
files: z.array(z.string().min(1)).min(1),
|
|
12
|
+
});
|
|
13
|
+
/** Internal structured output contract submitted by analyzeModules; requires a nonempty plan
|
|
14
|
+
* and rejects unknown fields. Filesystem validity and ownership are checked by validatePlan. */
|
|
15
|
+
export const modulePlanSchema = z.strictObject({
|
|
16
|
+
/** Analysis overview retained in the plan and printed by formatReport. */
|
|
17
|
+
summary: z.string().min(1),
|
|
18
|
+
modules: z.array(moduleSchema).min(1),
|
|
19
|
+
});
|
|
20
|
+
/** Severity values accepted for Finding.severity.
|
|
21
|
+
* The findings schema and review prompt both derive their vocabulary from this list. */
|
|
22
|
+
export const findingSeverities = ['P0', 'P1', 'P2', 'P3'];
|
|
23
|
+
/** Internal finding shape used by reviewOutputSchema. Rejects unknown fields; validateFindings
|
|
24
|
+
* separately checks canonical module ownership and inclusive ranges against current source text. */
|
|
25
|
+
export const findingSchema = z.strictObject({
|
|
26
|
+
severity: z.enum(findingSeverities),
|
|
27
|
+
title: z.string().min(1),
|
|
28
|
+
body: z.string().min(1),
|
|
29
|
+
/** Actionable remedy text, retained for reporting and the optional fix task. */
|
|
30
|
+
suggestion: z.string().min(1),
|
|
31
|
+
/** Source path relative to the directory reviewed by the agent. */
|
|
32
|
+
file: z.string().min(1),
|
|
33
|
+
/** First source line, 1-based and inclusive. */
|
|
34
|
+
lineStart: z.number().int().min(1),
|
|
35
|
+
/** Last source line, inclusive; ordering and file bounds require validateFindings. */
|
|
36
|
+
lineEnd: z.number().int().min(1),
|
|
37
|
+
});
|
|
38
|
+
/** Internal output schema supplied by reviewAngle for review and verification agents.
|
|
39
|
+
* An empty findings array is valid. validateFindings checks source locations after parsing. */
|
|
40
|
+
export const reviewOutputSchema = z.strictObject({ findings: z.array(findingSchema) });
|
|
41
|
+
/** Internal output schema supplied by applyFix for the agent's result after editing sources.
|
|
42
|
+
* applyFix checks Git state and observed edits before applying a patch to the original tree. */
|
|
43
|
+
export const fixResultSchema = z.strictObject({
|
|
44
|
+
summary: z.string().min(1),
|
|
45
|
+
/** Agent-reported paths. The field is required and may be empty. */
|
|
46
|
+
changedFiles: z.array(z.string().min(1)),
|
|
47
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Finding, ModulePlan, ReviewModule } from "./schema.js";
|
|
2
|
+
/** Check the listed assignments in a schema-parsed plan and return a copy with canonical,
|
|
3
|
+
* slash-separated paths relative to cwd. Pass the canonical absolute review directory and
|
|
4
|
+
* absolute workflow storage path for the source view being checked. Duplicate or oversized IDs,
|
|
5
|
+
* duplicate file ownership, invalid paths and filesystem failures reject in traversal order.
|
|
6
|
+
*
|
|
7
|
+
* analyzeModules checks the agent's source view first and, in fix mode, checks the resulting
|
|
8
|
+
* assignments against the original tree before selecting modules. A rejection prevents dispatch. */
|
|
9
|
+
export declare function validatePlan(cwd: string, plan: ModulePlan, workflowDirectory: string): Promise<ModulePlan>;
|
|
10
|
+
/** Return copies of schema-parsed findings with canonical source paths, preserving order.
|
|
11
|
+
* Requires canonical absolute cwd, absolute workflow storage and a module from validatePlan.
|
|
12
|
+
* Checks ownership and inclusive ranges using one UTF-8 read per canonical file in this call,
|
|
13
|
+
* with no source lock.
|
|
14
|
+
* Invalid paths, ownership, ranges or filesystem failures reject. reviewAngle awaits the check
|
|
15
|
+
* inside withAgentWorktree before returning completed findings or recording failed coverage. */
|
|
16
|
+
export declare function validateFindings(cwd: string, module: ReviewModule, findings: Finding[], workflowDirectory: string): Promise<Finding[]>;
|
|
17
|
+
/** Invoke the supplied local validator once and await its value. A synchronous throw or rejection
|
|
18
|
+
* becomes REVIEW_VALIDATION_FAILED, with the original Error message or a generic message.
|
|
19
|
+
* analyzeModules and reviewAngle supply callbacks after the agent returns. Analysis propagates
|
|
20
|
+
* this failure, while reviewAngle records it as failed coverage. */
|
|
21
|
+
export declare function validateOutput<T>(validate: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFile, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { HarnessSdkError } from '@anureo/sdk';
|
|
4
|
+
import { assertReviewablePath, isInsideDirectory } from "./paths.js";
|
|
5
|
+
/** Check the listed assignments in a schema-parsed plan and return a copy with canonical,
|
|
6
|
+
* slash-separated paths relative to cwd. Pass the canonical absolute review directory and
|
|
7
|
+
* absolute workflow storage path for the source view being checked. Duplicate or oversized IDs,
|
|
8
|
+
* duplicate file ownership, invalid paths and filesystem failures reject in traversal order.
|
|
9
|
+
*
|
|
10
|
+
* analyzeModules checks the agent's source view first and, in fix mode, checks the resulting
|
|
11
|
+
* assignments against the original tree before selecting modules. A rejection prevents dispatch. */
|
|
12
|
+
export async function validatePlan(cwd, plan, workflowDirectory) {
|
|
13
|
+
const claimedIds = new Set();
|
|
14
|
+
const claimedFiles = new Set();
|
|
15
|
+
const modules = [];
|
|
16
|
+
for (const module of plan.modules) {
|
|
17
|
+
claimModuleId(module.id, claimedIds);
|
|
18
|
+
const files = [];
|
|
19
|
+
for (const file of module.files) {
|
|
20
|
+
const canonicalFile = await resolveReviewableSourcePath(cwd, file, workflowDirectory);
|
|
21
|
+
claimSourceFile(canonicalFile, claimedFiles);
|
|
22
|
+
files.push(canonicalFile);
|
|
23
|
+
}
|
|
24
|
+
modules.push({ ...module, files });
|
|
25
|
+
}
|
|
26
|
+
return { ...plan, modules };
|
|
27
|
+
}
|
|
28
|
+
/** Return copies of schema-parsed findings with canonical source paths, preserving order.
|
|
29
|
+
* Requires canonical absolute cwd, absolute workflow storage and a module from validatePlan.
|
|
30
|
+
* Checks ownership and inclusive ranges using one UTF-8 read per canonical file in this call,
|
|
31
|
+
* with no source lock.
|
|
32
|
+
* Invalid paths, ownership, ranges or filesystem failures reject. reviewAngle awaits the check
|
|
33
|
+
* inside withAgentWorktree before returning completed findings or recording failed coverage. */
|
|
34
|
+
export async function validateFindings(cwd, module, findings, workflowDirectory) {
|
|
35
|
+
const lineCounts = new Map();
|
|
36
|
+
const validated = [];
|
|
37
|
+
for (const finding of findings) {
|
|
38
|
+
const file = await resolveReviewableSourcePath(cwd, finding.file, workflowDirectory);
|
|
39
|
+
if (!module.files.includes(file))
|
|
40
|
+
throw new Error(`Finding is outside module ${module.id}: ${file}`);
|
|
41
|
+
let lines = lineCounts.get(file);
|
|
42
|
+
if (lines === undefined) {
|
|
43
|
+
const text = await readFile(resolve(cwd, file), 'utf8');
|
|
44
|
+
lines = countSourceLines(text);
|
|
45
|
+
lineCounts.set(file, lines);
|
|
46
|
+
}
|
|
47
|
+
assertFindingLocation(finding, file, lines);
|
|
48
|
+
validated.push({ ...finding, file });
|
|
49
|
+
}
|
|
50
|
+
return validated;
|
|
51
|
+
}
|
|
52
|
+
/** Invoke the supplied local validator once and await its value. A synchronous throw or rejection
|
|
53
|
+
* becomes REVIEW_VALIDATION_FAILED, with the original Error message or a generic message.
|
|
54
|
+
* analyzeModules and reviewAngle supply callbacks after the agent returns. Analysis propagates
|
|
55
|
+
* this failure, while reviewAngle records it as failed coverage. */
|
|
56
|
+
export async function validateOutput(validate) {
|
|
57
|
+
try {
|
|
58
|
+
return await validate();
|
|
59
|
+
}
|
|
60
|
+
catch (cause) {
|
|
61
|
+
throw new HarnessSdkError('REVIEW_VALIDATION_FAILED', cause instanceof Error ? cause.message : 'Invalid review output');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Reserve the exact ID after checking uniqueness and the same 256-byte UTF-8 cap
|
|
65
|
+
* applied to caller-selected module IDs. */
|
|
66
|
+
function claimModuleId(id, claimedIds) {
|
|
67
|
+
if (claimedIds.has(id))
|
|
68
|
+
throw new Error(`Duplicate module id: ${id}`);
|
|
69
|
+
if (Buffer.byteLength(id) > 256)
|
|
70
|
+
throw new Error(`Module id is too long (max 256 bytes): ${id.slice(0, 64)}`);
|
|
71
|
+
claimedIds.add(id);
|
|
72
|
+
}
|
|
73
|
+
/** Claim a canonical relative path in the plan-wide ownership set. Reject repeated claims
|
|
74
|
+
* within a module or across modules, including paths that resolved from different aliases. */
|
|
75
|
+
function claimSourceFile(file, claimedFiles) {
|
|
76
|
+
if (claimedFiles.has(file))
|
|
77
|
+
throw new Error(`Source file assigned more than once: ${file}`);
|
|
78
|
+
claimedFiles.add(file);
|
|
79
|
+
}
|
|
80
|
+
/** Resolve one relative source path for both validators, rejecting storage, metadata, escapes,
|
|
81
|
+
* non-files and filesystem failures. Resolve symlinks before scope checks so aliases cannot
|
|
82
|
+
* disguise excluded targets; return the canonical path relative to cwd with '/' separators. */
|
|
83
|
+
async function resolveReviewableSourcePath(cwd, file, workflowDirectory) {
|
|
84
|
+
if (isAbsolute(file))
|
|
85
|
+
throw new Error(`Expected a relative source path: ${file}`);
|
|
86
|
+
const absolute = await realpath(resolve(cwd, file));
|
|
87
|
+
await assertReviewablePath(cwd, absolute, workflowDirectory);
|
|
88
|
+
if (!isInsideDirectory(cwd, absolute) || !(await stat(absolute)).isFile()) {
|
|
89
|
+
throw new Error(`Source file is outside the review directory or is not a file: ${file}`);
|
|
90
|
+
}
|
|
91
|
+
return relative(cwd, absolute).split(sep).join('/');
|
|
92
|
+
}
|
|
93
|
+
/** Count addressable source lines across CRLF and single-character line terminators.
|
|
94
|
+
* A final terminator does not create an extra line; empty files have no valid finding location. */
|
|
95
|
+
function countSourceLines(text) {
|
|
96
|
+
const segments = text.split(/\r\n|[\n\r\u2028\u2029]/u);
|
|
97
|
+
return text.length === 0 ? 0 : segments.length - (segments.at(-1) === '' ? 1 : 0);
|
|
98
|
+
}
|
|
99
|
+
/** Reject a schema-parsed finding unless its inclusive range fits the observed source line count.
|
|
100
|
+
* Keep the normalized path in the diagnostic, matching the finding returned on success. */
|
|
101
|
+
function assertFindingLocation(finding, file, lines) {
|
|
102
|
+
if (finding.lineStart < 1 || finding.lineEnd < finding.lineStart || finding.lineEnd > lines) {
|
|
103
|
+
throw new Error(`Invalid finding location: ${file}:${finding.lineStart}-${finding.lineEnd}; expected 1 <= lineStart <= lineEnd <= ${lines}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Internal checkout paths valid during the withAgentWorktree callback. */
|
|
2
|
+
export interface AgentWorktree {
|
|
3
|
+
/** Absolute Git root of the detached checkout. */
|
|
4
|
+
root: string;
|
|
5
|
+
/** Absolute parent directory where applyFix stages its baseline and patch. */
|
|
6
|
+
scratch: string;
|
|
7
|
+
}
|
|
8
|
+
/** Run one agent operation and await its result and any checkout cleanup.
|
|
9
|
+
* Supply cwd and the workflow storage directory as absolute paths. If supplied, root must be
|
|
10
|
+
* the canonical Git root containing cwd.
|
|
11
|
+
* With a root, the callback receives a checkout-local cwd and any in-repository storage path
|
|
12
|
+
* is remapped for validation. Without a root, the callback receives the original paths and
|
|
13
|
+
* no checkout handle. Await all checkout access inside the callback, including patch staging.
|
|
14
|
+
*
|
|
15
|
+
* analyzeModules uses the returned plan before dispatching reviews. reviewAngle and applyFix
|
|
16
|
+
* report checkout rejections as failed angle or fix results, respectively.
|
|
17
|
+
* Setup or callback failures propagate even if cleanup also fails. Cleanup alone rejects with
|
|
18
|
+
* REVIEW_FIX_CLEANUP and can leave files behind, even after applyFix patches the original tree. */
|
|
19
|
+
export declare function withAgentWorktree<T>(options: {
|
|
20
|
+
root?: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
directory: string;
|
|
23
|
+
}, use: (cwd: string, directory: string, worktree?: AgentWorktree) => Promise<T>): Promise<T>;
|
package/src/worktree.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Creates a private Git checkout of the current source for one agent session.
|
|
2
|
+
import { copyFile, lstat, mkdir, mkdtemp, realpath, rm, readlink, symlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
5
|
+
import { HarnessSdkError } from '@anureo/sdk';
|
|
6
|
+
import { reviewGit } from "./git.js";
|
|
7
|
+
import { hasOperationalPathSegment, isInsideDirectory } from "./paths.js";
|
|
8
|
+
/** Run one agent operation and await its result and any checkout cleanup.
|
|
9
|
+
* Supply cwd and the workflow storage directory as absolute paths. If supplied, root must be
|
|
10
|
+
* the canonical Git root containing cwd.
|
|
11
|
+
* With a root, the callback receives a checkout-local cwd and any in-repository storage path
|
|
12
|
+
* is remapped for validation. Without a root, the callback receives the original paths and
|
|
13
|
+
* no checkout handle. Await all checkout access inside the callback, including patch staging.
|
|
14
|
+
*
|
|
15
|
+
* analyzeModules uses the returned plan before dispatching reviews. reviewAngle and applyFix
|
|
16
|
+
* report checkout rejections as failed angle or fix results, respectively.
|
|
17
|
+
* Setup or callback failures propagate even if cleanup also fails. Cleanup alone rejects with
|
|
18
|
+
* REVIEW_FIX_CLEANUP and can leave files behind, even after applyFix patches the original tree. */
|
|
19
|
+
export async function withAgentWorktree(options, use) {
|
|
20
|
+
const { root, cwd, directory } = options;
|
|
21
|
+
if (root === undefined)
|
|
22
|
+
return use(cwd, directory);
|
|
23
|
+
const scratch = await mkdtemp(resolve(await realpath(tmpdir()), 'anureo-review-'));
|
|
24
|
+
const checkout = resolve(scratch, 'checkout');
|
|
25
|
+
const worktree = { root: checkout, scratch };
|
|
26
|
+
let registered = false;
|
|
27
|
+
let failed = false;
|
|
28
|
+
try {
|
|
29
|
+
const head = await registerCheckout(root, worktree);
|
|
30
|
+
registered = true;
|
|
31
|
+
await reviewGit(checkout, 'read-tree', head);
|
|
32
|
+
await copySource(root, checkout, directory);
|
|
33
|
+
const localCwd = resolve(checkout, relative(root, cwd));
|
|
34
|
+
const localDirectory = isInsideDirectory(root, directory) ? resolve(checkout, relative(root, directory)) : directory;
|
|
35
|
+
await mkdir(localCwd, { recursive: true });
|
|
36
|
+
return await use(localCwd, localDirectory, worktree);
|
|
37
|
+
}
|
|
38
|
+
catch (cause) {
|
|
39
|
+
failed = true;
|
|
40
|
+
throw cause;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
try {
|
|
44
|
+
if (registered)
|
|
45
|
+
await reviewGit(root, 'worktree', 'remove', '--force', checkout);
|
|
46
|
+
await rm(scratch, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
catch (cause) {
|
|
49
|
+
if (!failed)
|
|
50
|
+
throw new HarnessSdkError('REVIEW_FIX_CLEANUP', `Could not remove temporary worktree at ${checkout}`, false, false, { cause });
|
|
51
|
+
// A secondary cleanup failure must not hide a setup or callback failure.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Register a detached checkout with hooks disabled and return the baseline for its index.
|
|
56
|
+
* Fall back to an empty commit if HEAD lookup fails, including on an unborn branch. Reject on
|
|
57
|
+
* registration failure. withAgentWorktree owns index initialization, source copying and removal. */
|
|
58
|
+
async function registerCheckout(root, worktree) {
|
|
59
|
+
const head = await reviewGit(root, 'rev-parse', '--verify', 'HEAD').then(value => value.trim(), () => emptyCommit(root, worktree.scratch));
|
|
60
|
+
await reviewGit(root, '-c', 'core.hooksPath=' + resolve(worktree.scratch, 'hooks'), 'worktree', 'add', '--detach', '--no-checkout', worktree.root, head);
|
|
61
|
+
return head;
|
|
62
|
+
}
|
|
63
|
+
/** Write a fallback tree and commit into the source repository's object store without updating
|
|
64
|
+
* refs. registerCheckout awaits the commit ID and propagates filesystem and Git failures. */
|
|
65
|
+
async function emptyCommit(root, scratch) {
|
|
66
|
+
const empty = resolve(scratch, 'empty');
|
|
67
|
+
await writeFile(empty, '');
|
|
68
|
+
const tree = (await reviewGit(root, 'hash-object', '-w', '-t', 'tree', '--no-filters', empty)).trim();
|
|
69
|
+
return (await reviewGit(root, '-c', 'user.name=Anureo', '-c', 'user.email=anureo@localhost', '-c', 'commit.gpgSign=false', 'commit-tree', tree, '-m', 'Empty review baseline')).trim();
|
|
70
|
+
}
|
|
71
|
+
/** Copy current tracked and non-ignored untracked files before withAgentWorktree invokes its
|
|
72
|
+
* callback. Exclude operational paths and both the configured and resolved workflow storage
|
|
73
|
+
* paths. Reads are sequential and acquire no lock. Git or unexpected copy failures reject. */
|
|
74
|
+
async function copySource(root, checkout, directory) {
|
|
75
|
+
const storage = await realpath(directory).catch(() => directory);
|
|
76
|
+
const files = new Set((await reviewGit(root, 'ls-files', '-z', '--cached', '--others', '--exclude-standard')).split('\0'));
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
if (!file || hasOperationalPathSegment(file.split('/')))
|
|
79
|
+
continue;
|
|
80
|
+
const source = resolve(root, file);
|
|
81
|
+
if (isInsideDirectory(storage, source) || isInsideDirectory(directory, source))
|
|
82
|
+
continue;
|
|
83
|
+
await copySourceEntry({ root, checkout, file, source });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Materialize regular files and symlinks with lexical targets inside root, rebasing links to
|
|
87
|
+
* the checkout. Treat ENOENT and ENOTDIR as absent entries because the index can still list
|
|
88
|
+
* deleted paths. Other copy failures propagate to copySource. */
|
|
89
|
+
async function copySourceEntry({ root, checkout, file, source }) {
|
|
90
|
+
const parent = dirname(source);
|
|
91
|
+
try {
|
|
92
|
+
// Git can list old indexed children of a directory replaced by a symlink.
|
|
93
|
+
if (await realpath(parent) !== parent)
|
|
94
|
+
return;
|
|
95
|
+
const info = await lstat(source);
|
|
96
|
+
const target = resolve(checkout, file);
|
|
97
|
+
const targetParent = dirname(target);
|
|
98
|
+
if (info.isFile()) {
|
|
99
|
+
await mkdir(targetParent, { recursive: true });
|
|
100
|
+
await copyFile(source, target);
|
|
101
|
+
}
|
|
102
|
+
else if (info.isSymbolicLink()) {
|
|
103
|
+
const link = resolve(parent, await readlink(source));
|
|
104
|
+
if (!isInsideDirectory(root, link))
|
|
105
|
+
return;
|
|
106
|
+
await mkdir(targetParent, { recursive: true });
|
|
107
|
+
const localLink = relative(targetParent, resolve(checkout, relative(root, link))) || '.';
|
|
108
|
+
const type = await lstat(link).then(value => value.isDirectory() ? 'dir' : 'file', () => 'file');
|
|
109
|
+
await symlink(localLink, target, type);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (cause) {
|
|
113
|
+
const code = cause && typeof cause === 'object' && 'code' in cause ? String(cause.code) : undefined;
|
|
114
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR')
|
|
115
|
+
throw cause;
|
|
116
|
+
}
|
|
117
|
+
}
|
package/README.md
DELETED