create-quiver 0.15.0 → 0.15.2
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/.github/workflows/ci.yml +3 -0
- package/CHANGELOG.md +6 -0
- package/README.md +33 -0
- package/README_FOR_AI.md +13 -6
- package/ROADMAP.md +5 -0
- package/docs/AI_CONTEXT.md.template +4 -0
- package/docs/AI_ONBOARDING_PROMPT.md.template +8 -0
- package/docs/CLI_UX_GUIDE.md +31 -2
- package/docs/COMMANDS.md.template +16 -5
- package/docs/TROUBLESHOOTING.md +70 -0
- package/docs/TROUBLESHOOTING.md.template +20 -0
- package/docs/WORKFLOW.md.template +2 -2
- package/docs/getting-started/installation.md +86 -0
- package/docs/reference/commands.md +39 -5
- package/package.json +1 -1
- package/specs/quiver-v31-ai-model-catalog-agent-selection/EVIDENCE_REPORT.md +185 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/EXECUTION_PLAN.md +85 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/SPEC.md +337 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/STATUS.md +30 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/pr.md +98 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +30 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/slice.json +62 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/slice.json +72 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/slice.json +78 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/slice.json +77 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/slice.json +84 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/slice.json +72 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/EXECUTION_BRIEF.md +58 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/slice.json +84 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/EXECUTION_BRIEF.md +59 -0
- package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/slice.json +94 -0
- package/specs/quiver-v32-npx-installation-guidance/EVIDENCE_REPORT.md +26 -0
- package/specs/quiver-v32-npx-installation-guidance/SPEC.md +55 -0
- package/specs/quiver-v32-npx-installation-guidance/STATUS.md +23 -0
- package/specs/quiver-v32-npx-installation-guidance/pr.md +72 -0
- package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/slice.json +75 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/EVIDENCE_REPORT.md +93 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/EXECUTION_PLAN.md +83 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/SPEC.md +158 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/STATUS.md +31 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/pr.md +109 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/CLOSURE_BRIEF.md +30 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/slice.json +65 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/slice.json +74 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/CLOSURE_BRIEF.md +20 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/slice.json +79 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/slice.json +75 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/slice.json +73 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/CLOSURE_BRIEF.md +20 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/slice.json +91 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/CLOSURE_BRIEF.md +20 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/slice.json +87 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/CLOSURE_BRIEF.md +21 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/slice.json +97 -0
- package/src/create-quiver/commands/ai.js +561 -46
- package/src/create-quiver/commands/flow.js +76 -14
- package/src/create-quiver/commands/spec.js +8 -1
- package/src/create-quiver/index.js +36 -12
- package/src/create-quiver/lib/agent-profiles.js +332 -4
- package/src/create-quiver/lib/ai/approval-candidates.js +86 -0
- package/src/create-quiver/lib/ai/execution-plan.js +7 -2
- package/src/create-quiver/lib/ai/executor.js +9 -2
- package/src/create-quiver/lib/ai/model-catalog.js +333 -0
- package/src/create-quiver/lib/ai/plan-review.js +77 -1
- package/src/create-quiver/lib/ai/providers.js +143 -4
- package/src/create-quiver/lib/ai/run-state.js +21 -4
- package/src/create-quiver/lib/approvals.js +103 -0
- package/src/create-quiver/lib/cli/selectors.js +53 -0
- package/src/create-quiver/lib/git.js +13 -4
- package/src/create-quiver/lib/paths.js +45 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
|
|
4
|
+
const { redactSecrets, truncateText } = require('./evidence');
|
|
4
5
|
const { quiverInternalPaths } = require('./init-layout');
|
|
5
6
|
|
|
6
7
|
const PLANNER_APPROVAL_PHASES = Object.freeze(['acceptance', 'technical-plan']);
|
|
@@ -185,6 +186,107 @@ function renderApprovalStatus(report) {
|
|
|
185
186
|
return `approved ${report.phase}`;
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
function safePreview(text, maxLength = 180) {
|
|
190
|
+
const firstLines = String(text || '')
|
|
191
|
+
.split(/\r?\n/)
|
|
192
|
+
.map((line) => line.trim())
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
.slice(0, 3)
|
|
195
|
+
.join(' ');
|
|
196
|
+
const truncated = truncateText(redactSecrets(firstLines), maxLength);
|
|
197
|
+
return {
|
|
198
|
+
text: truncated.text.replace(/\s+/g, ' ').trim(),
|
|
199
|
+
truncated: truncated.truncated,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readCandidatePreview(projectRoot, draft) {
|
|
204
|
+
const draftPath = draft?.path || '';
|
|
205
|
+
if (!draftPath) {
|
|
206
|
+
return safePreview('');
|
|
207
|
+
}
|
|
208
|
+
const resolved = path.resolve(projectRoot, draftPath);
|
|
209
|
+
if (!fs.existsSync(resolved)) {
|
|
210
|
+
return {
|
|
211
|
+
text: '(missing draft artifact)',
|
|
212
|
+
truncated: false,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return safePreview(fs.readFileSync(resolved, 'utf8'));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildApprovalCandidate(projectRoot, phase, draft, latestVersion, report) {
|
|
219
|
+
const version = Number(draft.version || 0) || null;
|
|
220
|
+
const isLatest = Boolean(version && latestVersion && version === latestVersion);
|
|
221
|
+
const artifactExists = Boolean(draft.path && fs.existsSync(path.resolve(projectRoot, draft.path)));
|
|
222
|
+
const preview = readCandidatePreview(projectRoot, draft);
|
|
223
|
+
const approvable = isLatest && artifactExists && (report.status === 'draft' || report.status === 'stale' || report.status === 'approved');
|
|
224
|
+
const nextCommand = version
|
|
225
|
+
? `npx create-quiver ai approve --phase ${phase} --version ${version}`
|
|
226
|
+
: `npx create-quiver ai approve --phase ${phase} --version <n>`;
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
phase,
|
|
230
|
+
version,
|
|
231
|
+
label: version ? `v${version}` : 'unknown version',
|
|
232
|
+
path: draft.path || '',
|
|
233
|
+
source_file: draft.source_file || '',
|
|
234
|
+
created_at: draft.created_at || '',
|
|
235
|
+
raw_artifact_path: draft.raw_artifact_path || null,
|
|
236
|
+
output_source: draft.output_source || null,
|
|
237
|
+
input_compaction: draft.input_compaction || null,
|
|
238
|
+
current: isLatest,
|
|
239
|
+
latest: isLatest,
|
|
240
|
+
recommended: approvable,
|
|
241
|
+
approvable,
|
|
242
|
+
blocked: !approvable,
|
|
243
|
+
status: approvable ? 'approvable' : isLatest ? 'blocked' : 'history',
|
|
244
|
+
reason: approvable
|
|
245
|
+
? 'latest draft is eligible for approval'
|
|
246
|
+
: isLatest
|
|
247
|
+
? 'latest draft artifact is missing or not eligible'
|
|
248
|
+
: `not current; latest draft version is ${latestVersion || 'unknown'}`,
|
|
249
|
+
preview: preview.text,
|
|
250
|
+
preview_truncated: preview.truncated,
|
|
251
|
+
next_command: nextCommand,
|
|
252
|
+
recommended_action: approvable ? 'approve' : 'inspect',
|
|
253
|
+
review: null,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function buildPlannerApprovalCandidates(projectRoot, phase) {
|
|
258
|
+
const normalizedPhase = normalizePhase(phase);
|
|
259
|
+
if (!PLANNER_APPROVAL_PHASES.includes(normalizedPhase)) {
|
|
260
|
+
throw new Error(formatError(`approval candidates are only supported for planner phases: ${PLANNER_APPROVAL_PHASES.join(', ')}`));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const report = readPhaseApproval(projectRoot, normalizedPhase);
|
|
264
|
+
const drafts = normalizeDrafts(report.meta);
|
|
265
|
+
const latestVersion = latestDraftVersion(report.meta);
|
|
266
|
+
const candidates = drafts.map((draft) => buildApprovalCandidate(projectRoot, normalizedPhase, draft, latestVersion, report));
|
|
267
|
+
const current = candidates.find((candidate) => candidate.current) || null;
|
|
268
|
+
const recommended = candidates.find((candidate) => candidate.recommended) || null;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
phase: normalizedPhase,
|
|
272
|
+
approval_status: report.status,
|
|
273
|
+
latest_version: latestVersion,
|
|
274
|
+
current,
|
|
275
|
+
recommended,
|
|
276
|
+
candidates,
|
|
277
|
+
history: candidates.filter((candidate) => !candidate.current),
|
|
278
|
+
approved: report.approved
|
|
279
|
+
? {
|
|
280
|
+
path: report.approved.path,
|
|
281
|
+
version: Number(report.meta?.approved?.version || 0) || null,
|
|
282
|
+
source_file: report.meta?.approved?.source_file || '',
|
|
283
|
+
approved_at: report.meta?.approved?.approved_at || '',
|
|
284
|
+
}
|
|
285
|
+
: null,
|
|
286
|
+
next_command: recommended?.next_command || `npx create-quiver ai plan --phase ${normalizedPhase}${normalizedPhase === 'acceptance' ? ' --input <requirements.md>' : ''} --dry-run`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
188
290
|
function writeApprovalArtifacts(projectRoot, phase, kind, sourceFile, contents, options = {}) {
|
|
189
291
|
const normalizedPhase = normalizePhase(phase);
|
|
190
292
|
const root = approvalRoot(projectRoot, normalizedPhase);
|
|
@@ -372,6 +474,7 @@ module.exports = {
|
|
|
372
474
|
approvePlannerPhase,
|
|
373
475
|
findDraftVersion,
|
|
374
476
|
latestDraftVersion,
|
|
477
|
+
buildPlannerApprovalCandidates,
|
|
375
478
|
normalizePhase,
|
|
376
479
|
readPhaseApproval,
|
|
377
480
|
renderApprovalStatus,
|
|
@@ -101,7 +101,60 @@ async function selectOption(message, rawOptions, options = {}) {
|
|
|
101
101
|
].join('\n')));
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
async function promptText(message, options = {}) {
|
|
105
|
+
const explicit = String(options.value || '').trim();
|
|
106
|
+
if (explicit) return explicit;
|
|
107
|
+
|
|
108
|
+
const mode = resolveUxMode({
|
|
109
|
+
interactive: options.interactive === true,
|
|
110
|
+
json: options.json,
|
|
111
|
+
noColor: options.noColor,
|
|
112
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
113
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
114
|
+
stderrIsTTY: options.stderrIsTTY,
|
|
115
|
+
}, options.env || process.env, {
|
|
116
|
+
input: options.input || process.stdin,
|
|
117
|
+
output: options.output || process.stdout,
|
|
118
|
+
error: options.error || process.stderr,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (mode.usePrompts) {
|
|
122
|
+
if (typeof options.promptText === 'function') {
|
|
123
|
+
const value = await options.promptText(message, options);
|
|
124
|
+
const normalized = String(value || '').trim();
|
|
125
|
+
if (!normalized && options.required !== false) {
|
|
126
|
+
throw new Error(formatError(`${options.name || 'input'} is required.`));
|
|
127
|
+
}
|
|
128
|
+
return normalized;
|
|
129
|
+
}
|
|
130
|
+
const clack = await loadClack(options.prompts);
|
|
131
|
+
const value = await clack.text({
|
|
132
|
+
message,
|
|
133
|
+
placeholder: options.placeholder,
|
|
134
|
+
initialValue: options.initialValue,
|
|
135
|
+
validate: options.required === false
|
|
136
|
+
? undefined
|
|
137
|
+
: (input) => (String(input || '').trim() ? undefined : 'Required'),
|
|
138
|
+
});
|
|
139
|
+
if (clack.isCancel && clack.isCancel(value)) {
|
|
140
|
+
throw new Error(formatError(`${options.name || 'input'} was canceled.`));
|
|
141
|
+
}
|
|
142
|
+
return String(value || '').trim();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.defaultValue !== undefined && options.defaultValue !== null) {
|
|
146
|
+
return String(options.defaultValue).trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const flag = options.flag || '--value';
|
|
150
|
+
throw new Error(formatError([
|
|
151
|
+
`${options.name || 'input'} requires an explicit value.`,
|
|
152
|
+
`Use ${flag} <value> or rerun with --interactive.`,
|
|
153
|
+
].join('\n')));
|
|
154
|
+
}
|
|
155
|
+
|
|
104
156
|
module.exports = {
|
|
105
157
|
normalizeSelectorOptions,
|
|
158
|
+
promptText,
|
|
106
159
|
selectOption,
|
|
107
160
|
};
|
|
@@ -19,6 +19,14 @@ function tryGit(args, cwd, options = {}) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function withWindowsLongPaths(args, options = {}) {
|
|
23
|
+
const platform = options.platform || process.platform;
|
|
24
|
+
if (platform !== 'win32') {
|
|
25
|
+
return args;
|
|
26
|
+
}
|
|
27
|
+
return ['-c', 'core.longpaths=true', ...args];
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
function hasRef(repoRoot, ref) {
|
|
23
31
|
try {
|
|
24
32
|
runGit(['show-ref', '--verify', '--quiet', ref], repoRoot);
|
|
@@ -45,11 +53,11 @@ function fetchRemote(repoRoot, remote = 'origin', args = ['--prune']) {
|
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
function worktreePrune(repoRoot) {
|
|
48
|
-
tryGit(['worktree', 'prune'], repoRoot);
|
|
56
|
+
tryGit(withWindowsLongPaths(['worktree', 'prune']), repoRoot);
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
function worktreeList(repoRoot) {
|
|
52
|
-
const text = tryGit(['worktree', 'list', '--porcelain'], repoRoot);
|
|
60
|
+
const text = tryGit(withWindowsLongPaths(['worktree', 'list', '--porcelain']), repoRoot);
|
|
53
61
|
const entries = [];
|
|
54
62
|
const chunks = text.trim().split('\n\n').filter(Boolean);
|
|
55
63
|
|
|
@@ -79,7 +87,7 @@ function worktreeAdd(repoRoot, worktreePath, ref, options = {}) {
|
|
|
79
87
|
args.push('--force');
|
|
80
88
|
}
|
|
81
89
|
args.push(worktreePath, ref);
|
|
82
|
-
return runGit(args, repoRoot);
|
|
90
|
+
return runGit(withWindowsLongPaths(args, options), repoRoot);
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
function worktreeRemove(repoRoot, worktreePath, force = false) {
|
|
@@ -88,7 +96,7 @@ function worktreeRemove(repoRoot, worktreePath, force = false) {
|
|
|
88
96
|
args.push('--force');
|
|
89
97
|
}
|
|
90
98
|
args.push(worktreePath);
|
|
91
|
-
return runGit(args, repoRoot);
|
|
99
|
+
return runGit(withWindowsLongPaths(args), repoRoot);
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
function branchDelete(repoRoot, branchName, force = false) {
|
|
@@ -207,6 +215,7 @@ module.exports = {
|
|
|
207
215
|
runGit,
|
|
208
216
|
statusPorcelain,
|
|
209
217
|
tryGit,
|
|
218
|
+
withWindowsLongPaths,
|
|
210
219
|
worktreeAdd,
|
|
211
220
|
worktreeList,
|
|
212
221
|
worktreePrune,
|
|
@@ -84,9 +84,42 @@ function realpathOrResolve(filePath, pathLib = path) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
const
|
|
87
|
+
function realpathCandidates(filePath, pathLib = path) {
|
|
88
|
+
const normalized = normalizeGitBashDrivePath(filePath, pathLib);
|
|
89
|
+
const candidates = [];
|
|
90
|
+
|
|
91
|
+
const add = (candidate) => {
|
|
92
|
+
if (!candidate) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const resolved = pathLib.resolve(normalizeGitBashDrivePath(candidate, pathLib));
|
|
96
|
+
if (!candidates.includes(resolved)) {
|
|
97
|
+
candidates.push(resolved);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
add(fs.realpathSync(normalized));
|
|
103
|
+
} catch {
|
|
104
|
+
// Fall back below for paths that do not exist yet.
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof fs.realpathSync.native === 'function') {
|
|
108
|
+
try {
|
|
109
|
+
add(fs.realpathSync.native(normalized));
|
|
110
|
+
} catch {
|
|
111
|
+
// Fall back below for paths that do not exist yet.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (candidates.length === 0) {
|
|
116
|
+
add(normalized);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return candidates;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isCandidateInsideRoot(rootPath, targetPath, pathLib = path) {
|
|
90
123
|
const windowsPath = pathLib === path.win32 || process.platform === 'win32';
|
|
91
124
|
const comparableRoot = windowsPath ? rootPath.toLowerCase() : rootPath;
|
|
92
125
|
const comparableTarget = windowsPath ? targetPath.toLowerCase() : targetPath;
|
|
@@ -99,6 +132,15 @@ function isPathInsideRoot(root, target, pathLib = path) {
|
|
|
99
132
|
return Boolean(relative && !relative.startsWith('..') && !pathLib.isAbsolute(relative));
|
|
100
133
|
}
|
|
101
134
|
|
|
135
|
+
function isPathInsideRoot(root, target, pathLib = path) {
|
|
136
|
+
const rootCandidates = realpathCandidates(root, pathLib);
|
|
137
|
+
const targetCandidates = realpathCandidates(target, pathLib);
|
|
138
|
+
|
|
139
|
+
return rootCandidates.some((rootPath) => targetCandidates.some((targetPath) => (
|
|
140
|
+
isCandidateInsideRoot(rootPath, targetPath, pathLib)
|
|
141
|
+
)));
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
function assertPathInsideRoot(root, target, label = 'path', pathLib = path) {
|
|
103
145
|
if (!isPathInsideRoot(root, target, pathLib)) {
|
|
104
146
|
throw new Error(formatError(`${label} must stay inside the project root: ${toPosixPath(target, pathLib)}`));
|