anhedral 0.8.5 → 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 +14 -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 +10 -9
|
@@ -4,23 +4,23 @@ import { writeFile } from '../util.js';
|
|
|
4
4
|
import { EXTENSION_DEPENDENCIES } from '../dependencies.js';
|
|
5
5
|
import { childPackageName, htmlText, jsString, markdownHeading } from '../render.js';
|
|
6
6
|
export async function scaffoldExtension(root, options) {
|
|
7
|
-
const { projectName, displayName } = options;
|
|
7
|
+
const { projectName, displayName, extensionSurface = 'sidepanel' } = options;
|
|
8
8
|
const dir = path.join(root, 'apps/extension');
|
|
9
9
|
writePackageJson(dir, projectName);
|
|
10
10
|
writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({ extends: './.wxt/tsconfig.json', compilerOptions: { strict: true, jsx: 'react-jsx', paths: { '@/*': ['./src/*'] } } }, null, 2) + '\n');
|
|
11
11
|
writeReactLintConfig(dir);
|
|
12
|
-
writeWxtConfig(dir, displayName);
|
|
12
|
+
writeWxtConfig(dir, displayName, extensionSurface);
|
|
13
13
|
writeEnvExample(dir);
|
|
14
14
|
writePostcssConfig(dir);
|
|
15
|
-
writeTailwindConfig(dir);
|
|
16
15
|
writeShadcnConfig(dir);
|
|
17
|
-
writeReadme(dir, displayName);
|
|
16
|
+
writeReadme(dir, displayName, extensionSurface);
|
|
18
17
|
writeCnUtil(dir);
|
|
19
18
|
writeButtonComponent(dir);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
if (extensionSurface === 'sidepanel')
|
|
20
|
+
writeBackground(dir);
|
|
21
|
+
writeSurfaceEntry(dir, extensionSurface);
|
|
22
|
+
writeSurfaceHtml(dir, displayName, extensionSurface);
|
|
23
|
+
writeSurfaceApp(dir, extensionSurface);
|
|
24
24
|
writeStyles(dir);
|
|
25
25
|
}
|
|
26
26
|
function writePackageJson(dir, projectName) {
|
|
@@ -38,10 +38,10 @@ function writePackageJson(dir, projectName) {
|
|
|
38
38
|
lint: 'eslint src --max-warnings 0',
|
|
39
39
|
},
|
|
40
40
|
dependencies: EXTENSION_DEPENDENCIES.dependencies,
|
|
41
|
-
devDependencies: { ...EXTENSION_DEPENDENCIES.devDependencies, '@workspace/eslint-config': 'workspace:*', eslint: '
|
|
41
|
+
devDependencies: { ...EXTENSION_DEPENDENCIES.devDependencies, '@workspace/eslint-config': 'workspace:*', eslint: '9.39.5' },
|
|
42
42
|
}, null, 2) + '\n');
|
|
43
43
|
}
|
|
44
|
-
function writeWxtConfig(dir, displayName) {
|
|
44
|
+
function writeWxtConfig(dir, displayName, surface) {
|
|
45
45
|
const nameLiteral = jsString(displayName);
|
|
46
46
|
const descriptionLiteral = jsString(`${displayName} Chrome Extension`);
|
|
47
47
|
const actionTitleLiteral = jsString(`Open ${displayName}`);
|
|
@@ -59,15 +59,14 @@ export default defineConfig({
|
|
|
59
59
|
description: ${descriptionLiteral},
|
|
60
60
|
version: '0.1.0',
|
|
61
61
|
...(crxPublicKey ? { key: crxPublicKey } : {}),
|
|
62
|
-
minimum_chrome_version: '114',
|
|
63
|
-
permissions: ['activeTab', 'scripting', 'sidePanel'],
|
|
62
|
+
${surface === 'sidepanel' ? "minimum_chrome_version: '114'," : ''}
|
|
63
|
+
permissions: ${surface === 'sidepanel' ? "['activeTab', 'scripting', 'sidePanel']" : "['activeTab', 'scripting']"},
|
|
64
64
|
host_permissions: [],
|
|
65
65
|
action: {
|
|
66
66
|
default_title: ${actionTitleLiteral},
|
|
67
|
+
${surface === 'popup' ? "default_popup: 'popup.html'," : ''}
|
|
67
68
|
},
|
|
68
|
-
side_panel: {
|
|
69
|
-
default_path: 'sidepanel.html',
|
|
70
|
-
},
|
|
69
|
+
${surface === 'sidepanel' ? "side_panel: { default_path: 'sidepanel.html' }," : ''}
|
|
71
70
|
};
|
|
72
71
|
},
|
|
73
72
|
modules: ['@wxt-dev/module-react'],
|
|
@@ -91,8 +90,7 @@ function writeEnvExample(dir) {
|
|
|
91
90
|
function writePostcssConfig(dir) {
|
|
92
91
|
writeFile(path.join(dir, 'postcss.config.cjs'), `module.exports = {
|
|
93
92
|
plugins: {
|
|
94
|
-
tailwindcss: {},
|
|
95
|
-
autoprefixer: {},
|
|
93
|
+
"@tailwindcss/postcss": {},
|
|
96
94
|
},
|
|
97
95
|
};
|
|
98
96
|
`);
|
|
@@ -164,62 +162,10 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
164
162
|
Button.displayName = 'Button';
|
|
165
163
|
`);
|
|
166
164
|
}
|
|
167
|
-
function
|
|
168
|
-
writeFile(path.join(dir, 'tailwind.config.cjs'), `/** @type {import('tailwindcss').Config} */
|
|
169
|
-
module.exports = {
|
|
170
|
-
content: ['./src/**/*.{ts,tsx,html}'],
|
|
171
|
-
theme: {
|
|
172
|
-
extend: {
|
|
173
|
-
colors: {
|
|
174
|
-
border: 'hsl(var(--border))',
|
|
175
|
-
input: 'hsl(var(--input))',
|
|
176
|
-
ring: 'hsl(var(--ring))',
|
|
177
|
-
background: 'hsl(var(--background))',
|
|
178
|
-
foreground: 'hsl(var(--foreground))',
|
|
179
|
-
primary: {
|
|
180
|
-
DEFAULT: 'hsl(var(--primary))',
|
|
181
|
-
foreground: 'hsl(var(--primary-foreground))',
|
|
182
|
-
},
|
|
183
|
-
secondary: {
|
|
184
|
-
DEFAULT: 'hsl(var(--secondary))',
|
|
185
|
-
foreground: 'hsl(var(--secondary-foreground))',
|
|
186
|
-
},
|
|
187
|
-
destructive: {
|
|
188
|
-
DEFAULT: 'hsl(var(--destructive))',
|
|
189
|
-
foreground: 'hsl(var(--destructive-foreground))',
|
|
190
|
-
},
|
|
191
|
-
muted: {
|
|
192
|
-
DEFAULT: 'hsl(var(--muted))',
|
|
193
|
-
foreground: 'hsl(var(--muted-foreground))',
|
|
194
|
-
},
|
|
195
|
-
accent: {
|
|
196
|
-
DEFAULT: 'hsl(var(--accent))',
|
|
197
|
-
foreground: 'hsl(var(--accent-foreground))',
|
|
198
|
-
},
|
|
199
|
-
popover: {
|
|
200
|
-
DEFAULT: 'hsl(var(--popover))',
|
|
201
|
-
foreground: 'hsl(var(--popover-foreground))',
|
|
202
|
-
},
|
|
203
|
-
card: {
|
|
204
|
-
DEFAULT: 'hsl(var(--card))',
|
|
205
|
-
foreground: 'hsl(var(--card-foreground))',
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
borderRadius: {
|
|
209
|
-
lg: 'var(--radius)',
|
|
210
|
-
md: 'calc(var(--radius) - 2px)',
|
|
211
|
-
sm: 'calc(var(--radius) - 4px)',
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
plugins: [],
|
|
216
|
-
};
|
|
217
|
-
`);
|
|
218
|
-
}
|
|
219
|
-
function writeReadme(dir, displayName) {
|
|
165
|
+
function writeReadme(dir, displayName, surface) {
|
|
220
166
|
writeFile(path.join(dir, 'README.md'), `# ${markdownHeading(displayName)} Chrome Extension
|
|
221
167
|
|
|
222
|
-
WXT
|
|
168
|
+
WXT ${surface} extension generated by anhedral. Choose the surface for the task; content UI, options pages and background-only behavior require targeted implementation.
|
|
223
169
|
|
|
224
170
|
## Development
|
|
225
171
|
|
|
@@ -236,29 +182,25 @@ Run these commands from \`apps/extension\`. Anhedral generates only \`.env.examp
|
|
|
236
182
|
|
|
237
183
|
Run \`pnpm build\`, then load \`.output/chrome-mv3\` as an unpacked extension from \`chrome://extensions\`.
|
|
238
184
|
|
|
239
|
-
The
|
|
185
|
+
${surface === 'sidepanel' ? "The browser action opens the Side Panel API surface (Chrome 114+) with the sidePanel permission." : "The browser action opens popup.html; no sidePanel permission or panel background handler is generated."} Active-page reads use a user-triggered activeTab + scripting grant instead of persistent site access. Remove these permissions and the example read action if the product does not need page inspection.
|
|
240
186
|
`);
|
|
241
187
|
}
|
|
242
188
|
function writeBackground(dir) {
|
|
243
|
-
writeFile(path.join(dir, 'src/entrypoints/background.ts'), `
|
|
244
|
-
sidePanel: {
|
|
245
|
-
setPanelBehavior: (behavior: { openPanelOnActionClick: boolean }) => Promise<void>;
|
|
246
|
-
};
|
|
247
|
-
};
|
|
189
|
+
writeFile(path.join(dir, 'src/entrypoints/background.ts'), `import { browser } from 'wxt/browser';
|
|
248
190
|
|
|
249
191
|
export default defineBackground(() => {
|
|
250
192
|
|
|
251
193
|
// Open the side panel when the extension icon is clicked.
|
|
252
|
-
|
|
194
|
+
browser.sidePanel
|
|
253
195
|
.setPanelBehavior({ openPanelOnActionClick: true })
|
|
254
196
|
.catch(() => {});
|
|
255
197
|
});
|
|
256
198
|
`);
|
|
257
199
|
}
|
|
258
|
-
function
|
|
259
|
-
writeFile(path.join(dir,
|
|
200
|
+
function writeSurfaceEntry(dir, surface) {
|
|
201
|
+
writeFile(path.join(dir, `src/entrypoints/${surface}/main.tsx`), `import * as React from 'react';
|
|
260
202
|
import { createRoot } from 'react-dom/client';
|
|
261
|
-
import {
|
|
203
|
+
import { ExtensionApp } from './app';
|
|
262
204
|
import '../../styles/main.css';
|
|
263
205
|
|
|
264
206
|
const container = document.getElementById('root');
|
|
@@ -266,14 +208,14 @@ if (container) {
|
|
|
266
208
|
const root = createRoot(container);
|
|
267
209
|
root.render(
|
|
268
210
|
<React.StrictMode>
|
|
269
|
-
<
|
|
211
|
+
<ExtensionApp />
|
|
270
212
|
</React.StrictMode>
|
|
271
213
|
);
|
|
272
214
|
}
|
|
273
215
|
`);
|
|
274
216
|
}
|
|
275
|
-
function
|
|
276
|
-
writeFile(path.join(dir,
|
|
217
|
+
function writeSurfaceHtml(dir, displayName, surface) {
|
|
218
|
+
writeFile(path.join(dir, `src/entrypoints/${surface}/index.html`), `<!DOCTYPE html>
|
|
277
219
|
<html lang="en">
|
|
278
220
|
<head>
|
|
279
221
|
<meta charset="UTF-8" />
|
|
@@ -287,8 +229,9 @@ function writeSidepanelHtml(dir, displayName) {
|
|
|
287
229
|
</html>
|
|
288
230
|
`);
|
|
289
231
|
}
|
|
290
|
-
function
|
|
291
|
-
writeFile(path.join(dir,
|
|
232
|
+
function writeSurfaceApp(dir, surface) {
|
|
233
|
+
writeFile(path.join(dir, `src/entrypoints/${surface}/app.tsx`), `import * as React from 'react';
|
|
234
|
+
import { browser } from 'wxt/browser';
|
|
292
235
|
import { Button } from '../../components/ui/button';
|
|
293
236
|
|
|
294
237
|
type PageSnapshot = {
|
|
@@ -297,17 +240,21 @@ type PageSnapshot = {
|
|
|
297
240
|
};
|
|
298
241
|
|
|
299
242
|
async function readPageSnapshot(): Promise<PageSnapshot> {
|
|
300
|
-
const [tab] = await
|
|
243
|
+
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
|
301
244
|
if (!tab?.id) throw new Error('No active tab is available.');
|
|
302
|
-
const [
|
|
245
|
+
const [injection] = await browser.scripting.executeScript({
|
|
303
246
|
target: { tabId: tab.id },
|
|
304
247
|
func: () => ({ title: document.title, location: window.location.href }),
|
|
305
248
|
});
|
|
249
|
+
return requireSnapshot(injection?.result);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function requireSnapshot(result: PageSnapshot | undefined): PageSnapshot {
|
|
306
253
|
if (!result) throw new Error('The active page did not return a snapshot.');
|
|
307
|
-
return result
|
|
254
|
+
return result;
|
|
308
255
|
}
|
|
309
256
|
|
|
310
|
-
export function
|
|
257
|
+
export function ExtensionApp() {
|
|
311
258
|
const [page, setPage] = React.useState<PageSnapshot | null>(null);
|
|
312
259
|
const [pageError, setPageError] = React.useState<string | null>(null);
|
|
313
260
|
|
|
@@ -322,7 +269,7 @@ export function SidePanelApp() {
|
|
|
322
269
|
|
|
323
270
|
|
|
324
271
|
return (
|
|
325
|
-
<div style={{ padding: 24 }}>
|
|
272
|
+
<div style={{ padding: 24${surface === 'popup' ? ', minWidth: 320' : ''} }}>
|
|
326
273
|
<h2>Welcome!</h2>
|
|
327
274
|
<Button type="button" onClick={() => void readActivePage()}>Read active page</Button>
|
|
328
275
|
{page ? (
|
|
@@ -338,9 +285,32 @@ export function SidePanelApp() {
|
|
|
338
285
|
`);
|
|
339
286
|
}
|
|
340
287
|
function writeStyles(dir) {
|
|
341
|
-
writeFile(path.join(dir, 'src/styles/main.css'), `@
|
|
342
|
-
|
|
343
|
-
@
|
|
288
|
+
writeFile(path.join(dir, 'src/styles/main.css'), `@import "tailwindcss";
|
|
289
|
+
|
|
290
|
+
@theme inline {
|
|
291
|
+
--color-background: hsl(var(--background));
|
|
292
|
+
--color-foreground: hsl(var(--foreground));
|
|
293
|
+
--color-card: hsl(var(--card));
|
|
294
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
295
|
+
--color-popover: hsl(var(--popover));
|
|
296
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
297
|
+
--color-primary: hsl(var(--primary));
|
|
298
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
299
|
+
--color-secondary: hsl(var(--secondary));
|
|
300
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
301
|
+
--color-muted: hsl(var(--muted));
|
|
302
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
303
|
+
--color-accent: hsl(var(--accent));
|
|
304
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
305
|
+
--color-destructive: hsl(var(--destructive));
|
|
306
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
307
|
+
--color-border: hsl(var(--border));
|
|
308
|
+
--color-input: hsl(var(--input));
|
|
309
|
+
--color-ring: hsl(var(--ring));
|
|
310
|
+
--radius-lg: var(--radius);
|
|
311
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
312
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
313
|
+
}
|
|
344
314
|
|
|
345
315
|
@layer base {
|
|
346
316
|
:root {
|
package/dist/platforms/mobile.js
CHANGED
|
@@ -159,7 +159,7 @@ export async function scaffoldMobile(root, options) {
|
|
|
159
159
|
lint: 'eslint app components lib --max-warnings 0',
|
|
160
160
|
},
|
|
161
161
|
dependencies: { ...MOBILE_APP_DEPENDENCIES.dependencies, ...MOBILE_NATIVEWIND_DEPENDENCIES.dependencies },
|
|
162
|
-
devDependencies: { ...MOBILE_APP_DEPENDENCIES.devDependencies, '@workspace/eslint-config': 'workspace:*', eslint: '
|
|
162
|
+
devDependencies: { ...MOBILE_APP_DEPENDENCIES.devDependencies, '@workspace/eslint-config': 'workspace:*', eslint: '9.39.5' },
|
|
163
163
|
}, null, 2) + '\n');
|
|
164
164
|
writeFile(path.join(dir, 'app.json'), JSON.stringify({
|
|
165
165
|
expo: {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Preserve unrelated starter YAML while replacing the settings this initializer owns. */
|
|
2
|
+
export declare function setPnpmPolicy(root: string, policy: Record<string, unknown>): void;
|
|
3
|
+
/** Only reads the JSON-compatible values written above, never arbitrary client YAML. */
|
|
4
|
+
export declare function generatedPnpmPolicy(root: string, key: string): Record<string, string>;
|
|
5
|
+
/** Merge reviewed patches without deleting starter-maintained mappings. */
|
|
6
|
+
export declare function mergePnpmPatches(root: string, patches: Record<string, string>): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeFile } from './util.js';
|
|
4
|
+
function policyBlock(key) {
|
|
5
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6
|
+
return new RegExp(`^(?:${escaped}|"${escaped}"|'${escaped}')[ \\t]*:.*(?:\\r?\\n|$)(?:[ \\t]+[^\\n]*(?:\\n|$)|#[^\\n]*(?:\\n|$)|[ \\t]*\\n)*`, 'gm');
|
|
7
|
+
}
|
|
8
|
+
/** Preserve unrelated starter YAML while replacing the settings this initializer owns. */
|
|
9
|
+
export function setPnpmPolicy(root, policy) {
|
|
10
|
+
const file = path.join(root, 'pnpm-workspace.yaml');
|
|
11
|
+
let source = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
12
|
+
for (const [key, value] of Object.entries(policy)) {
|
|
13
|
+
source = source.replace(policyBlock(key), '');
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
continue;
|
|
16
|
+
source += `${source.endsWith('\n') || !source ? '' : '\n'}${key}: ${JSON.stringify(value)}\n`;
|
|
17
|
+
}
|
|
18
|
+
writeFile(file, source);
|
|
19
|
+
}
|
|
20
|
+
/** Only reads the JSON-compatible values written above, never arbitrary client YAML. */
|
|
21
|
+
export function generatedPnpmPolicy(root, key) {
|
|
22
|
+
const source = readFileSync(path.join(root, 'pnpm-workspace.yaml'), 'utf8');
|
|
23
|
+
const value = source.match(new RegExp(`^${key}: (.+)$`, 'm'))?.[1];
|
|
24
|
+
return value ? JSON.parse(value) : {};
|
|
25
|
+
}
|
|
26
|
+
/** Merge reviewed patches without deleting starter-maintained mappings. */
|
|
27
|
+
export function mergePnpmPatches(root, patches) {
|
|
28
|
+
const file = path.join(root, 'pnpm-workspace.yaml');
|
|
29
|
+
const source = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
30
|
+
const blocks = [...source.matchAll(policyBlock('patchedDependencies'))];
|
|
31
|
+
if (blocks.length > 1)
|
|
32
|
+
throw new Error('Duplicate patchedDependencies configuration');
|
|
33
|
+
const block = blocks[0]?.[0] ?? '';
|
|
34
|
+
const existing = patchRows(block, patches);
|
|
35
|
+
const replacement = 'patchedDependencies:\n' + existing + Object.entries(patches)
|
|
36
|
+
.map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)}\n`).join('');
|
|
37
|
+
const remaining = source.replace(policyBlock('patchedDependencies'), '');
|
|
38
|
+
writeFile(file, remaining + `${remaining.endsWith('\n') || !remaining ? '' : '\n'}${replacement}`);
|
|
39
|
+
}
|
|
40
|
+
function patchRows(block, patches) {
|
|
41
|
+
if (!block)
|
|
42
|
+
return '';
|
|
43
|
+
const [header, ...rows] = block.split(/\r?\n/);
|
|
44
|
+
const value = header.slice(header.indexOf(':') + 1).trim();
|
|
45
|
+
if (value && !value.startsWith('#')) {
|
|
46
|
+
const object = JSON.parse(value);
|
|
47
|
+
if (!object || Array.isArray(object) || typeof object !== 'object' ||
|
|
48
|
+
Object.values(object).some((entry) => typeof entry !== 'string'))
|
|
49
|
+
throw new Error('Unsupported patchedDependencies mapping');
|
|
50
|
+
return Object.entries(object).filter(([key]) => !Object.hasOwn(patches, key))
|
|
51
|
+
.map(([key, entry]) => ` ${JSON.stringify(key)}: ${JSON.stringify(entry)}\n`).join('');
|
|
52
|
+
}
|
|
53
|
+
return rows.filter((row) => {
|
|
54
|
+
if (!row.trim() || row.trimStart().startsWith('#'))
|
|
55
|
+
return true;
|
|
56
|
+
const match = row.match(/^ (?:"([^"\\]+)"|'([^']+)'|([^\s:'"]+)):\s*(?:"[^"\\]+"|'[^']+'|[^\s{}[\]&*!|>]+)(?:\s+#.*)?\s*$/);
|
|
57
|
+
if (!match)
|
|
58
|
+
throw new Error('Unsupported patchedDependencies mapping');
|
|
59
|
+
return !Object.hasOwn(patches, match[1] ?? match[2] ?? match[3]);
|
|
60
|
+
}).filter((row, index, rows) => row || index < rows.length - 1).map((row) => `${row}\n`).join('');
|
|
61
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { evaluateProjectProgress } from "./progress.js";
|
|
2
|
+
export type EnvironmentProgress = ReturnType<typeof evaluateProjectProgress>["environments"][number];
|
|
3
|
+
interface ProgressNode {
|
|
4
|
+
label?: string;
|
|
5
|
+
id: string;
|
|
6
|
+
capability?: string;
|
|
7
|
+
resourceId?: string;
|
|
8
|
+
status: "planned" | "building" | "configured" | "verified" | "blocked";
|
|
9
|
+
}
|
|
10
|
+
export interface ProviderObservation {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
status: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function componentResources<T extends ProviderObservation>(node: ProgressNode, resources: readonly T[]): T[];
|
|
16
|
+
export declare function providerAttention(resources: readonly ProviderObservation[]): boolean;
|
|
17
|
+
export declare function componentObservations(progress: EnvironmentProgress | undefined, node: ProgressNode): {
|
|
18
|
+
stale: boolean;
|
|
19
|
+
capability: string;
|
|
20
|
+
milestone: import("./progress.js").Milestone;
|
|
21
|
+
configurationFingerprint: string;
|
|
22
|
+
resourceRef?: string;
|
|
23
|
+
revisionRef?: string;
|
|
24
|
+
outcome: "passed" | "failed" | "blocked";
|
|
25
|
+
source: "agent" | "independent";
|
|
26
|
+
observedAt: string;
|
|
27
|
+
evidence: string;
|
|
28
|
+
accountRef?: string;
|
|
29
|
+
}[];
|
|
30
|
+
export declare function latestObservations(observations: EnvironmentProgress["observations"]): {
|
|
31
|
+
stale: boolean;
|
|
32
|
+
capability: string;
|
|
33
|
+
milestone: import("./progress.js").Milestone;
|
|
34
|
+
configurationFingerprint: string;
|
|
35
|
+
resourceRef?: string;
|
|
36
|
+
revisionRef?: string;
|
|
37
|
+
outcome: "passed" | "failed" | "blocked";
|
|
38
|
+
source: "agent" | "independent";
|
|
39
|
+
observedAt: string;
|
|
40
|
+
evidence: string;
|
|
41
|
+
accountRef?: string;
|
|
42
|
+
}[];
|
|
43
|
+
export declare function componentState(progress: EnvironmentProgress | undefined, node: ProgressNode, resources?: readonly ProviderObservation[]): "blocked" | "planned" | "building" | "configured" | "verified";
|
|
44
|
+
/** A failed optional route does not block a working alternative for the same scope. */
|
|
45
|
+
export declare function discoveryAttention(discovery: EnvironmentProgress["discovery"]): {
|
|
46
|
+
stale: boolean;
|
|
47
|
+
provider: string;
|
|
48
|
+
route: "plugin" | "api" | "cli" | "browser";
|
|
49
|
+
check: "installed" | "callable" | "local-working" | "authorized" | "integration-tested";
|
|
50
|
+
expiresAt: string;
|
|
51
|
+
outcome: "passed" | "failed" | "blocked";
|
|
52
|
+
source: "agent" | "independent";
|
|
53
|
+
observedAt: string;
|
|
54
|
+
evidence: string;
|
|
55
|
+
accountRef?: string;
|
|
56
|
+
}[];
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function componentResources(node, resources) {
|
|
2
|
+
return resources.filter((resource) => node.resourceId
|
|
3
|
+
? resource.id === node.resourceId
|
|
4
|
+
: resource.id === node.id ||
|
|
5
|
+
(!!node.label && resource.name === node.label));
|
|
6
|
+
}
|
|
7
|
+
export function providerAttention(resources) {
|
|
8
|
+
return resources.some((resource) => resource.status === "missing" || resource.status === "error");
|
|
9
|
+
}
|
|
10
|
+
export function componentObservations(progress, node) {
|
|
11
|
+
return (progress?.observations.filter((item) => (item.capability === (node.capability || node.id) &&
|
|
12
|
+
(!node.resourceId ||
|
|
13
|
+
!item.resourceRef ||
|
|
14
|
+
item.resourceRef === node.resourceId)) ||
|
|
15
|
+
(!!node.resourceId && item.resourceRef === node.resourceId)) || []);
|
|
16
|
+
}
|
|
17
|
+
export function latestObservations(observations) {
|
|
18
|
+
const latest = new Map();
|
|
19
|
+
for (const observation of observations) {
|
|
20
|
+
const key = JSON.stringify([
|
|
21
|
+
observation.capability,
|
|
22
|
+
observation.milestone,
|
|
23
|
+
observation.source,
|
|
24
|
+
observation.accountRef,
|
|
25
|
+
observation.resourceRef,
|
|
26
|
+
]);
|
|
27
|
+
const previous = latest.get(key);
|
|
28
|
+
if (!previous ||
|
|
29
|
+
Date.parse(observation.observedAt) >= Date.parse(previous.observedAt))
|
|
30
|
+
latest.set(key, observation);
|
|
31
|
+
}
|
|
32
|
+
return [...latest.values()];
|
|
33
|
+
}
|
|
34
|
+
export function componentState(progress, node, resources = []) {
|
|
35
|
+
if (providerAttention(componentResources(node, resources)))
|
|
36
|
+
return "blocked";
|
|
37
|
+
const observations = latestObservations(componentObservations(progress, node));
|
|
38
|
+
const current = observations.filter((item) => !item.stale);
|
|
39
|
+
if (current.some((item) => item.outcome !== "passed"))
|
|
40
|
+
return "blocked";
|
|
41
|
+
if (current.some((item) => item.milestone === "tested"))
|
|
42
|
+
return "verified";
|
|
43
|
+
if (current.some((item) => ["provisioned", "connected"].includes(item.milestone)))
|
|
44
|
+
return "configured";
|
|
45
|
+
if (current.some((item) => item.milestone === "generated"))
|
|
46
|
+
return "building";
|
|
47
|
+
if (observations.length)
|
|
48
|
+
return "planned";
|
|
49
|
+
return node.status;
|
|
50
|
+
}
|
|
51
|
+
/** A failed optional route does not block a working alternative for the same scope. */
|
|
52
|
+
export function discoveryAttention(discovery) {
|
|
53
|
+
const level = {
|
|
54
|
+
installed: 0,
|
|
55
|
+
callable: 1,
|
|
56
|
+
"local-working": 1,
|
|
57
|
+
authorized: 2,
|
|
58
|
+
"integration-tested": 3,
|
|
59
|
+
};
|
|
60
|
+
const current = discovery.filter((item) => !item.stale);
|
|
61
|
+
return current.filter((item) => item.outcome !== "passed" &&
|
|
62
|
+
!current.some((other) => other.outcome === "passed" &&
|
|
63
|
+
other.provider === item.provider &&
|
|
64
|
+
other.route !== item.route &&
|
|
65
|
+
(!item.accountRef || other.accountRef === item.accountRef) &&
|
|
66
|
+
(item.source !== "independent" || other.source === "independent") &&
|
|
67
|
+
level[other.check] >= level[item.check] &&
|
|
68
|
+
level[other.check] >= 2));
|
|
69
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export declare const PROGRESS_FILE = "anhedral.progress.json";
|
|
2
|
+
export declare const MILESTONES: readonly ["generated", "provisioned", "connected", "tested", "deployed"];
|
|
3
|
+
export type Milestone = typeof MILESTONES[number];
|
|
4
|
+
type Outcome = 'passed' | 'failed' | 'blocked';
|
|
5
|
+
type Source = 'agent' | 'independent';
|
|
6
|
+
interface Evidence {
|
|
7
|
+
outcome: Outcome;
|
|
8
|
+
source: Source;
|
|
9
|
+
observedAt: string;
|
|
10
|
+
evidence: string;
|
|
11
|
+
accountRef?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ProgressObservation extends Evidence {
|
|
14
|
+
capability: string;
|
|
15
|
+
milestone: Milestone;
|
|
16
|
+
configurationFingerprint: string;
|
|
17
|
+
resourceRef?: string;
|
|
18
|
+
revisionRef?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface DiscoveryObservation extends Evidence {
|
|
21
|
+
provider: string;
|
|
22
|
+
route: 'plugin' | 'api' | 'cli' | 'browser';
|
|
23
|
+
check: 'installed' | 'callable' | 'local-working' | 'authorized' | 'integration-tested';
|
|
24
|
+
expiresAt: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ProgressEnvironment {
|
|
27
|
+
id: string;
|
|
28
|
+
observations: ProgressObservation[];
|
|
29
|
+
discovery: DiscoveryObservation[];
|
|
30
|
+
blockers: string[];
|
|
31
|
+
nextAction?: string;
|
|
32
|
+
previewUrl?: string;
|
|
33
|
+
liveUrl?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ProjectProgress {
|
|
36
|
+
schemaVersion: 1;
|
|
37
|
+
revision: number;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
environments: ProgressEnvironment[];
|
|
40
|
+
}
|
|
41
|
+
export interface ProgressReport {
|
|
42
|
+
revision: number;
|
|
43
|
+
environment: string;
|
|
44
|
+
observations?: ProgressObservation[];
|
|
45
|
+
discovery?: DiscoveryObservation[];
|
|
46
|
+
blockers?: string[];
|
|
47
|
+
nextAction?: string | null;
|
|
48
|
+
previewUrl?: string | null;
|
|
49
|
+
liveUrl?: string | null;
|
|
50
|
+
}
|
|
51
|
+
export declare function validateProgressReport(value: unknown): ProgressReport;
|
|
52
|
+
export declare function validateProjectProgress(value: unknown): ProjectProgress;
|
|
53
|
+
export declare function readProjectProgress(root: string): ProjectProgress | null;
|
|
54
|
+
/** Bounded source/configuration digest; secrets, dependencies and generated output are excluded. */
|
|
55
|
+
export declare function projectFingerprint(root: string): string;
|
|
56
|
+
/** Reports are caller assertions. Only actual checking adapters should use source: independent. */
|
|
57
|
+
export declare function reportProjectProgress(root: string, input: ProgressReport): ProjectProgress;
|
|
58
|
+
export declare function evaluateProjectProgress(record: ProjectProgress, fingerprint: string, now?: number): {
|
|
59
|
+
environments: {
|
|
60
|
+
observations: {
|
|
61
|
+
stale: boolean;
|
|
62
|
+
capability: string;
|
|
63
|
+
milestone: Milestone;
|
|
64
|
+
configurationFingerprint: string;
|
|
65
|
+
resourceRef?: string;
|
|
66
|
+
revisionRef?: string;
|
|
67
|
+
outcome: Outcome;
|
|
68
|
+
source: Source;
|
|
69
|
+
observedAt: string;
|
|
70
|
+
evidence: string;
|
|
71
|
+
accountRef?: string;
|
|
72
|
+
}[];
|
|
73
|
+
discovery: {
|
|
74
|
+
stale: boolean;
|
|
75
|
+
provider: string;
|
|
76
|
+
route: "plugin" | "api" | "cli" | "browser";
|
|
77
|
+
check: "installed" | "callable" | "local-working" | "authorized" | "integration-tested";
|
|
78
|
+
expiresAt: string;
|
|
79
|
+
outcome: Outcome;
|
|
80
|
+
source: Source;
|
|
81
|
+
observedAt: string;
|
|
82
|
+
evidence: string;
|
|
83
|
+
accountRef?: string;
|
|
84
|
+
}[];
|
|
85
|
+
id: string;
|
|
86
|
+
blockers: string[];
|
|
87
|
+
nextAction?: string;
|
|
88
|
+
previewUrl?: string;
|
|
89
|
+
liveUrl?: string;
|
|
90
|
+
}[];
|
|
91
|
+
schemaVersion: 1;
|
|
92
|
+
revision: number;
|
|
93
|
+
updatedAt: string;
|
|
94
|
+
};
|
|
95
|
+
export {};
|