anhedral 0.6.1 → 0.7.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/README.md +8 -3
- package/assets/anhedral-cli-init-technical.svg +4 -4
- package/assets/anhedral-cli-init.svg +4 -4
- package/dist/bin.js +27 -8
- package/dist/capabilities.d.ts +54 -0
- package/dist/capabilities.js +90 -0
- package/dist/command.d.ts +5 -0
- package/dist/command.js +16 -0
- package/dist/dependencies.js +5 -14
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/platforms/desktop.js +24 -19
- package/dist/platforms/extension.js +22 -39
- package/dist/platforms/lint.d.ts +1 -0
- package/dist/platforms/lint.js +5 -0
- package/dist/platforms/mobile.js +21 -15
- package/dist/readiness.d.ts +20 -0
- package/dist/readiness.js +61 -0
- package/dist/render.d.ts +0 -1
- package/dist/render.js +1 -1
- package/dist/standard.js +170 -124
- package/dist/transaction.js +238 -195
- package/dist/util.js +4 -2
- package/dist/version.d.ts +1 -2
- package/dist/version.js +1 -2
- package/docs/application-stack-standard.md +13 -10
- package/package.json +5 -4
- /package/{anhedral.svg → assets/anhedral.svg} +0 -0
- /package/{favicon.ico → assets/favicon.ico} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { rmSync } from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
2
|
+
import { writeReactLintConfig } from './lint.js';
|
|
3
3
|
import { writeFile } from '../util.js';
|
|
4
4
|
import { EXTENSION_DEPENDENCIES } from '../dependencies.js';
|
|
5
5
|
import { childPackageName, htmlText, jsString, markdownHeading } from '../render.js';
|
|
@@ -7,7 +7,8 @@ export async function scaffoldExtension(root, options) {
|
|
|
7
7
|
const { projectName, displayName } = 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
|
+
writeReactLintConfig(dir);
|
|
11
12
|
writeWxtConfig(dir, displayName);
|
|
12
13
|
writeEnvExample(dir);
|
|
13
14
|
writePostcssConfig(dir);
|
|
@@ -22,16 +23,6 @@ export async function scaffoldExtension(root, options) {
|
|
|
22
23
|
writeSidepanelApp(dir);
|
|
23
24
|
writeStyles(dir);
|
|
24
25
|
}
|
|
25
|
-
function cleanWxtStarterFiles(dir) {
|
|
26
|
-
for (const relativePath of [
|
|
27
|
-
'.git',
|
|
28
|
-
'entrypoints',
|
|
29
|
-
'assets/react.svg',
|
|
30
|
-
'public/wxt.svg',
|
|
31
|
-
]) {
|
|
32
|
-
rmSync(path.join(dir, relativePath), { recursive: true, force: true });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
26
|
function writePackageJson(dir, projectName) {
|
|
36
27
|
writeFile(path.join(dir, 'package.json'), JSON.stringify({
|
|
37
28
|
name: childPackageName(projectName, 'chrome-ext'),
|
|
@@ -44,9 +35,10 @@ function writePackageJson(dir, projectName) {
|
|
|
44
35
|
postinstall: 'wxt prepare',
|
|
45
36
|
zip: 'wxt zip',
|
|
46
37
|
typecheck: 'tsc --noEmit',
|
|
38
|
+
lint: 'eslint src --max-warnings 0',
|
|
47
39
|
},
|
|
48
40
|
dependencies: EXTENSION_DEPENDENCIES.dependencies,
|
|
49
|
-
devDependencies: EXTENSION_DEPENDENCIES.devDependencies,
|
|
41
|
+
devDependencies: { ...EXTENSION_DEPENDENCIES.devDependencies, '@workspace/eslint-config': 'workspace:*', eslint: '10.0.3' },
|
|
50
42
|
}, null, 2) + '\n');
|
|
51
43
|
}
|
|
52
44
|
function writeWxtConfig(dir, displayName) {
|
|
@@ -68,7 +60,7 @@ export default defineConfig({
|
|
|
68
60
|
version: '0.1.0',
|
|
69
61
|
...(crxPublicKey ? { key: crxPublicKey } : {}),
|
|
70
62
|
minimum_chrome_version: '114',
|
|
71
|
-
permissions:
|
|
63
|
+
permissions: ['activeTab', 'scripting', 'sidePanel'],
|
|
72
64
|
host_permissions: [],
|
|
73
65
|
action: {
|
|
74
66
|
default_title: ${actionTitleLiteral},
|
|
@@ -143,7 +135,7 @@ import { cn } from '@/lib/utils';
|
|
|
143
135
|
|
|
144
136
|
type ButtonVariant = 'default' | 'outline';
|
|
145
137
|
|
|
146
|
-
|
|
138
|
+
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
147
139
|
variant?: ButtonVariant;
|
|
148
140
|
};
|
|
149
141
|
|
|
@@ -225,15 +217,6 @@ module.exports = {
|
|
|
225
217
|
`);
|
|
226
218
|
}
|
|
227
219
|
function writeReadme(dir, displayName) {
|
|
228
|
-
const configuration = [
|
|
229
|
-
null,
|
|
230
|
-
null,
|
|
231
|
-
'Set `VITE_CRX_PUBLIC_KEY` only when you need a stable Chrome extension ID.',
|
|
232
|
-
].filter((value) => value !== null).join(' ');
|
|
233
|
-
const networkIntegrations = [].join(' and ');
|
|
234
|
-
const hostPermissionsGuidance = networkIntegrations
|
|
235
|
-
? `\n\nThe manifest derives deduplicated Chrome host permissions from ${networkIntegrations}. Only absolute HTTP(S) URLs are accepted; malformed configured URLs fail the build. Set production URLs in the extension environment before publishing—no source edit is required.`
|
|
236
|
-
: '';
|
|
237
220
|
writeFile(path.join(dir, 'README.md'), `# ${markdownHeading(displayName)} Chrome Extension
|
|
238
221
|
|
|
239
222
|
WXT side-panel extension generated by anhedral.
|
|
@@ -247,13 +230,13 @@ pnpm build
|
|
|
247
230
|
pnpm zip
|
|
248
231
|
\`\`\`
|
|
249
232
|
|
|
250
|
-
Run these commands from \`apps/extension\`. Anhedral generates only \`.env.example\`; keep the local \`.env\` file uncommitted.
|
|
233
|
+
Run these commands from \`apps/extension\`. Anhedral generates only \`.env.example\`; keep the local \`.env\` file uncommitted. Set \`VITE_CRX_PUBLIC_KEY\` only when you need a stable Chrome extension ID.
|
|
251
234
|
|
|
252
235
|
## Chrome
|
|
253
236
|
|
|
254
237
|
Run \`pnpm build\`, then load \`.output/chrome-mv3\` as an unpacked extension from \`chrome://extensions\`.
|
|
255
238
|
|
|
256
|
-
The extension uses Chrome's Side Panel API. The browser action opens \`sidepanel.html\`, \`wxt.config.ts\` declares the \`sidePanel\` permission and Chrome 114+ minimum version, and active-page reads use a user-triggered \`activeTab\` + \`scripting\` grant instead of persistent site access
|
|
239
|
+
The extension uses Chrome's Side Panel API. The browser action opens \`sidepanel.html\`, \`wxt.config.ts\` declares the \`sidePanel\` permission and Chrome 114+ minimum version, and active-page reads use a user-triggered \`activeTab\` + \`scripting\` grant instead of persistent site access.
|
|
257
240
|
`);
|
|
258
241
|
}
|
|
259
242
|
function writeBackground(dir) {
|
|
@@ -283,7 +266,7 @@ if (container) {
|
|
|
283
266
|
const root = createRoot(container);
|
|
284
267
|
root.render(
|
|
285
268
|
<React.StrictMode>
|
|
286
|
-
|
|
269
|
+
<SidePanelApp />
|
|
287
270
|
</React.StrictMode>
|
|
288
271
|
);
|
|
289
272
|
}
|
|
@@ -313,25 +296,25 @@ type PageSnapshot = {
|
|
|
313
296
|
location: string;
|
|
314
297
|
};
|
|
315
298
|
|
|
299
|
+
async function readPageSnapshot(): Promise<PageSnapshot> {
|
|
300
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
301
|
+
if (!tab?.id) throw new Error('No active tab is available.');
|
|
302
|
+
const [{ result }] = await chrome.scripting.executeScript({
|
|
303
|
+
target: { tabId: tab.id },
|
|
304
|
+
func: () => ({ title: document.title, location: window.location.href }),
|
|
305
|
+
});
|
|
306
|
+
if (!result) throw new Error('The active page did not return a snapshot.');
|
|
307
|
+
return result as PageSnapshot;
|
|
308
|
+
}
|
|
309
|
+
|
|
316
310
|
export function SidePanelApp() {
|
|
317
311
|
const [page, setPage] = React.useState<PageSnapshot | null>(null);
|
|
318
312
|
const [pageError, setPageError] = React.useState<string | null>(null);
|
|
319
313
|
|
|
320
314
|
const readActivePage = React.useCallback(async () => {
|
|
321
315
|
setPageError(null);
|
|
322
|
-
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
323
|
-
if (!tab.id) {
|
|
324
|
-
setPageError('No active tab is available.');
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
316
|
try {
|
|
329
|
-
|
|
330
|
-
target: { tabId: tab.id },
|
|
331
|
-
func: () => ({ title: document.title, location: window.location.href }),
|
|
332
|
-
});
|
|
333
|
-
if (!injection?.result) throw new Error('The active page did not return a snapshot.');
|
|
334
|
-
setPage(injection.result as PageSnapshot);
|
|
317
|
+
setPage(await readPageSnapshot());
|
|
335
318
|
} catch {
|
|
336
319
|
setPageError('Chrome does not allow this page to be inspected. Open a normal website and try again.');
|
|
337
320
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeReactLintConfig(directory: string): void;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { writeFile } from '../util.js';
|
|
3
|
+
export function writeReactLintConfig(directory) {
|
|
4
|
+
writeFile(path.join(directory, 'eslint.config.mjs'), 'import { config } from "@workspace/eslint-config/react-internal";\nexport default [...config, { files: ["**/*.{ts,tsx,cts}"], languageOptions: { parserOptions: { babelOptions: { babelrc: false, configFile: false, parserOpts: { plugins: ["jsx", "typescript"] } } } }, rules: { "no-undef": "off", "no-unused-vars": "off" } }];\n');
|
|
5
|
+
}
|
package/dist/platforms/mobile.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { writeReactLintConfig } from './lint.js';
|
|
2
3
|
import { appendGitignore, writeFile } from '../util.js';
|
|
3
4
|
import { childPackageName, jsString } from '../render.js';
|
|
4
5
|
import { MOBILE_APP_DEPENDENCIES, MOBILE_NATIVEWIND_DEPENDENCIES, EAS_CLI_VERSION, } from '../dependencies.js';
|
|
@@ -29,6 +30,17 @@ import { twMerge } from 'tailwind-merge';
|
|
|
29
30
|
export function cn(...inputs: ClassValue[]) {
|
|
30
31
|
return twMerge(clsx(inputs));
|
|
31
32
|
}
|
|
33
|
+
`);
|
|
34
|
+
writeFile(path.join(dir, 'components/ui/button.tsx'), `import { Pressable, Text, type PressableProps } from 'react-native';
|
|
35
|
+
import { cn } from '@/lib/utils';
|
|
36
|
+
|
|
37
|
+
type ButtonProps = Omit<PressableProps, 'children'> & { children: string; className?: string };
|
|
38
|
+
|
|
39
|
+
export function Button({ children, className, ...props }: ButtonProps) {
|
|
40
|
+
return <Pressable accessibilityRole="button" className={cn('h-10 items-center justify-center rounded-md bg-primary px-4', className)} {...props}>
|
|
41
|
+
<Text className="font-medium text-primary-foreground">{children}</Text>
|
|
42
|
+
</Pressable>;
|
|
43
|
+
}
|
|
32
44
|
`);
|
|
33
45
|
writeFile(path.join(dir, 'babel.config.js'), `module.exports = function (api) {
|
|
34
46
|
api.cache(true);
|
|
@@ -45,6 +57,7 @@ const config = getDefaultConfig(__dirname);
|
|
|
45
57
|
module.exports = withNativeWind(config, { input: './global.css', inlineRem: 16 });
|
|
46
58
|
`);
|
|
47
59
|
writeFile(path.join(dir, 'nativewind-env.d.ts'), '/// <reference types="nativewind/types" />\n');
|
|
60
|
+
writeFile(path.join(dir, 'styles.d.ts'), "declare module '*.css';\n");
|
|
48
61
|
writeFile(path.join(dir, 'tailwind.config.js'), `const { hairlineWidth } = require('nativewind/theme');
|
|
49
62
|
|
|
50
63
|
/** @type {import('tailwindcss').Config} */
|
|
@@ -122,6 +135,7 @@ export async function scaffoldMobile(root, options) {
|
|
|
122
135
|
const dir = path.join(root, 'apps/mobile');
|
|
123
136
|
const { projectName, displayName } = options;
|
|
124
137
|
const nameLiteral = jsString(displayName);
|
|
138
|
+
writeReactLintConfig(dir);
|
|
125
139
|
writeFile(path.join(dir, 'package.json'), JSON.stringify({
|
|
126
140
|
name: childPackageName(projectName, 'mobile'),
|
|
127
141
|
version: '0.1.0',
|
|
@@ -142,9 +156,10 @@ export async function scaffoldMobile(root, options) {
|
|
|
142
156
|
'submit:ios': `pnpm dlx eas-cli@${EAS_CLI_VERSION} submit --platform ios --profile production --latest`,
|
|
143
157
|
'submit:android': `pnpm dlx eas-cli@${EAS_CLI_VERSION} submit --platform android --profile production --latest`,
|
|
144
158
|
typecheck: 'tsc --noEmit',
|
|
159
|
+
lint: 'eslint app components lib --max-warnings 0',
|
|
145
160
|
},
|
|
146
161
|
dependencies: { ...MOBILE_APP_DEPENDENCIES.dependencies, ...MOBILE_NATIVEWIND_DEPENDENCIES.dependencies },
|
|
147
|
-
devDependencies: MOBILE_APP_DEPENDENCIES.devDependencies,
|
|
162
|
+
devDependencies: { ...MOBILE_APP_DEPENDENCIES.devDependencies, '@workspace/eslint-config': 'workspace:*', eslint: '10.0.3' },
|
|
148
163
|
}, null, 2) + '\n');
|
|
149
164
|
writeFile(path.join(dir, 'app.json'), JSON.stringify({
|
|
150
165
|
expo: {
|
|
@@ -153,9 +168,8 @@ export async function scaffoldMobile(root, options) {
|
|
|
153
168
|
version: '1.0.0',
|
|
154
169
|
orientation: 'portrait',
|
|
155
170
|
scheme: expoScheme(projectName),
|
|
156
|
-
userInterfaceStyle: 'automatic',
|
|
157
171
|
web: { bundler: 'metro', output: 'static' },
|
|
158
|
-
plugins: ['expo-router',
|
|
172
|
+
plugins: ['expo-router', 'expo-status-bar'],
|
|
159
173
|
experiments: { reactCompiler: true, typedRoutes: true },
|
|
160
174
|
},
|
|
161
175
|
}, null, 2) + '\n');
|
|
@@ -175,7 +189,6 @@ export async function scaffoldMobile(root, options) {
|
|
|
175
189
|
],
|
|
176
190
|
}, null, 2) + '\n');
|
|
177
191
|
writeReactNativeReusablesConfig(dir);
|
|
178
|
-
const layoutBody = ` return <>${"<Stack />"}<PortalHost /></>;`;
|
|
179
192
|
writeFile(path.join(dir, 'app/_layout.tsx'), `import '@/global.css';
|
|
180
193
|
|
|
181
194
|
import { PortalHost } from '@rn-primitives/portal';
|
|
@@ -183,15 +196,12 @@ import { Stack } from 'expo-router';
|
|
|
183
196
|
|
|
184
197
|
|
|
185
198
|
export default function RootLayout() {
|
|
186
|
-
|
|
199
|
+
return <><Stack /><PortalHost /></>;
|
|
187
200
|
}
|
|
188
201
|
`);
|
|
189
|
-
const homeImports = [].join('\n');
|
|
190
202
|
writeFile(path.join(dir, 'app/index.tsx'), `import { StatusBar } from 'expo-status-bar';
|
|
191
|
-
import {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const enabledModules = [];
|
|
203
|
+
import { Linking, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
204
|
+
import { Button } from '@/components/ui/button';
|
|
195
205
|
|
|
196
206
|
export default function HomeScreen() {
|
|
197
207
|
return (
|
|
@@ -200,7 +210,7 @@ export default function HomeScreen() {
|
|
|
200
210
|
<View style={styles.content}>
|
|
201
211
|
<Text accessibilityRole="header" style={styles.title}>{${nameLiteral}}</Text>
|
|
202
212
|
<Text selectable style={styles.subtitle}>Deterministic Expo application generated by Anhedral.</Text>
|
|
203
|
-
{
|
|
213
|
+
<Button onPress={() => void Linking.openURL('https://docs.expo.dev')}>Learn more</Button>
|
|
204
214
|
</View>
|
|
205
215
|
</ScrollView>
|
|
206
216
|
);
|
|
@@ -210,12 +220,8 @@ const styles = StyleSheet.create({
|
|
|
210
220
|
screen: { flex: 1, backgroundColor: '#f8fafc' },
|
|
211
221
|
container: { flexGrow: 1, justifyContent: 'center', padding: 32 },
|
|
212
222
|
content: { gap: 12 },
|
|
213
|
-
actions: { gap: 8, paddingTop: 8 },
|
|
214
223
|
title: { color: '#0f172a', fontSize: 34, fontWeight: '700' },
|
|
215
224
|
subtitle: { color: '#475569', fontSize: 17, paddingBottom: 12 },
|
|
216
|
-
module: { color: '#1e293b', fontSize: 15 },
|
|
217
|
-
status: { color: '#475569', fontSize: 14 },
|
|
218
|
-
error: { color: '#b91c1c', fontSize: 14 },
|
|
219
225
|
});
|
|
220
226
|
`);
|
|
221
227
|
writeFile(path.join(dir, '.env.example'), '');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type StandardProduct } from './standard-products.js';
|
|
2
|
+
export declare function inspectProject(root: string): {
|
|
3
|
+
schemaVersion: number;
|
|
4
|
+
root: string;
|
|
5
|
+
checks: {
|
|
6
|
+
id: string;
|
|
7
|
+
ok: boolean;
|
|
8
|
+
detail: string;
|
|
9
|
+
}[];
|
|
10
|
+
localReady: boolean;
|
|
11
|
+
providerVerification: string;
|
|
12
|
+
productionReady: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function validateSetupPlan(plan: unknown): asserts plan is {
|
|
15
|
+
schemaVersion: 1;
|
|
16
|
+
cliVersion: string;
|
|
17
|
+
registryVersion: 1;
|
|
18
|
+
products: StandardProduct[];
|
|
19
|
+
hosting: 'cloudflare' | 'vercel';
|
|
20
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { CAPABILITY_REGISTRY, resolveStandardProducts } from './capabilities.js';
|
|
4
|
+
import { STANDARD_PRODUCTS } from './standard-products.js';
|
|
5
|
+
export function inspectProject(root) {
|
|
6
|
+
const checks = [];
|
|
7
|
+
const record = (id, ok, detail) => checks.push({ id, ok, detail });
|
|
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
|
+
record('lockfile', existsSync(path.join(root, 'pnpm-lock.yaml')), 'Committed dependency resolution required');
|
|
11
|
+
try {
|
|
12
|
+
const plan = read('anhedral.setup.json');
|
|
13
|
+
validateSetupPlan(plan);
|
|
14
|
+
const standard = read('anhedral.standard.json');
|
|
15
|
+
record('setup-plan', true, `Requirements recorded by CLI ${plan.cliVersion}`);
|
|
16
|
+
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
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
record('setup-plan', false, 'Missing, invalid or unsupported setup plan; inspect the project before changing it');
|
|
21
|
+
}
|
|
22
|
+
return { schemaVersion: 1, root, checks, localReady: checks.every((check) => check.ok), providerVerification: 'unverified', productionReady: false };
|
|
23
|
+
}
|
|
24
|
+
export function validateSetupPlan(plan) {
|
|
25
|
+
if (!plan || typeof plan !== 'object')
|
|
26
|
+
throw new Error('Invalid setup plan.');
|
|
27
|
+
const value = plan;
|
|
28
|
+
if (value.schemaVersion !== 1 || value.registryVersion !== 1 || typeof value.cliVersion !== 'string')
|
|
29
|
+
throw new Error('Unsupported setup plan version.');
|
|
30
|
+
if (!Array.isArray(value.products) || !value.products.length || !value.products.every((id) => STANDARD_PRODUCTS.includes(id)))
|
|
31
|
+
throw new Error('Unknown setup capabilities.');
|
|
32
|
+
if (value.hosting !== 'cloudflare' && value.hosting !== 'vercel')
|
|
33
|
+
throw new Error('Invalid hosting.');
|
|
34
|
+
validateRequirements(value.setup, value.products);
|
|
35
|
+
const selected = new Set(value.products);
|
|
36
|
+
resolveStandardProducts(selected, value.hosting, false);
|
|
37
|
+
if (selected.size !== value.products.length)
|
|
38
|
+
throw new Error('Invalid or incomplete setup selection.');
|
|
39
|
+
}
|
|
40
|
+
function validateRequirements(setup, products) {
|
|
41
|
+
if (!setup || typeof setup !== 'object')
|
|
42
|
+
throw new Error('Missing setup requirements.');
|
|
43
|
+
const value = setup;
|
|
44
|
+
if (value.resourcesProvisioned !== false || !Array.isArray(value.capabilities) || value.capabilities.length !== products.length)
|
|
45
|
+
throw new Error('Invalid capability requirements.');
|
|
46
|
+
const ids = value.capabilities.map((item) => validateCapability(item));
|
|
47
|
+
if (new Set(ids).size !== products.length || !products.every((id) => ids.includes(id)))
|
|
48
|
+
throw new Error('Capability selection mismatch.');
|
|
49
|
+
}
|
|
50
|
+
function validateCapability(item) {
|
|
51
|
+
if (!item || typeof item !== 'object')
|
|
52
|
+
throw new Error('Invalid capability.');
|
|
53
|
+
const value = item;
|
|
54
|
+
if (typeof value.id !== 'string' || typeof value.purpose !== 'string' || !['planned', 'starter'].includes(String(value.status)))
|
|
55
|
+
throw new Error('Invalid capability metadata.');
|
|
56
|
+
for (const key of ['tools', 'access', 'credentials', 'resources', 'verify']) {
|
|
57
|
+
if (!Array.isArray(value[key]) || !value[key].every((entry) => typeof entry === 'string'))
|
|
58
|
+
throw new Error('Invalid requirement fields.');
|
|
59
|
+
}
|
|
60
|
+
return value.id;
|
|
61
|
+
}
|
package/dist/render.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ export declare function jsString(value: string): string;
|
|
|
4
4
|
export declare function htmlText(value: string): string;
|
|
5
5
|
/** Keep generated Markdown headings on one line and prevent heading/code injection. */
|
|
6
6
|
export declare function markdownHeading(value: string): string;
|
|
7
|
-
export declare function assertPackageName(value: string): string;
|
|
8
7
|
/** Derive a valid, deterministic unscoped npm package name from arbitrary text. */
|
|
9
8
|
export declare function packageNameFromText(value: string): string;
|
|
10
9
|
export declare function childPackageName(projectName: string, suffix: string): string;
|
package/dist/render.js
CHANGED
|
@@ -20,7 +20,7 @@ export function markdownHeading(value) {
|
|
|
20
20
|
const normalized = value.replace(/[\r\n]+/g, ' ').trim() || 'Anhedral App';
|
|
21
21
|
return normalized.replace(/([\\`*_{}\[\]()<>#+.!|~-])/g, '\\$1');
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
function assertPackageName(value) {
|
|
24
24
|
const leafName = value.slice(value.lastIndexOf('/') + 1);
|
|
25
25
|
if (value.length > MAX_PACKAGE_NAME_LENGTH
|
|
26
26
|
|| !PACKAGE_NAME_PATTERN.test(value)
|