@uppercut-labs/agent-native 0.0.0-stage → 1.1.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 +21 -0
- package/README.md +164 -2
- package/agent/agent-prompt-short.md +1 -0
- package/agent/agent-prompt.md +26 -0
- package/bin/agent-native-site.mjs +269 -0
- package/bin/discover-routes.mjs +162 -0
- package/bin/verify-publication.mjs +317 -0
- package/dist/adapters/zod.d.ts +4 -0
- package/dist/adapters/zod.d.ts.map +1 -0
- package/dist/adapters/zod.js +20 -0
- package/dist/adapters/zod.js.map +1 -0
- package/dist/astro.d.ts +13 -0
- package/dist/astro.d.ts.map +1 -0
- package/dist/astro.js +41 -0
- package/dist/astro.js.map +1 -0
- package/dist/auth.d.ts +52 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +128 -0
- package/dist/auth.js.map +1 -0
- package/dist/browser.d.ts +47 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +219 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +80 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +484 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/assert-never.d.ts +2 -0
- package/dist/core/assert-never.d.ts.map +1 -0
- package/dist/core/assert-never.js +5 -0
- package/dist/core/assert-never.js.map +1 -0
- package/dist/core/binding-internal.d.ts +10 -0
- package/dist/core/binding-internal.d.ts.map +1 -0
- package/dist/core/binding-internal.js +15 -0
- package/dist/core/binding-internal.js.map +1 -0
- package/dist/core/composition.d.ts +119 -0
- package/dist/core/composition.d.ts.map +1 -0
- package/dist/core/composition.js +545 -0
- package/dist/core/composition.js.map +1 -0
- package/dist/core/contracts.d.ts +55 -0
- package/dist/core/contracts.d.ts.map +1 -0
- package/dist/core/contracts.js +271 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/diagnostics.d.ts +14 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +29 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/executor.d.ts +52 -0
- package/dist/core/executor.d.ts.map +1 -0
- package/dist/core/executor.js +98 -0
- package/dist/core/executor.js.map +1 -0
- package/dist/core/registry.d.ts +37 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +75 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/schema.d.ts +9 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +36 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/discovery.d.ts +16 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +23 -0
- package/dist/discovery.js.map +1 -0
- package/dist/doctor.d.ts +200 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +287 -0
- package/dist/doctor.js.map +1 -0
- package/dist/harness-codex.d.ts +17 -0
- package/dist/harness-codex.d.ts.map +1 -0
- package/dist/harness-codex.js +485 -0
- package/dist/harness-codex.js.map +1 -0
- package/dist/harness-mcp-bridge.d.ts +14 -0
- package/dist/harness-mcp-bridge.d.ts.map +1 -0
- package/dist/harness-mcp-bridge.js +159 -0
- package/dist/harness-mcp-bridge.js.map +1 -0
- package/dist/harness.d.ts +72 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +101 -0
- package/dist/harness.js.map +1 -0
- package/dist/http.d.ts +20 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +537 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +73 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +1039 -0
- package/dist/init.js.map +1 -0
- package/dist/mcp-apps.d.ts +9 -0
- package/dist/mcp-apps.d.ts.map +1 -0
- package/dist/mcp-apps.js +171 -0
- package/dist/mcp-apps.js.map +1 -0
- package/dist/mcp-core.d.ts +64 -0
- package/dist/mcp-core.d.ts.map +1 -0
- package/dist/mcp-core.js +358 -0
- package/dist/mcp-core.js.map +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2 -0
- package/dist/mcp.js.map +1 -0
- package/dist/next-browser.d.ts +14 -0
- package/dist/next-browser.d.ts.map +1 -0
- package/dist/next-browser.js +14 -0
- package/dist/next-browser.js.map +1 -0
- package/dist/next.d.ts +24 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +28 -0
- package/dist/next.js.map +1 -0
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +3 -0
- package/dist/package-version.js.map +1 -0
- package/dist/publication.d.ts +115 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +441 -0
- package/dist/publication.js.map +1 -0
- package/dist/registry.d.ts +3 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +2 -0
- package/dist/registry.js.map +1 -0
- package/docs/adopt-existing-site.md +189 -0
- package/docs/browser.md +70 -0
- package/docs/capabilities-and-bindings.md +141 -0
- package/docs/cli.md +88 -0
- package/docs/compatibility.md +32 -0
- package/docs/composition-and-versioning.md +113 -0
- package/docs/doctor-and-troubleshooting.md +58 -0
- package/docs/evidence/GHSA-ch52-4w7c-c8xp-2026-10-03.md +17 -0
- package/docs/evidence/GHSA-ch52-4w7c-c8xp-2026-10-10.md +30 -0
- package/docs/evidence/GHSA-ch52-4w7c-c8xp-2026-10-12.md +10 -0
- package/docs/evidence/UAN-002-house-style-review.md +29 -0
- package/docs/evidence/UAN-007-astro.md +43 -0
- package/docs/evidence/UAN-008-worker-sidecar.md +56 -0
- package/docs/evidence/UAN-009-init.md +43 -0
- package/docs/evidence/UAN-010-auth.md +17 -0
- package/docs/evidence/UAN-011-discovery.md +18 -0
- package/docs/evidence/UAN-012-mcp-apps.md +14 -0
- package/docs/evidence/UAN-013-astro-on-demand.md +47 -0
- package/docs/evidence/UAN-014-next.md +57 -0
- package/docs/evidence/UAN-015-node-hono.md +69 -0
- package/docs/evidence/UAN-016-composition.md +52 -0
- package/docs/evidence/UAN-017-version-migrations.md +50 -0
- package/docs/evidence/UAN-018-surface-overrides.md +30 -0
- package/docs/evidence/UAN-019-doctor.md +19 -0
- package/docs/evidence/UAN-020-negative-security-fixtures.md +61 -0
- package/docs/evidence/UAN-020-operational-failure-safety.md +49 -0
- package/docs/evidence/UAN-021-docs-coverage.md +30 -0
- package/docs/evidence/UAN-022-cross-platform-license-notices.md +39 -0
- package/docs/evidence/UAN-022-overhead-license-audit.md +233 -0
- package/docs/evidence/UAN-022-package-portability.md +54 -0
- package/docs/evidence/UAN-023-commercial-agent-hosts.md +36 -0
- package/docs/evidence/UAN-023-inspector-host-probe.md +37 -0
- package/docs/evidence/UAN-023-inspector-web-and-astro-adoption.md +44 -0
- package/docs/evidence/UAN-023-native-webmcp-chrome.md +44 -0
- package/docs/evidence/UAN-024-release-readiness-2026-10-10.md +33 -0
- package/docs/evidence/UAN-024-release-readiness.md +41 -0
- package/docs/evidence/codex-harness.md +56 -0
- package/docs/evidence/gate-2-domain-expansion.md +35 -0
- package/docs/evidence/recovery-and-selection-recipes-2026-10-15.md +63 -0
- package/docs/for-agents.md +50 -0
- package/docs/frameworks.md +62 -0
- package/docs/getting-started.md +109 -0
- package/docs/harness-codex.md +117 -0
- package/docs/harness-mcp-bridge.md +30 -0
- package/docs/http.md +80 -0
- package/docs/installation.md +95 -0
- package/docs/mcp-apps.md +57 -0
- package/docs/mcp.md +178 -0
- package/docs/next.md +73 -0
- package/docs/permissions-and-discovery.md +58 -0
- package/docs/programmatic-harness.md +88 -0
- package/docs/readability-frameworks.md +18 -0
- package/docs/reference.md +161 -0
- package/docs/release-handoff.md +41 -0
- package/docs/site-publication.md +85 -0
- package/docs/stability.md +53 -0
- package/docs/static-sites.md +141 -0
- package/docs/support-and-limitations.md +20 -0
- package/docs/surfaces.md +27 -0
- package/docs/upgrading-to-1.0.md +26 -0
- package/package.json +188 -4
- package/skill/agent-native/SKILL.md +54 -0
- package/skill/agent-native/references/adapter-decision.md +19 -0
- package/skill/agent-native/references/verification.md +25 -0
package/dist/init.js
ADDED
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, link, mkdir, readFile, realpath, rename, rm, rmdir, writeFile, } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export const INIT_PLAN_VERSION = 'uan.init-plan/v1';
|
|
6
|
+
export const INIT_OWNERSHIP_VERSION = 'uan.init-ownership/v1';
|
|
7
|
+
const MANIFEST_PATH = '.agent-native/ownership.json';
|
|
8
|
+
const BOOTSTRAP_PATH = '.agent-native/browser-entry.mjs';
|
|
9
|
+
const SETTINGS_PATH = '.agent-native/init.json';
|
|
10
|
+
const LOCKFILES = {
|
|
11
|
+
'package-lock.json': 'npm',
|
|
12
|
+
'pnpm-lock.yaml': 'pnpm',
|
|
13
|
+
'yarn.lock': 'yarn',
|
|
14
|
+
'bun.lock': 'bun',
|
|
15
|
+
'bun.lockb': 'bun',
|
|
16
|
+
};
|
|
17
|
+
function requireAt(values, index) {
|
|
18
|
+
const value = values[index];
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
throw new Error(`internal error: missing element at index ${index}`);
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function requireValue(record, key) {
|
|
24
|
+
const value = record[key];
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
throw new Error(`internal error: missing record entry ${key}`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function hash(value) {
|
|
30
|
+
return createHash('sha256').update(value).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
function stableValue(value) {
|
|
33
|
+
if (Array.isArray(value))
|
|
34
|
+
return value.map(stableValue);
|
|
35
|
+
if (typeof value !== 'object' || value === null)
|
|
36
|
+
return value;
|
|
37
|
+
return Object.fromEntries(Object.entries(value)
|
|
38
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
39
|
+
.map(([key, child]) => [key, stableValue(child)]));
|
|
40
|
+
}
|
|
41
|
+
function stableJson(value) {
|
|
42
|
+
return JSON.stringify(stableValue(value));
|
|
43
|
+
}
|
|
44
|
+
async function exists(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
await lstat(filePath);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
|
|
51
|
+
return false;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function assertNoSymlink(root, relativePath) {
|
|
56
|
+
const segments = relativePath.split(/[\\/]/).filter(Boolean);
|
|
57
|
+
let current = root;
|
|
58
|
+
for (const segment of segments) {
|
|
59
|
+
if (segment === '.' || segment === '..')
|
|
60
|
+
throw new Error('managed path is not normalized');
|
|
61
|
+
current = path.join(current, segment);
|
|
62
|
+
if (!(await exists(current)))
|
|
63
|
+
continue;
|
|
64
|
+
const info = await lstat(current);
|
|
65
|
+
if (info.isSymbolicLink())
|
|
66
|
+
throw new Error(`refusing symlink in managed path: ${relativePath}`);
|
|
67
|
+
}
|
|
68
|
+
let existingParent = path.dirname(current);
|
|
69
|
+
while (!(await exists(existingParent)))
|
|
70
|
+
existingParent = path.dirname(existingParent);
|
|
71
|
+
const parent = await realpath(existingParent);
|
|
72
|
+
if (parent !== root && !parent.startsWith(root + path.sep)) {
|
|
73
|
+
throw new Error(`managed path escapes project root: ${relativePath}`);
|
|
74
|
+
}
|
|
75
|
+
return current;
|
|
76
|
+
}
|
|
77
|
+
async function readOptional(root, relativePath) {
|
|
78
|
+
const target = await assertNoSymlink(root, relativePath);
|
|
79
|
+
if (!(await exists(target)))
|
|
80
|
+
return null;
|
|
81
|
+
return readFile(target, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
async function packageInfo(directory) {
|
|
84
|
+
try {
|
|
85
|
+
const packagePath = path.join(directory, 'package.json');
|
|
86
|
+
if ((await lstat(packagePath)).isSymbolicLink())
|
|
87
|
+
throw new Error('refusing symlink package.json');
|
|
88
|
+
const parsed = JSON.parse(await readFile(packagePath, 'utf8'));
|
|
89
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
90
|
+
return null;
|
|
91
|
+
return parsed;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
|
|
95
|
+
return null;
|
|
96
|
+
throw new Error(`cannot safely inspect package.json at ${directory}`, { cause: error });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function listDirectories(directory) {
|
|
100
|
+
try {
|
|
101
|
+
const entries = await (await import('node:fs/promises')).readdir(directory, {
|
|
102
|
+
withFileTypes: true,
|
|
103
|
+
});
|
|
104
|
+
return entries
|
|
105
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
106
|
+
.map((entry) => path.join(directory, entry.name));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
|
|
110
|
+
return [];
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function sourceTokens(source) {
|
|
115
|
+
let tokens = [];
|
|
116
|
+
for (let index = 0; index < source.length;) {
|
|
117
|
+
const char = requireAt(source, index);
|
|
118
|
+
if (/\s/.test(char)) {
|
|
119
|
+
index += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (char === '/' && source[index + 1] === '/') {
|
|
123
|
+
index = source.indexOf('\n', index + 2);
|
|
124
|
+
if (index < 0)
|
|
125
|
+
break;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (char === '/' && source[index + 1] === '*') {
|
|
129
|
+
const close = source.indexOf('*/', index + 2);
|
|
130
|
+
if (close < 0)
|
|
131
|
+
throw new Error('unterminated comment in Astro config');
|
|
132
|
+
index = close + 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === "'" || char === '"') {
|
|
136
|
+
let value = '';
|
|
137
|
+
index += 1;
|
|
138
|
+
while (index < source.length && source[index] !== char) {
|
|
139
|
+
if (source[index] === '\\') {
|
|
140
|
+
value += source[index + 1] ?? '';
|
|
141
|
+
index += 2;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
value += requireAt(source, index);
|
|
145
|
+
index += 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (source[index] !== char)
|
|
149
|
+
throw new Error('unterminated string in Astro config');
|
|
150
|
+
index += 1;
|
|
151
|
+
tokens.push(`string:${value}`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const word = source.slice(index).match(/^[A-Za-z_$][\w$]*/)?.[0];
|
|
155
|
+
if (word !== undefined) {
|
|
156
|
+
tokens.push(`word:${word}`);
|
|
157
|
+
index += word.length;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (source.startsWith('...', index)) {
|
|
161
|
+
tokens.push('...');
|
|
162
|
+
index += 3;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
tokens.push(char);
|
|
166
|
+
index += 1;
|
|
167
|
+
}
|
|
168
|
+
return tokens;
|
|
169
|
+
}
|
|
170
|
+
function staticAstroOutput(source) {
|
|
171
|
+
const tokens = sourceTokens(source);
|
|
172
|
+
const start = tokens.findIndex((token, index) => token === 'word:defineConfig' && tokens[index + 1] === '(' && tokens[index + 2] === '{');
|
|
173
|
+
if (start < 0)
|
|
174
|
+
return { output: 'unknown', serverAdapter: false, dynamic: true, patchable: false };
|
|
175
|
+
let depth = 0;
|
|
176
|
+
let output;
|
|
177
|
+
let serverAdapter = false;
|
|
178
|
+
let end = -1;
|
|
179
|
+
for (let index = start + 2; index < tokens.length; index += 1) {
|
|
180
|
+
const token = requireAt(tokens, index);
|
|
181
|
+
if (token === '{')
|
|
182
|
+
depth += 1;
|
|
183
|
+
else if (token === '}') {
|
|
184
|
+
depth -= 1;
|
|
185
|
+
if (depth === 0) {
|
|
186
|
+
end = index;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (depth === 1 &&
|
|
191
|
+
token === 'word:output' &&
|
|
192
|
+
tokens[index + 1] === ':' &&
|
|
193
|
+
tokens[index + 3] !== undefined &&
|
|
194
|
+
(tokens[index + 2] === 'string:static' || tokens[index + 2] === 'string:server') &&
|
|
195
|
+
(tokens[index + 3] === ',' || tokens[index + 3] === '}')) {
|
|
196
|
+
output = requireAt(tokens, index + 2).slice('string:'.length);
|
|
197
|
+
}
|
|
198
|
+
else if (depth === 1 &&
|
|
199
|
+
token === 'word:adapter' &&
|
|
200
|
+
tokens[index + 1] === ':' &&
|
|
201
|
+
tokens[index + 2] !== ',' &&
|
|
202
|
+
tokens[index + 2] !== '}' &&
|
|
203
|
+
tokens[index + 2] !== 'word:undefined' &&
|
|
204
|
+
tokens[index + 2] !== 'word:null') {
|
|
205
|
+
serverAdapter = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (end < 0)
|
|
209
|
+
return { output: 'unknown', serverAdapter: false, dynamic: true, patchable: false };
|
|
210
|
+
const configTokens = tokens.slice(start, end + 1);
|
|
211
|
+
const dynamic = configTokens.some((token, index) => token === '...' ||
|
|
212
|
+
token === 'word:await' ||
|
|
213
|
+
token === 'word:async' ||
|
|
214
|
+
(token === 'word:process' &&
|
|
215
|
+
configTokens[index + 1] === '.' &&
|
|
216
|
+
configTokens[index + 2] === 'word:env') ||
|
|
217
|
+
(token === 'word:import' && configTokens[index + 1] === '('));
|
|
218
|
+
if (dynamic || output === undefined)
|
|
219
|
+
return { output: 'unknown', serverAdapter, dynamic, patchable: false };
|
|
220
|
+
const patchable = /^import\s+\{\s*defineConfig\s*\}\s+from\s+(['"])astro\/config\1;\s*export\s+default\s+defineConfig\s*\(\s*\{\s*output\s*:\s*(['"])static\2\s*,?\s*\}\s*\);\s*$/s.test(source);
|
|
221
|
+
return { output: output, serverAdapter, dynamic: false, patchable };
|
|
222
|
+
}
|
|
223
|
+
async function inspectAstroRoutes(directory) {
|
|
224
|
+
let onDemandRoutes = [];
|
|
225
|
+
let staticProtocolFiles = [];
|
|
226
|
+
const pagesRoot = path.join(directory, 'src/pages');
|
|
227
|
+
async function visit(current) {
|
|
228
|
+
for (const entry of await (await import('node:fs/promises'))
|
|
229
|
+
.readdir(current, {
|
|
230
|
+
withFileTypes: true,
|
|
231
|
+
})
|
|
232
|
+
.catch((error) => {
|
|
233
|
+
if (typeof error === 'object' &&
|
|
234
|
+
error !== null &&
|
|
235
|
+
'code' in error &&
|
|
236
|
+
error.code === 'ENOENT')
|
|
237
|
+
return [];
|
|
238
|
+
throw error;
|
|
239
|
+
})) {
|
|
240
|
+
if (entry.isSymbolicLink())
|
|
241
|
+
continue;
|
|
242
|
+
const absolute = path.join(current, entry.name);
|
|
243
|
+
if (entry.isDirectory()) {
|
|
244
|
+
await visit(absolute);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (!/\.(?:astro|[cm]?[jt]s)$/.test(entry.name))
|
|
248
|
+
continue;
|
|
249
|
+
const relative = path.relative(pagesRoot, absolute).split(path.sep).join('/');
|
|
250
|
+
const source = await readFile(absolute, 'utf8');
|
|
251
|
+
const onDemand = /export\s+const\s+prerender\s*=\s*false\b/.test(source);
|
|
252
|
+
if (onDemand)
|
|
253
|
+
onDemandRoutes.push(relative);
|
|
254
|
+
if (/^(?:mcp(?:\.|\/)|agent-native\/v1\/)/.test(relative) &&
|
|
255
|
+
(!onDemand ||
|
|
256
|
+
!/export\s+(?:async\s+)?function\s+(?:POST|ALL)\b|export\s+const\s+(?:POST|ALL)\b/.test(source))) {
|
|
257
|
+
staticProtocolFiles.push(`src/pages/${relative}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
await visit(pagesRoot);
|
|
262
|
+
for (const relative of ['mcp', 'mcp.json']) {
|
|
263
|
+
if (await exists(path.join(directory, 'public', relative)))
|
|
264
|
+
staticProtocolFiles.push(`public/${relative}`);
|
|
265
|
+
}
|
|
266
|
+
const publicProtocolRoot = path.join(directory, 'public/agent-native/v1');
|
|
267
|
+
for (const entry of await (await import('node:fs/promises'))
|
|
268
|
+
.readdir(publicProtocolRoot, {
|
|
269
|
+
withFileTypes: true,
|
|
270
|
+
})
|
|
271
|
+
.catch((error) => {
|
|
272
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT')
|
|
273
|
+
return [];
|
|
274
|
+
throw error;
|
|
275
|
+
})) {
|
|
276
|
+
if (entry.isFile())
|
|
277
|
+
staticProtocolFiles.push(`public/agent-native/v1/${entry.name}`);
|
|
278
|
+
}
|
|
279
|
+
return { onDemandRoutes: onDemandRoutes.sort(), staticProtocolFiles: staticProtocolFiles.sort() };
|
|
280
|
+
}
|
|
281
|
+
async function astroEvidence(directory) {
|
|
282
|
+
const pkg = await packageInfo(directory);
|
|
283
|
+
const deps = [
|
|
284
|
+
pkg?.['dependencies'],
|
|
285
|
+
pkg?.['devDependencies'],
|
|
286
|
+
pkg?.['peerDependencies'],
|
|
287
|
+
].filter((value) => typeof value === 'object' && value !== null);
|
|
288
|
+
const packageHasAstro = deps.some((group) => Object.hasOwn(group, 'astro'));
|
|
289
|
+
let configPath = null;
|
|
290
|
+
for (const candidate of [
|
|
291
|
+
'astro.config.mjs',
|
|
292
|
+
'astro.config.js',
|
|
293
|
+
'astro.config.ts',
|
|
294
|
+
'astro.config.mts',
|
|
295
|
+
]) {
|
|
296
|
+
if (await exists(path.join(directory, candidate))) {
|
|
297
|
+
configPath = candidate;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!packageHasAstro && configPath === null)
|
|
302
|
+
return {
|
|
303
|
+
found: false,
|
|
304
|
+
rendering: 'unknown',
|
|
305
|
+
serverAdapter: false,
|
|
306
|
+
onDemandRoutes: [],
|
|
307
|
+
staticProtocolFiles: [],
|
|
308
|
+
dynamic: false,
|
|
309
|
+
routeConflict: false,
|
|
310
|
+
patchable: false,
|
|
311
|
+
configPath: null,
|
|
312
|
+
configSha256: null,
|
|
313
|
+
evidence: [],
|
|
314
|
+
};
|
|
315
|
+
let evidence = [];
|
|
316
|
+
if (packageHasAstro)
|
|
317
|
+
evidence.push('package.json: Astro dependency');
|
|
318
|
+
if (configPath === null)
|
|
319
|
+
return {
|
|
320
|
+
found: true,
|
|
321
|
+
rendering: 'unknown',
|
|
322
|
+
serverAdapter: false,
|
|
323
|
+
onDemandRoutes: [],
|
|
324
|
+
staticProtocolFiles: [],
|
|
325
|
+
dynamic: true,
|
|
326
|
+
routeConflict: await hasMcpRoute(directory),
|
|
327
|
+
patchable: false,
|
|
328
|
+
configPath: null,
|
|
329
|
+
configSha256: null,
|
|
330
|
+
evidence,
|
|
331
|
+
};
|
|
332
|
+
evidence.push(configPath);
|
|
333
|
+
const configFile = await assertNoSymlink(await realpath(directory), configPath);
|
|
334
|
+
const text = await readFile(configFile, 'utf8');
|
|
335
|
+
const { output, serverAdapter, dynamic, patchable } = staticAstroOutput(text);
|
|
336
|
+
const routes = await inspectAstroRoutes(directory);
|
|
337
|
+
const rendering = output === 'static' && routes.onDemandRoutes.length > 0 ? 'on-demand' : output;
|
|
338
|
+
if (rendering !== 'unknown')
|
|
339
|
+
evidence.push(`literal output: ${rendering}`);
|
|
340
|
+
if (serverAdapter)
|
|
341
|
+
evidence.push('Astro server adapter configured');
|
|
342
|
+
evidence.push(...routes.onDemandRoutes.map((route) => `on-demand route: src/pages/${route}`));
|
|
343
|
+
evidence.push(...routes.staticProtocolFiles.map((file) => `static protocol file: ${file}`));
|
|
344
|
+
return {
|
|
345
|
+
found: true,
|
|
346
|
+
rendering,
|
|
347
|
+
serverAdapter,
|
|
348
|
+
onDemandRoutes: routes.onDemandRoutes,
|
|
349
|
+
staticProtocolFiles: routes.staticProtocolFiles,
|
|
350
|
+
dynamic,
|
|
351
|
+
routeConflict: await hasMcpRoute(directory),
|
|
352
|
+
patchable,
|
|
353
|
+
configPath,
|
|
354
|
+
configSha256: hash(text),
|
|
355
|
+
evidence,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
async function hasMcpRoute(directory) {
|
|
359
|
+
for (const base of ['src/pages', 'src/app', 'pages', 'app']) {
|
|
360
|
+
const root = path.join(directory, base);
|
|
361
|
+
for (const name of [
|
|
362
|
+
'mcp.astro',
|
|
363
|
+
'mcp.js',
|
|
364
|
+
'mcp.ts',
|
|
365
|
+
'mcp.mjs',
|
|
366
|
+
'mcp/index.astro',
|
|
367
|
+
'mcp/index.js',
|
|
368
|
+
'mcp/index.ts',
|
|
369
|
+
'mcp/route.ts',
|
|
370
|
+
'mcp/route.js',
|
|
371
|
+
'mcp/route.mjs',
|
|
372
|
+
'mcp/route.jsx',
|
|
373
|
+
'mcp/route.tsx',
|
|
374
|
+
]) {
|
|
375
|
+
if (await exists(path.join(root, name)))
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
async function nextEvidence(directory) {
|
|
382
|
+
const pkg = await packageInfo(directory);
|
|
383
|
+
const deps = [
|
|
384
|
+
pkg?.['dependencies'],
|
|
385
|
+
pkg?.['devDependencies'],
|
|
386
|
+
pkg?.['peerDependencies'],
|
|
387
|
+
].filter((value) => typeof value === 'object' && value !== null);
|
|
388
|
+
const packageHasNext = deps.some((group) => Object.hasOwn(group, 'next'));
|
|
389
|
+
let markers = [];
|
|
390
|
+
for (const candidate of [
|
|
391
|
+
'next.config.js',
|
|
392
|
+
'next.config.mjs',
|
|
393
|
+
'next.config.ts',
|
|
394
|
+
'app',
|
|
395
|
+
'src/app',
|
|
396
|
+
'pages',
|
|
397
|
+
'src/pages',
|
|
398
|
+
]) {
|
|
399
|
+
if (await exists(path.join(directory, candidate)))
|
|
400
|
+
markers.push(candidate);
|
|
401
|
+
}
|
|
402
|
+
const found = packageHasNext || markers.some((marker) => marker.startsWith('next.config.'));
|
|
403
|
+
if (!found)
|
|
404
|
+
return { found: false, rendering: 'unknown', routeConflict: false, evidence: [] };
|
|
405
|
+
const routeConflict = await hasMcpRoute(directory);
|
|
406
|
+
return {
|
|
407
|
+
found: true,
|
|
408
|
+
rendering: 'server',
|
|
409
|
+
routeConflict,
|
|
410
|
+
evidence: [
|
|
411
|
+
...(packageHasNext ? ['package.json: Next dependency'] : []),
|
|
412
|
+
...markers.map((marker) => `Next marker: ${marker}`),
|
|
413
|
+
...(routeConflict ? ['existing route: /mcp'] : []),
|
|
414
|
+
],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
async function hostingEvidence(directory) {
|
|
418
|
+
let found = [];
|
|
419
|
+
for (const [marker, host] of [
|
|
420
|
+
['wrangler.toml', 'cloudflare'],
|
|
421
|
+
['wrangler.json', 'cloudflare'],
|
|
422
|
+
['wrangler.jsonc', 'cloudflare'],
|
|
423
|
+
['vercel.json', 'vercel'],
|
|
424
|
+
['netlify.toml', 'netlify'],
|
|
425
|
+
]) {
|
|
426
|
+
if (await exists(path.join(directory, marker)))
|
|
427
|
+
found.push({ marker, host });
|
|
428
|
+
}
|
|
429
|
+
let unsupported = [];
|
|
430
|
+
for (const marker of ['firebase.json', 'amplify.yml', 'fly.toml', 'render.yaml']) {
|
|
431
|
+
if (await exists(path.join(directory, marker)))
|
|
432
|
+
unsupported.push(marker);
|
|
433
|
+
}
|
|
434
|
+
const distinct = [
|
|
435
|
+
...new Set(found.map((item) => item.host)),
|
|
436
|
+
];
|
|
437
|
+
return {
|
|
438
|
+
hosting: distinct.length === 1 ? requireAt(distinct, 0) : 'unknown',
|
|
439
|
+
evidence: [...found.map((item) => item.marker), ...unsupported],
|
|
440
|
+
unsupported,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/** Inspect project files as data only. No configuration or application module is imported. */
|
|
444
|
+
export async function detectExistingProject(projectRoot, overrides = {}) {
|
|
445
|
+
const root = await realpath(projectRoot);
|
|
446
|
+
const rootPackage = await packageInfo(root);
|
|
447
|
+
let candidates = [];
|
|
448
|
+
const rootAstro = await astroEvidence(root);
|
|
449
|
+
const rootNext = await nextEvidence(root);
|
|
450
|
+
if (rootAstro.found || rootNext.found)
|
|
451
|
+
candidates.push(root);
|
|
452
|
+
const workspaceValue = rootPackage?.['workspaces'];
|
|
453
|
+
const workspacePaths = Array.isArray(workspaceValue)
|
|
454
|
+
? workspaceValue.filter((item) => typeof item === 'string')
|
|
455
|
+
: typeof workspaceValue === 'object' &&
|
|
456
|
+
workspaceValue !== null &&
|
|
457
|
+
'packages' in workspaceValue &&
|
|
458
|
+
Array.isArray(workspaceValue['packages'])
|
|
459
|
+
? workspaceValue['packages'].filter((item) => typeof item === 'string')
|
|
460
|
+
: [];
|
|
461
|
+
const hasWorkspaceLayout = workspacePaths.length > 0 || (await exists(path.join(root, 'pnpm-workspace.yaml')));
|
|
462
|
+
if (hasWorkspaceLayout) {
|
|
463
|
+
for (const group of ['apps', 'packages', 'sites']) {
|
|
464
|
+
for (const child of await listDirectories(path.join(root, group))) {
|
|
465
|
+
if (((await astroEvidence(child)).found || (await nextEvidence(child)).found) &&
|
|
466
|
+
!candidates.includes(child))
|
|
467
|
+
candidates.push(child);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const overrideRoot = overrides.appRoot === undefined ? null : path.resolve(root, overrides.appRoot);
|
|
472
|
+
if (overrideRoot !== null && overrideRoot !== root && !overrideRoot.startsWith(root + path.sep)) {
|
|
473
|
+
throw new Error('appRoot override must remain inside project root');
|
|
474
|
+
}
|
|
475
|
+
const overrideRealRoot = overrideRoot === null ? null : await realpath(overrideRoot);
|
|
476
|
+
if (overrideRealRoot !== null &&
|
|
477
|
+
overrideRealRoot !== root &&
|
|
478
|
+
!overrideRealRoot.startsWith(root + path.sep)) {
|
|
479
|
+
throw new Error('appRoot override resolves through a symlink outside project root');
|
|
480
|
+
}
|
|
481
|
+
const applicationRoot = overrideRealRoot ?? (candidates.length === 1 ? requireAt(candidates, 0) : null);
|
|
482
|
+
const appRelative = applicationRoot === null
|
|
483
|
+
? null
|
|
484
|
+
: path.relative(root, applicationRoot).split(path.sep).join('/');
|
|
485
|
+
const appAstro = applicationRoot === null ? null : await astroEvidence(applicationRoot);
|
|
486
|
+
const appNext = applicationRoot === null ? null : await nextEvidence(applicationRoot);
|
|
487
|
+
const detectedFramework = appAstro?.found && !appNext?.found
|
|
488
|
+
? 'astro'
|
|
489
|
+
: appNext?.found && !appAstro?.found
|
|
490
|
+
? 'next'
|
|
491
|
+
: 'unknown';
|
|
492
|
+
const framework = overrides.framework ?? detectedFramework;
|
|
493
|
+
const appEvidence = framework === 'astro' && appAstro !== null
|
|
494
|
+
? appAstro
|
|
495
|
+
: framework === 'next' && appNext !== null
|
|
496
|
+
? {
|
|
497
|
+
...appNext,
|
|
498
|
+
serverAdapter: false,
|
|
499
|
+
onDemandRoutes: [],
|
|
500
|
+
staticProtocolFiles: [],
|
|
501
|
+
dynamic: false,
|
|
502
|
+
patchable: false,
|
|
503
|
+
configPath: null,
|
|
504
|
+
configSha256: null,
|
|
505
|
+
}
|
|
506
|
+
: {
|
|
507
|
+
found: false,
|
|
508
|
+
rendering: 'unknown',
|
|
509
|
+
serverAdapter: false,
|
|
510
|
+
onDemandRoutes: [],
|
|
511
|
+
staticProtocolFiles: [],
|
|
512
|
+
dynamic: false,
|
|
513
|
+
routeConflict: false,
|
|
514
|
+
patchable: false,
|
|
515
|
+
configPath: null,
|
|
516
|
+
configSha256: null,
|
|
517
|
+
evidence: [],
|
|
518
|
+
};
|
|
519
|
+
let lockEvidence = [];
|
|
520
|
+
for (const name of Object.keys(LOCKFILES))
|
|
521
|
+
if (await exists(path.join(root, name)))
|
|
522
|
+
lockEvidence.push(name);
|
|
523
|
+
const lockManagers = [
|
|
524
|
+
...new Set(lockEvidence.map((file) => requireValue(LOCKFILES, file))),
|
|
525
|
+
];
|
|
526
|
+
let packageManager = lockManagers.length === 1 ? requireAt(lockManagers, 0) : 'unknown';
|
|
527
|
+
if (overrides.packageManager)
|
|
528
|
+
packageManager = overrides.packageManager;
|
|
529
|
+
const detectedHost = await hostingEvidence(root);
|
|
530
|
+
let unresolved = [];
|
|
531
|
+
if (applicationRoot === null)
|
|
532
|
+
unresolved.push(candidates.length > 1 ? 'application-root-ambiguous' : 'application-root-not-detected');
|
|
533
|
+
if (lockManagers.length > 1 && !overrides.packageManager)
|
|
534
|
+
unresolved.push('competing-lockfiles');
|
|
535
|
+
if (packageManager === 'unknown')
|
|
536
|
+
unresolved.push('package-manager-unknown');
|
|
537
|
+
if (framework === 'astro') {
|
|
538
|
+
if (appEvidence.dynamic)
|
|
539
|
+
unresolved.push('dynamic-config-unsupported');
|
|
540
|
+
else if ((appEvidence.rendering === 'static' || appEvidence.rendering === 'on-demand') &&
|
|
541
|
+
!appEvidence.patchable)
|
|
542
|
+
unresolved.push('astro-config-manual-integration');
|
|
543
|
+
if (appEvidence.rendering === 'server' || appEvidence.rendering === 'unknown')
|
|
544
|
+
unresolved.push(`rendering-${appEvidence.rendering}`);
|
|
545
|
+
if ((appEvidence.rendering === 'on-demand' || appEvidence.rendering === 'server') &&
|
|
546
|
+
!appEvidence.serverAdapter)
|
|
547
|
+
unresolved.push('server-adapter-missing');
|
|
548
|
+
if (appEvidence.staticProtocolFiles.length > 0)
|
|
549
|
+
unresolved.push('static-protocol-endpoint');
|
|
550
|
+
}
|
|
551
|
+
else if (framework === 'next') {
|
|
552
|
+
unresolved.push('next-manual-integration');
|
|
553
|
+
}
|
|
554
|
+
if (framework === 'unknown')
|
|
555
|
+
unresolved.push('framework-unknown');
|
|
556
|
+
const host = overrides.hosting ?? detectedHost.hosting;
|
|
557
|
+
if (host === 'unknown')
|
|
558
|
+
unresolved.push(detectedHost.unsupported.length > 0 ? 'unsupported-host-config' : 'hosting-unknown');
|
|
559
|
+
const evidence = [
|
|
560
|
+
...(applicationRoot === null
|
|
561
|
+
? []
|
|
562
|
+
: [`application root: ${appRelative || '.'}`, ...appEvidence.evidence]),
|
|
563
|
+
...lockEvidence.map((name) => `lockfile: ${name}`),
|
|
564
|
+
...detectedHost.evidence.map((name) => `hosting config: ${name}`),
|
|
565
|
+
];
|
|
566
|
+
return {
|
|
567
|
+
projectRoot: root,
|
|
568
|
+
applicationRoot: appRelative,
|
|
569
|
+
astroConfigPath: appEvidence.configPath,
|
|
570
|
+
astroConfigSha256: appEvidence.configSha256,
|
|
571
|
+
framework,
|
|
572
|
+
rendering: appEvidence.rendering,
|
|
573
|
+
serverAdapter: appEvidence.serverAdapter,
|
|
574
|
+
onDemandRoutes: appEvidence.onDemandRoutes,
|
|
575
|
+
staticProtocolFiles: appEvidence.staticProtocolFiles,
|
|
576
|
+
packageManager,
|
|
577
|
+
hosting: host,
|
|
578
|
+
hostingConfig: detectedHost.evidence.length === 1 ? requireAt(detectedHost.evidence, 0) : null,
|
|
579
|
+
routeConflict: appEvidence.routeConflict,
|
|
580
|
+
evidence,
|
|
581
|
+
unresolved: [...new Set(unresolved)],
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function validateOwnedManifest(manifest) {
|
|
585
|
+
if (manifest.schemaVersion !== INIT_OWNERSHIP_VERSION ||
|
|
586
|
+
!/^[a-f0-9]{64}$/.test(manifest.planDigest) ||
|
|
587
|
+
(manifest.status !== 'applying' && manifest.status !== 'applied'))
|
|
588
|
+
throw new Error('invalid init ownership manifest header');
|
|
589
|
+
const appRoot = manifest.applicationRoot;
|
|
590
|
+
if (appRoot !== null &&
|
|
591
|
+
appRoot !== '' &&
|
|
592
|
+
(path.posix.isAbsolute(appRoot) ||
|
|
593
|
+
appRoot
|
|
594
|
+
.split('/')
|
|
595
|
+
.some((part) => part === '..' || part === '.' || part === ''))) {
|
|
596
|
+
throw new Error('invalid application root in ownership manifest');
|
|
597
|
+
}
|
|
598
|
+
if (manifest.astroConfigPath !== null &&
|
|
599
|
+
!['astro.config.mjs', 'astro.config.js', 'astro.config.ts', 'astro.config.mts'].includes(manifest.astroConfigPath)) {
|
|
600
|
+
throw new Error('invalid Astro config path in ownership manifest');
|
|
601
|
+
}
|
|
602
|
+
const prefix = appRoot ? `${appRoot}/` : '';
|
|
603
|
+
let allowed = new Set([`${prefix}${BOOTSTRAP_PATH}`, `${prefix}${SETTINGS_PATH}`]);
|
|
604
|
+
if (manifest.astroConfigPath !== null)
|
|
605
|
+
allowed.add(`${prefix}${manifest.astroConfigPath}`);
|
|
606
|
+
const keys = Object.keys(manifest.files);
|
|
607
|
+
if (keys.length !== allowed.size || keys.some((key) => !allowed.has(key))) {
|
|
608
|
+
throw new Error('ownership manifest contains unexpected paths');
|
|
609
|
+
}
|
|
610
|
+
for (const record of Object.values(manifest.files)) {
|
|
611
|
+
if (!/^[a-f0-9]{64}$/.test(record.afterSha256) ||
|
|
612
|
+
(record.beforeSha256 !== null && !/^[a-f0-9]{64}$/.test(record.beforeSha256))) {
|
|
613
|
+
throw new Error('invalid file digest in ownership manifest');
|
|
614
|
+
}
|
|
615
|
+
if ((record.beforeSha256 !== null) !== (record.beforeContent !== undefined)) {
|
|
616
|
+
throw new Error('ownership manifest must retain every overwritten preimage');
|
|
617
|
+
}
|
|
618
|
+
if (record.beforeContent !== undefined && hash(record.beforeContent) !== record.beforeSha256) {
|
|
619
|
+
throw new Error('ownership manifest preimage does not match its digest');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (manifest.detection.applicationRoot !== manifest.applicationRoot ||
|
|
623
|
+
manifest.detection.astroConfigPath !== manifest.astroConfigPath)
|
|
624
|
+
throw new Error('ownership manifest detection does not match its path boundary');
|
|
625
|
+
const configRelative = manifest.astroConfigPath === null
|
|
626
|
+
? null
|
|
627
|
+
: path.posix.join(manifest.applicationRoot ?? '', manifest.astroConfigPath);
|
|
628
|
+
const configRecord = configRelative === null ? null : manifest.files[configRelative];
|
|
629
|
+
let patchedConfig = null;
|
|
630
|
+
if (configRecord !== null) {
|
|
631
|
+
if (configRecord === undefined ||
|
|
632
|
+
configRecord.beforeContent === undefined ||
|
|
633
|
+
hash(configRecord.beforeContent) !== manifest.detection.astroConfigSha256) {
|
|
634
|
+
throw new Error('ownership manifest lacks the original Astro config preimage');
|
|
635
|
+
}
|
|
636
|
+
patchedConfig = patchAstroConfig(configRecord.beforeContent);
|
|
637
|
+
}
|
|
638
|
+
const expected = makeScaffold(manifest.selectedChoices, manifest.detection, patchedConfig);
|
|
639
|
+
if (Object.keys(expected).length !== Object.keys(manifest.files).length) {
|
|
640
|
+
throw new Error('ownership manifest file set does not match generated output');
|
|
641
|
+
}
|
|
642
|
+
for (const [relativePath, content] of Object.entries(expected)) {
|
|
643
|
+
if (manifest.files[relativePath]?.afterSha256 !== hash(content)) {
|
|
644
|
+
throw new Error(`ownership manifest output digest does not match generated file: ${relativePath}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
export async function createInitPlan(projectRoot, overrides = {}, selectedChoices = null) {
|
|
649
|
+
const detection = await detectExistingProject(projectRoot, overrides);
|
|
650
|
+
const appPrefix = detection.applicationRoot ? `${detection.applicationRoot}/` : '';
|
|
651
|
+
const routeMode = selectedChoices?.routeMode ?? 'sidecar';
|
|
652
|
+
const conflicts = detection.routeConflict && routeMode === 'same-origin'
|
|
653
|
+
? ['existing /mcp route conflicts with the planned same-origin MCP endpoint']
|
|
654
|
+
: [];
|
|
655
|
+
const ownershipText = await readOptional(detection.projectRoot, MANIFEST_PATH);
|
|
656
|
+
let alreadyInitialized = false;
|
|
657
|
+
if (ownershipText !== null) {
|
|
658
|
+
const ownership = JSON.parse(ownershipText);
|
|
659
|
+
validateOwnedManifest(ownership);
|
|
660
|
+
alreadyInitialized =
|
|
661
|
+
ownership.schemaVersion === INIT_OWNERSHIP_VERSION && ownership.status === 'applied';
|
|
662
|
+
for (const [relativePath, record] of Object.entries(ownership.files)) {
|
|
663
|
+
const current = await readOptional(detection.projectRoot, relativePath);
|
|
664
|
+
if (current === null || hash(current) !== record.afterSha256)
|
|
665
|
+
alreadyInitialized = false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const proposedFiles = alreadyInitialized || detection.framework === 'next' || conflicts.length > 0
|
|
669
|
+
? []
|
|
670
|
+
: [
|
|
671
|
+
`${appPrefix}${BOOTSTRAP_PATH}`,
|
|
672
|
+
`${appPrefix}${SETTINGS_PATH}`,
|
|
673
|
+
...(detection.astroConfigPath ? [`${appPrefix}${detection.astroConfigPath}`] : []),
|
|
674
|
+
MANIFEST_PATH,
|
|
675
|
+
];
|
|
676
|
+
const payload = {
|
|
677
|
+
schemaVersion: INIT_PLAN_VERSION,
|
|
678
|
+
detection,
|
|
679
|
+
proposedFiles,
|
|
680
|
+
dependencyChanges: alreadyInitialized
|
|
681
|
+
? []
|
|
682
|
+
: [
|
|
683
|
+
{
|
|
684
|
+
name: '@uppercut-labs/agent-native',
|
|
685
|
+
action: 'add-after-release',
|
|
686
|
+
reason: 'The package is currently private and has no installable public release.',
|
|
687
|
+
},
|
|
688
|
+
],
|
|
689
|
+
manualIntegration: conflicts.length > 0
|
|
690
|
+
? 'Resolve or choose a different MCP route before applying; no files are proposed.'
|
|
691
|
+
: detection.framework === 'next'
|
|
692
|
+
? 'Next integration is planned without rewriting existing auth, routes, layout, or config. Add reviewed route modules that delegate to @uppercut-labs/agent-native/next and an optional client bootstrap from @uppercut-labs/agent-native/next/browser.'
|
|
693
|
+
: alreadyInitialized ||
|
|
694
|
+
detection.framework !== 'astro' ||
|
|
695
|
+
detection.rendering !== 'static' ||
|
|
696
|
+
detection.unresolved.includes('astro-config-manual-integration')
|
|
697
|
+
? 'Review Astro config manually; this init does not rewrite unsupported config shapes.'
|
|
698
|
+
: 'Astro config will be patched only from the recognized literal static template; review the generated diff before approval.',
|
|
699
|
+
conflicts,
|
|
700
|
+
alreadyInitialized,
|
|
701
|
+
selectedChoices,
|
|
702
|
+
detectionOverrides: overrides,
|
|
703
|
+
};
|
|
704
|
+
return { ...payload, planDigest: hash(stableJson(payload)) };
|
|
705
|
+
}
|
|
706
|
+
function patchAstroConfig(source) {
|
|
707
|
+
const supported = /^import\s+\{\s*defineConfig\s*\}\s+from\s+(['"])astro\/config\1;\s*export\s+default\s+defineConfig\s*\(\s*\{\s*output\s*:\s*(['"])static\2\s*,?\s*\}\s*\);\s*$/s;
|
|
708
|
+
if (!supported.test(source))
|
|
709
|
+
throw new Error('Astro config is not the supported literal static shape; review the integration plan manually');
|
|
710
|
+
if (source.includes('@uppercut-labs/agent-native/astro'))
|
|
711
|
+
throw new Error('Astro config already references Agent Native; inspect existing integration before init');
|
|
712
|
+
return `import { defineConfig } from 'astro/config';
|
|
713
|
+
import { fileURLToPath } from 'node:url';
|
|
714
|
+
import { agentNativeAstro } from '@uppercut-labs/agent-native/astro';
|
|
715
|
+
|
|
716
|
+
export default defineConfig({
|
|
717
|
+
output: 'static',
|
|
718
|
+
integrations: [
|
|
719
|
+
agentNativeAstro({
|
|
720
|
+
browserEntry: fileURLToPath(new URL('./.agent-native/browser-entry.mjs', import.meta.url)),
|
|
721
|
+
}),
|
|
722
|
+
],
|
|
723
|
+
});
|
|
724
|
+
`;
|
|
725
|
+
}
|
|
726
|
+
function makeScaffold(choices, detection, astroConfig) {
|
|
727
|
+
const appPrefix = detection.applicationRoot ? `${detection.applicationRoot}/` : '';
|
|
728
|
+
const bootstrapPath = `${appPrefix}${BOOTSTRAP_PATH}`;
|
|
729
|
+
const settingsPath = `${appPrefix}${SETTINGS_PATH}`;
|
|
730
|
+
const settings = {
|
|
731
|
+
schemaVersion: 'uan.init-settings/v1',
|
|
732
|
+
framework: detection.framework,
|
|
733
|
+
rendering: detection.rendering,
|
|
734
|
+
applicationRoot: detection.applicationRoot,
|
|
735
|
+
packageManager: detection.packageManager,
|
|
736
|
+
hosting: choices.hosting,
|
|
737
|
+
sidecarOrigin: choices.sidecarOrigin ?? null,
|
|
738
|
+
routeMode: choices.routeMode ?? 'sidecar',
|
|
739
|
+
browserEntry: bootstrapPath,
|
|
740
|
+
note: 'No application exports or handlers were discovered or registered. Add only explicitly reviewed definitions and bindings in application-owned code.',
|
|
741
|
+
};
|
|
742
|
+
return {
|
|
743
|
+
[bootstrapPath]: `// Tool-owned browser bootstrap. No project exports are discovered or registered.\nconst sidecarOrigin = ${JSON.stringify(choices.sidecarOrigin ?? null)};\ndocument.documentElement.dataset.agentNative = 'ready';\ndocument.dispatchEvent(new CustomEvent('agent-native:ready', { detail: { sidecarOrigin } }));\n`,
|
|
744
|
+
[settingsPath]: `${JSON.stringify(settings, null, 2)}\n`,
|
|
745
|
+
...(astroConfig === null ||
|
|
746
|
+
detection.applicationRoot === null ||
|
|
747
|
+
detection.astroConfigPath === null
|
|
748
|
+
? {}
|
|
749
|
+
: { [`${appPrefix}${detection.astroConfigPath}`]: astroConfig }),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
async function writeAtomic(destination, content) {
|
|
753
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
754
|
+
const temporary = `${destination}.tmp-${randomUUID()}`;
|
|
755
|
+
try {
|
|
756
|
+
await writeFile(temporary, content, { flag: 'wx' });
|
|
757
|
+
await rename(temporary, destination);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
await rm(temporary, { force: true });
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async function writeNewNoReplace(destination, content) {
|
|
765
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
766
|
+
const temporary = `${destination}.tmp-${randomUUID()}`;
|
|
767
|
+
try {
|
|
768
|
+
await writeFile(temporary, content, { flag: 'wx' });
|
|
769
|
+
await link(temporary, destination);
|
|
770
|
+
}
|
|
771
|
+
finally {
|
|
772
|
+
await rm(temporary, { force: true });
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async function replaceIfUnchanged(destination, content, expectedSha256) {
|
|
776
|
+
const temporary = `${destination}.tmp-${randomUUID()}`;
|
|
777
|
+
try {
|
|
778
|
+
await writeFile(temporary, content, { flag: 'wx' });
|
|
779
|
+
const current = await readFile(destination);
|
|
780
|
+
if (hash(current) !== expectedSha256)
|
|
781
|
+
throw new Error(`target changed before guarded replacement: ${destination}`);
|
|
782
|
+
await rename(temporary, destination);
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
await rm(temporary, { force: true });
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async function applyScaffoldFiles(root, scaffold, records, beforeTargetWrite, failAfterWrites) {
|
|
790
|
+
let writes = 0;
|
|
791
|
+
for (const [relativePath, content] of Object.entries(scaffold)) {
|
|
792
|
+
await beforeTargetWrite?.(relativePath);
|
|
793
|
+
const target = await assertNoSymlink(root, relativePath);
|
|
794
|
+
const record = records[relativePath];
|
|
795
|
+
if (record === undefined)
|
|
796
|
+
throw new Error(`ownership manifest is missing record: ${relativePath}`);
|
|
797
|
+
const current = await readOptional(root, relativePath);
|
|
798
|
+
if (current !== null && hash(current) === record.afterSha256)
|
|
799
|
+
continue;
|
|
800
|
+
if (record.beforeSha256 === null) {
|
|
801
|
+
if (current !== null)
|
|
802
|
+
throw new Error(`refusing to replace a file created after plan: ${relativePath}`);
|
|
803
|
+
await writeNewNoReplace(target, content);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
if (current === null || hash(current) !== record.beforeSha256) {
|
|
807
|
+
throw new Error(`refusing to replace a file changed after plan: ${relativePath}`);
|
|
808
|
+
}
|
|
809
|
+
await replaceIfUnchanged(target, content, record.beforeSha256);
|
|
810
|
+
}
|
|
811
|
+
writes += 1;
|
|
812
|
+
if (failAfterWrites === writes)
|
|
813
|
+
throw new Error('simulated interrupted init');
|
|
814
|
+
}
|
|
815
|
+
return writes;
|
|
816
|
+
}
|
|
817
|
+
export async function applyInitPlan(projectRoot, plan, choices, options) {
|
|
818
|
+
if (!options.approved)
|
|
819
|
+
throw new Error('plan must be explicitly approved');
|
|
820
|
+
if (plan.selectedChoices === null || stableJson(plan.selectedChoices) !== stableJson(choices))
|
|
821
|
+
throw new Error('apply choices must exactly match the reviewed plan');
|
|
822
|
+
const root = await realpath(projectRoot);
|
|
823
|
+
if (root !== plan.detection.projectRoot)
|
|
824
|
+
throw new Error('plan project root does not match apply root');
|
|
825
|
+
let planPayload = { ...plan };
|
|
826
|
+
delete planPayload['planDigest'];
|
|
827
|
+
if (hash(stableJson(planPayload)) !== plan.planDigest)
|
|
828
|
+
throw new Error('init plan digest is invalid');
|
|
829
|
+
const manifestTarget = await assertNoSymlink(root, MANIFEST_PATH);
|
|
830
|
+
const existingManifestText = await readOptional(root, MANIFEST_PATH);
|
|
831
|
+
let existingManifest = null;
|
|
832
|
+
if (!plan.alreadyInitialized && existingManifestText !== null) {
|
|
833
|
+
existingManifest = JSON.parse(existingManifestText);
|
|
834
|
+
validateOwnedManifest(existingManifest);
|
|
835
|
+
if (existingManifest.planDigest !== plan.planDigest ||
|
|
836
|
+
existingManifest.applicationRoot !== plan.detection.applicationRoot ||
|
|
837
|
+
existingManifest.astroConfigPath !== plan.detection.astroConfigPath ||
|
|
838
|
+
stableJson(existingManifest.detection) !== stableJson(plan.detection) ||
|
|
839
|
+
stableJson(existingManifest.selectedChoices) !== stableJson(choices))
|
|
840
|
+
throw new Error('existing init ownership manifest does not match this plan');
|
|
841
|
+
for (const [relativePath, record] of Object.entries(existingManifest.files)) {
|
|
842
|
+
const current = await readOptional(root, relativePath);
|
|
843
|
+
if (current === null) {
|
|
844
|
+
if (record.beforeSha256 !== null || existingManifest.status !== 'applying')
|
|
845
|
+
throw new Error('owned file disappeared after interruption: ' + relativePath);
|
|
846
|
+
}
|
|
847
|
+
else if (hash(current) !== record.afterSha256 &&
|
|
848
|
+
(record.beforeSha256 === null || hash(current) !== record.beforeSha256)) {
|
|
849
|
+
throw new Error('owned file changed after interruption: ' + relativePath);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
const currentDetection = await detectExistingProject(root, plan.detectionOverrides);
|
|
854
|
+
let resumableDetection = currentDetection;
|
|
855
|
+
if (existingManifest?.status === 'applying' && existingManifest.astroConfigPath !== null) {
|
|
856
|
+
const configPath = path.posix.join(existingManifest.applicationRoot ?? '', existingManifest.astroConfigPath);
|
|
857
|
+
const configRecord = existingManifest.files[configPath];
|
|
858
|
+
const currentConfig = await readOptional(root, configPath);
|
|
859
|
+
if (configRecord !== undefined &&
|
|
860
|
+
currentConfig !== null &&
|
|
861
|
+
hash(currentConfig) === configRecord.afterSha256) {
|
|
862
|
+
resumableDetection = {
|
|
863
|
+
...currentDetection,
|
|
864
|
+
astroConfigSha256: plan.detection.astroConfigSha256,
|
|
865
|
+
unresolved: currentDetection.unresolved.filter((item) => item !== 'astro-config-manual-integration' || plan.detection.unresolved.includes(item)),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (stableJson(resumableDetection) !== stableJson(plan.detection))
|
|
870
|
+
throw new Error('project evidence changed after plan creation; create a fresh plan');
|
|
871
|
+
if (plan.alreadyInitialized) {
|
|
872
|
+
const ownedText = await readOptional(root, MANIFEST_PATH);
|
|
873
|
+
if (ownedText === null)
|
|
874
|
+
throw new Error('ownership manifest disappeared after no-op plan');
|
|
875
|
+
const owned = JSON.parse(ownedText);
|
|
876
|
+
validateOwnedManifest(owned);
|
|
877
|
+
for (const [relativePath, record] of Object.entries(owned.files)) {
|
|
878
|
+
const current = await readOptional(root, relativePath);
|
|
879
|
+
if (current === null || hash(current) !== record.afterSha256)
|
|
880
|
+
throw new Error(`owned file changed after plan: ${relativePath}`);
|
|
881
|
+
}
|
|
882
|
+
return { status: 'no-op', ownedFiles: Object.keys(owned.files), planDigest: plan.planDigest };
|
|
883
|
+
}
|
|
884
|
+
const unresolved = plan.detection.unresolved.filter((item) => item !== 'hosting-unknown');
|
|
885
|
+
if (unresolved.length > 0)
|
|
886
|
+
throw new Error(`plan has unresolved detection: ${unresolved.join(', ')}`);
|
|
887
|
+
if (choices.hosting !== 'browser-only' && choices.sidecarOrigin === undefined)
|
|
888
|
+
throw new Error('a selected sidecar host requires an explicit sidecarOrigin');
|
|
889
|
+
if (choices.routeMode === 'same-origin' && plan.detection.rendering === 'static')
|
|
890
|
+
throw new Error('same-origin mode requires reviewed on-demand routes and a server adapter; choose sidecar for a static site');
|
|
891
|
+
if (choices.sidecarOrigin !== undefined) {
|
|
892
|
+
const parsed = new URL(choices.sidecarOrigin);
|
|
893
|
+
if (parsed.protocol !== 'https:' &&
|
|
894
|
+
parsed.hostname !== 'localhost' &&
|
|
895
|
+
parsed.hostname !== '127.0.0.1')
|
|
896
|
+
throw new Error('sidecarOrigin must use HTTPS except on localhost');
|
|
897
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash)
|
|
898
|
+
throw new Error('sidecarOrigin must not contain credentials, query, or fragment');
|
|
899
|
+
}
|
|
900
|
+
if (plan.detection.routeConflict && choices.routeMode === 'same-origin')
|
|
901
|
+
throw new Error('an existing /mcp route conflicts with same-origin mode');
|
|
902
|
+
const configRelative = plan.detection.astroConfigPath !== null && plan.detection.applicationRoot !== null
|
|
903
|
+
? path.posix.join(plan.detection.applicationRoot, plan.detection.astroConfigPath)
|
|
904
|
+
: null;
|
|
905
|
+
let originalConfig = null;
|
|
906
|
+
if (configRelative !== null) {
|
|
907
|
+
const currentConfig = await readOptional(root, configRelative);
|
|
908
|
+
const configRecord = existingManifest?.files[configRelative];
|
|
909
|
+
if (existingManifest?.status === 'applying' && configRecord !== undefined) {
|
|
910
|
+
if (currentConfig === null ||
|
|
911
|
+
(hash(currentConfig) !== configRecord.beforeSha256 &&
|
|
912
|
+
hash(currentConfig) !== configRecord.afterSha256))
|
|
913
|
+
throw new Error('Astro config changed after interrupted init');
|
|
914
|
+
originalConfig = configRecord.beforeContent ?? null;
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
if (currentConfig === null || hash(currentConfig) !== plan.detection.astroConfigSha256)
|
|
918
|
+
throw new Error('Astro config changed after plan creation');
|
|
919
|
+
originalConfig = currentConfig;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const patchedConfig = originalConfig === null ? null : patchAstroConfig(originalConfig);
|
|
923
|
+
const scaffold = makeScaffold(choices, plan.detection, patchedConfig);
|
|
924
|
+
if (existingManifest !== null && existingManifestText !== null) {
|
|
925
|
+
const existing = existingManifest;
|
|
926
|
+
for (const [relativePath, record] of Object.entries(existing.files)) {
|
|
927
|
+
const current = await readOptional(root, relativePath);
|
|
928
|
+
const expected = scaffold[relativePath];
|
|
929
|
+
if (expected === undefined || hash(expected) !== record.afterSha256)
|
|
930
|
+
throw new Error(`init choices differ from recorded plan: ${relativePath}`);
|
|
931
|
+
if (current !== null &&
|
|
932
|
+
hash(current) !== record.afterSha256 &&
|
|
933
|
+
hash(current) !== record.beforeSha256)
|
|
934
|
+
throw new Error(`owned file changed after interruption: ${relativePath}`);
|
|
935
|
+
if (current === null && record.beforeSha256 !== null)
|
|
936
|
+
throw new Error(`owned file disappeared after interruption: ${relativePath}`);
|
|
937
|
+
if (current === null && existing.status === 'applied')
|
|
938
|
+
throw new Error(`owned file disappeared after init: ${relativePath}`);
|
|
939
|
+
}
|
|
940
|
+
if (existing.status === 'applied')
|
|
941
|
+
return {
|
|
942
|
+
status: 'no-op',
|
|
943
|
+
ownedFiles: Object.keys(existing.files),
|
|
944
|
+
planDigest: plan.planDigest,
|
|
945
|
+
};
|
|
946
|
+
await applyScaffoldFiles(root, scaffold, existing.files, options.beforeTargetWrite, options.failAfterWrites);
|
|
947
|
+
const complete = { ...existing, status: 'applied' };
|
|
948
|
+
await replaceIfUnchanged(manifestTarget, `${JSON.stringify(complete, null, 2)}\n`, hash(existingManifestText));
|
|
949
|
+
return {
|
|
950
|
+
status: 'applied',
|
|
951
|
+
ownedFiles: Object.keys(existing.files),
|
|
952
|
+
planDigest: plan.planDigest,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
let records = {};
|
|
956
|
+
for (const [relativePath, content] of Object.entries(scaffold)) {
|
|
957
|
+
await assertNoSymlink(root, relativePath);
|
|
958
|
+
const current = await readOptional(root, relativePath);
|
|
959
|
+
const isConfig = relativePath.endsWith(plan.detection.astroConfigPath ?? '\u0000');
|
|
960
|
+
if (current !== null && !isConfig)
|
|
961
|
+
throw new Error(`refusing to overwrite existing file: ${relativePath}`);
|
|
962
|
+
if (isConfig && (current === null || hash(current) !== plan.detection.astroConfigSha256))
|
|
963
|
+
throw new Error(`Astro config changed after plan creation: ${relativePath}`);
|
|
964
|
+
records[relativePath] = {
|
|
965
|
+
beforeSha256: current === null ? null : hash(current),
|
|
966
|
+
afterSha256: hash(content),
|
|
967
|
+
...(current === null ? {} : { beforeContent: current }),
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const manifest = {
|
|
971
|
+
schemaVersion: INIT_OWNERSHIP_VERSION,
|
|
972
|
+
planDigest: plan.planDigest,
|
|
973
|
+
status: 'applying',
|
|
974
|
+
applicationRoot: plan.detection.applicationRoot,
|
|
975
|
+
astroConfigPath: plan.detection.astroConfigPath,
|
|
976
|
+
detection: plan.detection,
|
|
977
|
+
selectedChoices: choices,
|
|
978
|
+
files: records,
|
|
979
|
+
};
|
|
980
|
+
await writeNewNoReplace(manifestTarget, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
981
|
+
await applyScaffoldFiles(root, scaffold, records, options.beforeTargetWrite, options.failAfterWrites);
|
|
982
|
+
const complete = { ...manifest, status: 'applied' };
|
|
983
|
+
await replaceIfUnchanged(manifestTarget, `${JSON.stringify(complete, null, 2)}\n`, hash(`${JSON.stringify(manifest, null, 2)}\n`));
|
|
984
|
+
return { status: 'applied', ownedFiles: Object.keys(records), planDigest: plan.planDigest };
|
|
985
|
+
}
|
|
986
|
+
export async function restoreInit(projectRoot, options = {}) {
|
|
987
|
+
const root = await realpath(projectRoot);
|
|
988
|
+
const manifestText = await readOptional(root, MANIFEST_PATH);
|
|
989
|
+
if (manifestText === null)
|
|
990
|
+
return { status: 'no-op', removed: [], conflicts: [] };
|
|
991
|
+
const manifest = JSON.parse(manifestText);
|
|
992
|
+
validateOwnedManifest(manifest);
|
|
993
|
+
let conflicts = [];
|
|
994
|
+
let removable = [];
|
|
995
|
+
for (const [relativePath, record] of Object.entries(manifest.files)) {
|
|
996
|
+
const current = await readOptional(root, relativePath);
|
|
997
|
+
if (current !== null && hash(current) === record.afterSha256)
|
|
998
|
+
removable.push(relativePath);
|
|
999
|
+
else if (current !== null &&
|
|
1000
|
+
record.beforeSha256 !== null &&
|
|
1001
|
+
hash(current) === record.beforeSha256)
|
|
1002
|
+
continue;
|
|
1003
|
+
else if (current === null && record.beforeSha256 === null)
|
|
1004
|
+
continue;
|
|
1005
|
+
else
|
|
1006
|
+
conflicts.push(relativePath);
|
|
1007
|
+
}
|
|
1008
|
+
if (conflicts.length > 0)
|
|
1009
|
+
return { status: 'conflict', removed: [], conflicts };
|
|
1010
|
+
for (const relativePath of removable) {
|
|
1011
|
+
const record = requireValue(manifest.files, relativePath);
|
|
1012
|
+
await options.beforeTargetRestore?.(relativePath);
|
|
1013
|
+
const current = await readOptional(root, relativePath);
|
|
1014
|
+
if (current === null || hash(current) !== record.afterSha256) {
|
|
1015
|
+
return { status: 'conflict', removed: [], conflicts: [relativePath] };
|
|
1016
|
+
}
|
|
1017
|
+
const target = await assertNoSymlink(root, relativePath);
|
|
1018
|
+
if (record.beforeContent === undefined)
|
|
1019
|
+
await rm(target);
|
|
1020
|
+
else
|
|
1021
|
+
await replaceIfUnchanged(target, record.beforeContent, record.afterSha256);
|
|
1022
|
+
}
|
|
1023
|
+
await rm(await assertNoSymlink(root, MANIFEST_PATH));
|
|
1024
|
+
for (const directory of ['.agent-native']) {
|
|
1025
|
+
const target = path.join(root, directory);
|
|
1026
|
+
try {
|
|
1027
|
+
await rmdir(target);
|
|
1028
|
+
}
|
|
1029
|
+
catch (error) {
|
|
1030
|
+
if (typeof error !== 'object' ||
|
|
1031
|
+
error === null ||
|
|
1032
|
+
!('code' in error) ||
|
|
1033
|
+
error.code !== 'ENOTEMPTY')
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return { status: 'restored', removed: removable, conflicts: [] };
|
|
1038
|
+
}
|
|
1039
|
+
//# sourceMappingURL=init.js.map
|