anhedral 0.8.4 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -11
- package/assets/anhedral-cli-init-technical.svg +9 -9
- package/assets/anhedral-cli-init.svg +9 -9
- package/assets/security/LICENSES.txt +63 -0
- package/assets/security/audit-deps.mjs +295 -0
- package/assets/security/braces-regression.mjs +102 -0
- package/assets/security/braces@3.0.3.patch +285 -0
- package/assets/security/decode-uri-component@0.2.2.patch +149 -0
- package/assets/security/decoder-regression.mjs +37 -0
- package/assets/security/forge-regression.mjs +46 -0
- package/assets/security/manifest.json +53 -0
- package/assets/security/node-forge@1.4.0.patch +26 -0
- package/dist/bin.js +39 -2
- package/dist/capabilities.d.ts +11 -7
- package/dist/capabilities.js +26 -22
- package/dist/dependencies.d.ts +3 -2
- package/dist/dependencies.js +44 -40
- package/dist/evidence-validation.d.ts +2 -0
- package/dist/evidence-validation.js +18 -0
- package/dist/foundation.d.ts +6 -0
- package/dist/foundation.js +49 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/platforms/desktop.js +3 -3
- package/dist/platforms/extension.js +64 -94
- package/dist/platforms/mobile.js +1 -1
- package/dist/pnpm-policy.d.ts +6 -0
- package/dist/pnpm-policy.js +61 -0
- package/dist/progress-view.d.ts +57 -0
- package/dist/progress-view.js +69 -0
- package/dist/progress.d.ts +95 -0
- package/dist/progress.js +327 -0
- package/dist/project.d.ts +1 -0
- package/dist/readiness.d.ts +1 -0
- package/dist/readiness.js +7 -2
- package/dist/render.d.ts +4 -0
- package/dist/render.js +13 -0
- package/dist/security-backports.d.ts +3 -0
- package/dist/security-backports.js +21 -0
- package/dist/standard.d.ts +2 -0
- package/dist/standard.js +161 -98
- package/dist/transaction.js +34 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/application-stack-standard.md +85 -669
- package/package.json +18 -9
package/dist/progress.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, constants, existsSync, fsyncSync, fstatSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
5
|
+
import { isNonSecretEvidence, isProgressEnvironment } from './evidence-validation.js';
|
|
6
|
+
export const PROGRESS_FILE = 'anhedral.progress.json';
|
|
7
|
+
export const MILESTONES = ['generated', 'provisioned', 'connected', 'tested', 'deployed'];
|
|
8
|
+
const MAX_BYTES = 262144;
|
|
9
|
+
function object(value, keys) {
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
throw new Error('Expected a progress object.');
|
|
12
|
+
const result = value;
|
|
13
|
+
if (Object.keys(result).some(key => !keys.includes(key)))
|
|
14
|
+
throw new Error('Unknown progress field.');
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function text(value, max = 600) {
|
|
18
|
+
if (typeof value !== 'string' || !value.trim() || value.length > max || !isNonSecretEvidence(value))
|
|
19
|
+
throw new Error('Progress requires bounded nonsecret text.');
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function id(value) {
|
|
23
|
+
const result = text(value, 80);
|
|
24
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(result) || ['__proto__', 'constructor', 'prototype'].includes(result))
|
|
25
|
+
throw new Error('Invalid progress identifier.');
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
function environment(value) {
|
|
29
|
+
const result = text(value, 80);
|
|
30
|
+
if (!isProgressEnvironment(result))
|
|
31
|
+
throw new Error('Invalid progress environment.');
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function date(value) {
|
|
35
|
+
const result = text(value, 30);
|
|
36
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(result) || !Number.isFinite(Date.parse(result)))
|
|
37
|
+
throw new Error('Expected a UTC timestamp.');
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function member(value, options) {
|
|
41
|
+
if (!options.includes(value))
|
|
42
|
+
throw new Error('Invalid progress category.');
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function integer(value) {
|
|
46
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
47
|
+
throw new Error('Invalid progress revision.');
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function list(value, validate, max) {
|
|
51
|
+
if (!Array.isArray(value) || value.length > max)
|
|
52
|
+
throw new Error('Progress list exceeds its limit.');
|
|
53
|
+
return value.map(validate);
|
|
54
|
+
}
|
|
55
|
+
function url(value) {
|
|
56
|
+
const result = text(value, 2048);
|
|
57
|
+
const parsed = new URL(result);
|
|
58
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash || !(parsed.protocol === 'https:' || (parsed.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(parsed.hostname))))
|
|
59
|
+
throw new Error('Use a credential-free HTTPS or loopback preview URL without query or fragment.');
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function observation(value, discovery = false) {
|
|
63
|
+
const shared = ['outcome', 'source', 'observedAt', 'evidence', 'accountRef'];
|
|
64
|
+
const item = object(value, [...shared, ...(discovery ? ['provider', 'route', 'check', 'expiresAt'] : ['capability', 'milestone', 'configurationFingerprint', 'resourceRef', 'revisionRef'])]);
|
|
65
|
+
const evidence = { outcome: member(item.outcome, ['passed', 'failed', 'blocked']), source: member(item.source, ['agent', 'independent']), observedAt: date(item.observedAt), evidence: text(item.evidence) };
|
|
66
|
+
if (item.accountRef !== undefined)
|
|
67
|
+
evidence.accountRef = text(item.accountRef, 160);
|
|
68
|
+
if (discovery) {
|
|
69
|
+
const expiresAt = date(item.expiresAt);
|
|
70
|
+
if (Date.parse(expiresAt) <= Date.parse(evidence.observedAt) || Date.parse(expiresAt) - Date.parse(evidence.observedAt) > 86400000 * 30)
|
|
71
|
+
throw new Error('Discovery expires within 30 days of observation.');
|
|
72
|
+
if (evidence.outcome === 'passed' && ['authorized', 'integration-tested'].includes(String(item.check)) && !evidence.accountRef)
|
|
73
|
+
throw new Error('Successful account authorization and integration checks require an account reference.');
|
|
74
|
+
return { ...evidence, provider: id(item.provider), route: member(item.route, ['plugin', 'api', 'cli', 'browser']), check: member(item.check, ['installed', 'callable', 'local-working', 'authorized', 'integration-tested']), expiresAt };
|
|
75
|
+
}
|
|
76
|
+
const fingerprint = text(item.configurationFingerprint, 64);
|
|
77
|
+
if (!/^[a-f0-9]{64}$/.test(fingerprint))
|
|
78
|
+
throw new Error('Invalid configuration fingerprint.');
|
|
79
|
+
const result = { ...evidence, capability: id(item.capability), milestone: member(item.milestone, MILESTONES), configurationFingerprint: fingerprint };
|
|
80
|
+
for (const key of ['resourceRef', 'revisionRef'])
|
|
81
|
+
if (item[key] !== undefined)
|
|
82
|
+
result[key] = text(item[key], 160);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
export function validateProgressReport(value) {
|
|
86
|
+
const item = object(value, ['revision', 'environment', 'observations', 'discovery', 'blockers', 'nextAction', 'previewUrl', 'liveUrl']);
|
|
87
|
+
const result = { revision: integer(item.revision), environment: environment(item.environment) };
|
|
88
|
+
if (item.observations !== undefined)
|
|
89
|
+
result.observations = list(item.observations, v => observation(v), 100);
|
|
90
|
+
if (item.discovery !== undefined)
|
|
91
|
+
result.discovery = list(item.discovery, v => observation(v, true), 100);
|
|
92
|
+
if (item.blockers !== undefined)
|
|
93
|
+
result.blockers = list(item.blockers, v => text(v), 20);
|
|
94
|
+
if (item.nextAction !== undefined)
|
|
95
|
+
result.nextAction = item.nextAction === null ? null : text(item.nextAction);
|
|
96
|
+
for (const key of ['previewUrl', 'liveUrl'])
|
|
97
|
+
if (item[key] !== undefined)
|
|
98
|
+
result[key] = item[key] === null ? null : url(item[key]);
|
|
99
|
+
if (JSON.stringify(result).length > MAX_BYTES)
|
|
100
|
+
throw new Error('Progress report too large.');
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
export function validateProjectProgress(value) {
|
|
104
|
+
const item = object(value, ['schemaVersion', 'revision', 'updatedAt', 'environments']);
|
|
105
|
+
if (item.schemaVersion !== 1)
|
|
106
|
+
throw new Error('Unsupported progress schema.');
|
|
107
|
+
const environments = list(item.environments, entry => {
|
|
108
|
+
const env = object(entry, ['id', 'observations', 'discovery', 'blockers', 'nextAction', 'previewUrl', 'liveUrl']);
|
|
109
|
+
for (const key of ['nextAction', 'previewUrl', 'liveUrl'])
|
|
110
|
+
if (env[key] === null)
|
|
111
|
+
throw new Error('Persisted progress fields must be strings or absent.');
|
|
112
|
+
const { id: environment, ...fields } = env;
|
|
113
|
+
const report = validateProgressReport({ ...fields, environment, revision: 0 });
|
|
114
|
+
return { id: report.environment, observations: report.observations ?? [], discovery: report.discovery ?? [], blockers: report.blockers ?? [], ...Object.fromEntries(['nextAction', 'previewUrl', 'liveUrl'].filter(key => env[key] !== undefined).map(key => [key, env[key]])) };
|
|
115
|
+
}, 20);
|
|
116
|
+
if (new Set(environments.map(env => env.id)).size !== environments.length)
|
|
117
|
+
throw new Error('Duplicate progress environment.');
|
|
118
|
+
return { schemaVersion: 1, revision: integer(item.revision), updatedAt: date(item.updatedAt), environments };
|
|
119
|
+
}
|
|
120
|
+
function filePath(root, file) {
|
|
121
|
+
const canonical = realpathSync(root);
|
|
122
|
+
if (!lstatSync(canonical).isDirectory())
|
|
123
|
+
throw new Error('Project root must be a directory.');
|
|
124
|
+
const target = path.join(canonical, file);
|
|
125
|
+
try {
|
|
126
|
+
const entry = lstatSync(target);
|
|
127
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
128
|
+
throw new Error('Progress paths must be regular files.');
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error.code !== 'ENOENT')
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
return target;
|
|
135
|
+
}
|
|
136
|
+
function safeRead(file, limit = MAX_BYTES) {
|
|
137
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
138
|
+
try {
|
|
139
|
+
if (fstatSync(fd).size > limit)
|
|
140
|
+
throw new Error('Progress/configuration file exceeds size limit.');
|
|
141
|
+
const data = readFileSync(fd);
|
|
142
|
+
if (data.length > limit)
|
|
143
|
+
throw new Error('Progress/configuration file exceeds size limit.');
|
|
144
|
+
return data;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
closeSync(fd);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function readProjectProgress(root) {
|
|
151
|
+
const file = filePath(root, PROGRESS_FILE);
|
|
152
|
+
return existsSync(file) ? validateProjectProgress(JSON.parse(safeRead(file).toString('utf8'))) : null;
|
|
153
|
+
}
|
|
154
|
+
/** Bounded source/configuration digest; secrets, dependencies and generated output are excluded. */
|
|
155
|
+
export function projectFingerprint(root) {
|
|
156
|
+
const canonical = realpathSync(root);
|
|
157
|
+
const files = new Set(['anhedral.setup.json', 'anhedral.standard.json', 'package.json', 'pnpm-workspace.yaml', 'pnpm-lock.yaml', 'wrangler.json', 'wrangler.jsonc', 'wrangler.toml']);
|
|
158
|
+
const ignored = new Set(['node_modules', 'dist', 'build', 'out', 'coverage', '.git', '.next', '.turbo', '.artifacts', '.anhedral', '.cache', '.wrangler', '.output', '.expo', '.venv']);
|
|
159
|
+
const sourceFile = (name) => !['cloudflare-env.d.ts', 'worker-configuration.d.ts'].includes(name) && (/\.(?:[cm]?[jt]sx?|css|sql|prisma)$/.test(name) || /^(?:package|wrangler|tsconfig)(?:\.[\w-]+)?\.jsonc?$/.test(name) || name === 'wrangler.toml');
|
|
160
|
+
let entries = 0;
|
|
161
|
+
const visit = (directory, depth) => {
|
|
162
|
+
if (depth > 12)
|
|
163
|
+
throw new Error('Source fingerprint depth limit exceeded.');
|
|
164
|
+
for (const name of readdirSync(path.join(canonical, directory)).sort()) {
|
|
165
|
+
if (++entries > 4000)
|
|
166
|
+
throw new Error('Source fingerprint entry limit exceeded.');
|
|
167
|
+
if (name.startsWith('.') || ignored.has(name) || /(?:secret|credential|private[-_.]?key)/i.test(name))
|
|
168
|
+
continue;
|
|
169
|
+
const relative = path.join(directory, name), entry = lstatSync(path.join(canonical, relative));
|
|
170
|
+
if (entry.isSymbolicLink())
|
|
171
|
+
throw new Error('Source fingerprint paths cannot be symlinks.');
|
|
172
|
+
if (entry.isDirectory())
|
|
173
|
+
visit(relative, depth + 1);
|
|
174
|
+
else if (entry.isFile() && sourceFile(name))
|
|
175
|
+
files.add(relative);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
for (const directory of ['apps', 'packages', 'src', 'app', 'pages', 'components', 'lib']) {
|
|
179
|
+
const target = path.join(canonical, directory);
|
|
180
|
+
if (!existsSync(target))
|
|
181
|
+
continue;
|
|
182
|
+
if (lstatSync(target).isSymbolicLink())
|
|
183
|
+
throw new Error('Source fingerprint paths cannot be symlinks.');
|
|
184
|
+
if (lstatSync(target).isDirectory())
|
|
185
|
+
visit(directory, 0);
|
|
186
|
+
}
|
|
187
|
+
for (const name of readdirSync(canonical))
|
|
188
|
+
if (!name.startsWith('.') && !/(?:secret|credential|private[-_.]?key)/i.test(name) && sourceFile(name))
|
|
189
|
+
files.add(name);
|
|
190
|
+
const hash = createHash('sha256');
|
|
191
|
+
let bytes = 0;
|
|
192
|
+
for (const file of [...files].sort()) {
|
|
193
|
+
const target = filePath(root, file);
|
|
194
|
+
const content = existsSync(target) ? safeRead(target, file === 'pnpm-lock.yaml' ? 2097152 : MAX_BYTES) : Buffer.from('<absent>');
|
|
195
|
+
bytes += content.length;
|
|
196
|
+
if (bytes > 8388608)
|
|
197
|
+
throw new Error('Source fingerprint byte limit exceeded.');
|
|
198
|
+
hash.update(JSON.stringify(file));
|
|
199
|
+
hash.update(`:${content.length}:`);
|
|
200
|
+
hash.update(content);
|
|
201
|
+
}
|
|
202
|
+
return hash.digest('hex');
|
|
203
|
+
}
|
|
204
|
+
function merge(old, added, key) {
|
|
205
|
+
const entries = new Map(old.map(item => [key(item), item]));
|
|
206
|
+
for (const item of added) {
|
|
207
|
+
const previous = entries.get(key(item));
|
|
208
|
+
if (previous && Date.parse(previous.observedAt) > Date.parse(item.observedAt))
|
|
209
|
+
throw new Error('Older evidence cannot replace a newer observation.');
|
|
210
|
+
entries.set(key(item), item);
|
|
211
|
+
}
|
|
212
|
+
if (entries.size > 100)
|
|
213
|
+
throw new Error('Too many progress observations.');
|
|
214
|
+
return [...entries.values()];
|
|
215
|
+
}
|
|
216
|
+
function recoverDeadWriter(lock) {
|
|
217
|
+
const recovery = `${lock}.recovery`;
|
|
218
|
+
let guard;
|
|
219
|
+
try {
|
|
220
|
+
guard = openSync(recovery, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
if (!existsSync(lock))
|
|
227
|
+
return true;
|
|
228
|
+
const entry = lstatSync(lock);
|
|
229
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !process.getuid || entry.uid !== process.getuid())
|
|
230
|
+
return false;
|
|
231
|
+
let owner;
|
|
232
|
+
try {
|
|
233
|
+
owner = JSON.parse(safeRead(lock, 1024).toString('utf8'));
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || owner.hostname !== hostname())
|
|
239
|
+
return false;
|
|
240
|
+
try {
|
|
241
|
+
process.kill(owner.pid, 0);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (error.code !== 'ESRCH')
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const current = lstatSync(lock);
|
|
249
|
+
if (current.ino !== entry.ino || current.dev !== entry.dev || current.uid !== entry.uid || !current.isFile() || current.isSymbolicLink())
|
|
250
|
+
return false;
|
|
251
|
+
unlinkSync(lock);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
closeSync(guard);
|
|
256
|
+
unlinkSync(recovery);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function acquireProgressLock(lock) {
|
|
260
|
+
const busy = () => new Error('Progress update busy or lock ownership unknown; retry after the writer finishes. Unknown/crashed recovery locks require confirming no writer is running before removal.');
|
|
261
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
262
|
+
if (existsSync(`${lock}.recovery`))
|
|
263
|
+
throw busy();
|
|
264
|
+
try {
|
|
265
|
+
return openSync(lock, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (error.code !== 'EEXIST')
|
|
269
|
+
throw error;
|
|
270
|
+
if (attempt || !recoverDeadWriter(lock))
|
|
271
|
+
throw busy();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
throw busy();
|
|
275
|
+
}
|
|
276
|
+
/** Reports are caller assertions. Only actual checking adapters should use source: independent. */
|
|
277
|
+
export function reportProjectProgress(root, input) {
|
|
278
|
+
const report = validateProgressReport(input);
|
|
279
|
+
const destination = filePath(root, PROGRESS_FILE);
|
|
280
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
281
|
+
const lock = filePath(root, '.anhedral-progress.lock');
|
|
282
|
+
const fd = acquireProgressLock(lock);
|
|
283
|
+
try {
|
|
284
|
+
writeFileSync(fd, JSON.stringify({ pid: process.pid, hostname: hostname(), createdAt: new Date().toISOString() }));
|
|
285
|
+
fsyncSync(fd);
|
|
286
|
+
const current = readProjectProgress(root) ?? { schemaVersion: 1, revision: 0, updatedAt: new Date().toISOString(), environments: [] };
|
|
287
|
+
if (report.revision !== current.revision)
|
|
288
|
+
throw new Error('Progress revision conflict; reload before reporting.');
|
|
289
|
+
for (const item of [...(report.observations ?? []), ...(report.discovery ?? [])])
|
|
290
|
+
if (Date.parse(item.observedAt) > Date.now() + 300000)
|
|
291
|
+
throw new Error('Observation timestamp is in the future.');
|
|
292
|
+
const existing = current.environments.find(env => env.id === report.environment) ?? { id: report.environment, observations: [], discovery: [], blockers: [] };
|
|
293
|
+
const next = { ...existing, observations: merge(existing.observations, report.observations ?? [], item => JSON.stringify([item.capability, item.milestone, item.source, item.accountRef, item.resourceRef])), discovery: merge(existing.discovery, report.discovery ?? [], item => JSON.stringify([item.provider, item.route, item.check, item.source, item.accountRef])) };
|
|
294
|
+
if (report.blockers !== undefined)
|
|
295
|
+
next.blockers = report.blockers;
|
|
296
|
+
for (const key of ['nextAction', 'previewUrl', 'liveUrl'])
|
|
297
|
+
if (report[key] !== undefined) {
|
|
298
|
+
if (report[key] === null)
|
|
299
|
+
delete next[key];
|
|
300
|
+
else
|
|
301
|
+
next[key] = report[key];
|
|
302
|
+
}
|
|
303
|
+
const result = validateProjectProgress({ ...current, revision: current.revision + 1, updatedAt: new Date().toISOString(), environments: [...current.environments.filter(env => env.id !== next.id), next] });
|
|
304
|
+
const serialized = JSON.stringify(result, null, 2) + '\n';
|
|
305
|
+
if (Buffer.byteLength(serialized) > MAX_BYTES)
|
|
306
|
+
throw new Error('Progress record exceeds size limit.');
|
|
307
|
+
const output = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
308
|
+
try {
|
|
309
|
+
writeFileSync(output, serialized);
|
|
310
|
+
fsyncSync(output);
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
closeSync(output);
|
|
314
|
+
}
|
|
315
|
+
renameSync(temporary, destination);
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
closeSync(fd);
|
|
320
|
+
if (existsSync(temporary))
|
|
321
|
+
unlinkSync(temporary);
|
|
322
|
+
unlinkSync(lock);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
export function evaluateProjectProgress(record, fingerprint, now = Date.now()) {
|
|
326
|
+
return { ...record, environments: record.environments.map(env => ({ ...env, observations: env.observations.map(item => ({ ...item, stale: item.configurationFingerprint !== fingerprint })), discovery: env.discovery.map(item => ({ ...item, stale: Date.parse(item.expiresAt) <= now })) })) };
|
|
327
|
+
}
|
package/dist/project.d.ts
CHANGED
package/dist/readiness.d.ts
CHANGED
package/dist/readiness.js
CHANGED
|
@@ -6,15 +6,16 @@ export function inspectProject(root) {
|
|
|
6
6
|
const checks = [];
|
|
7
7
|
const record = (id, ok, detail) => checks.push({ id, ok, detail });
|
|
8
8
|
const read = (file) => JSON.parse(readFileSync(path.join(root, file), 'utf8'));
|
|
9
|
-
record('workspace', existsSync(path.join(root, 'pnpm-workspace.yaml')), 'pnpm workspace manifest');
|
|
10
9
|
record('lockfile', existsSync(path.join(root, 'pnpm-lock.yaml')), 'Committed dependency resolution required');
|
|
11
10
|
try {
|
|
12
11
|
const plan = read('anhedral.setup.json');
|
|
13
12
|
validateSetupPlan(plan);
|
|
14
13
|
const standard = read('anhedral.standard.json');
|
|
14
|
+
const layout = plan.layout ?? 'workspace';
|
|
15
|
+
record('foundation', layout === 'single' ? existsSync(path.join(root, 'package.json')) && existsSync(path.join(root, 'src/index.ts')) : existsSync(path.join(root, 'pnpm-workspace.yaml')), layout === 'single' ? 'Standalone Hono application' : 'pnpm workspace manifest');
|
|
15
16
|
record('setup-plan', true, `Requirements recorded by CLI ${plan.cliVersion}`);
|
|
16
17
|
record('cli-compatibility', plan.cliVersion === CAPABILITY_REGISTRY.cliVersion, 'Use the project CLI version or review upgrade differences');
|
|
17
|
-
record('selection', standard.schemaVersion === 1 && JSON.stringify(standard.products) === JSON.stringify(plan.products) && standard.hosting === (plan.products.includes('next') ? plan.hosting : null), 'Setup plan matches project selection');
|
|
18
|
+
record('selection', standard.schemaVersion === 1 && JSON.stringify(standard.products) === JSON.stringify(plan.products) && standard.hosting === (plan.products.includes('next') ? plan.hosting : null) && (standard.layout ?? 'workspace') === layout, 'Setup plan matches project selection');
|
|
18
19
|
}
|
|
19
20
|
catch {
|
|
20
21
|
record('setup-plan', false, 'Missing, invalid or unsupported setup plan; inspect the project before changing it');
|
|
@@ -31,6 +32,10 @@ export function validateSetupPlan(plan) {
|
|
|
31
32
|
throw new Error('Unknown setup capabilities.');
|
|
32
33
|
if (value.hosting !== 'cloudflare' && value.hosting !== 'vercel')
|
|
33
34
|
throw new Error('Invalid hosting.');
|
|
35
|
+
if (value.layout !== undefined && value.layout !== 'workspace' && value.layout !== 'single')
|
|
36
|
+
throw new Error('Invalid layout.');
|
|
37
|
+
if (value.layout === 'single' && (value.products.length !== 1 || value.products[0] !== 'hono'))
|
|
38
|
+
throw new Error('Unsupported standalone recipe.');
|
|
34
39
|
validateRequirements(value.setup, value.products);
|
|
35
40
|
const selected = new Set(value.products);
|
|
36
41
|
resolveStandardProducts(selected, value.hosting, false);
|
package/dist/render.d.ts
CHANGED
|
@@ -8,3 +8,7 @@ export declare function markdownHeading(value: string): string;
|
|
|
8
8
|
export declare function packageNameFromText(value: string): string;
|
|
9
9
|
export declare function childPackageName(projectName: string, suffix: string): string;
|
|
10
10
|
export declare function identifierSegment(value: string): string;
|
|
11
|
+
/** Project names also become bootstrap directory components; scoped names are not supported. */
|
|
12
|
+
export declare function assertProjectName(value: string): string;
|
|
13
|
+
/** Provider identifiers obey DNS/R2 limits independently of valid npm names. */
|
|
14
|
+
export declare function resourceName(projectName: string, suffix: string): string;
|
package/dist/render.js
CHANGED
|
@@ -72,3 +72,16 @@ export function identifierSegment(value) {
|
|
|
72
72
|
.replace(/^-+|-+$/g, '');
|
|
73
73
|
return segment || 'app';
|
|
74
74
|
}
|
|
75
|
+
/** Project names also become bootstrap directory components; scoped names are not supported. */
|
|
76
|
+
export function assertProjectName(value) {
|
|
77
|
+
if (typeof value !== 'string' || value.includes('/'))
|
|
78
|
+
throw new Error('Project name must be a valid unscoped npm package name.');
|
|
79
|
+
return assertPackageName(value);
|
|
80
|
+
}
|
|
81
|
+
/** Provider identifiers obey DNS/R2 limits independently of valid npm names. */
|
|
82
|
+
export function resourceName(projectName, suffix) {
|
|
83
|
+
const normalized = identifierSegment(projectName);
|
|
84
|
+
const digest = createHash('sha256').update(projectName, 'utf8').digest('hex').slice(0, 8);
|
|
85
|
+
const distinct = normalized === projectName ? normalized : `${normalized}-${digest}`;
|
|
86
|
+
return `${compactPackageSegment(distinct, 63 - suffix.length - 1)}-${suffix}`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cpSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { mergePnpmPatches } from './pnpm-policy.js';
|
|
5
|
+
const assets = fileURLToPath(new URL('../assets/security/', import.meta.url));
|
|
6
|
+
const manifest = () => JSON.parse(readFileSync(path.join(assets, 'manifest.json'), 'utf8'));
|
|
7
|
+
export function writeSecurityBackports(root) {
|
|
8
|
+
cpSync(assets, path.join(root, 'scripts/security'), { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
/** Resolve first, then patch only exact packages present in the selected recipe. */
|
|
11
|
+
export function configureSecurityBackports(root) {
|
|
12
|
+
const lockfile = readFileSync(path.join(root, 'pnpm-lock.yaml'), 'utf8');
|
|
13
|
+
const entries = manifest().packages.filter(({ name, version }) => {
|
|
14
|
+
const coordinate = `${name}@${version}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
+
return new RegExp(`^ ['"]?${coordinate}['"]?:`, 'm').test(lockfile);
|
|
16
|
+
});
|
|
17
|
+
if (!entries.length)
|
|
18
|
+
return false;
|
|
19
|
+
mergePnpmPatches(root, Object.fromEntries(entries.map(({ name, version, patch }) => [`${name}@${version}`, `scripts/security/${patch}`])));
|
|
20
|
+
return true;
|
|
21
|
+
}
|
package/dist/standard.d.ts
CHANGED