acrylblends 0.4.0 → 0.4.1

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.
Files changed (45) hide show
  1. package/dist/index.js +56 -2
  2. package/dist/index.js.map +1 -1
  3. package/docs/framework/LICENSE +23 -0
  4. package/docs/framework/README.md +63 -0
  5. package/docs/framework/UPSTREAM.md +3 -0
  6. package/docs/framework/api-summary.md +138 -0
  7. package/docs/framework/concepts.md +105 -0
  8. package/docs/framework/context.md +129 -0
  9. package/docs/framework/getting-started.md +146 -0
  10. package/docs/framework/helpers.md +118 -0
  11. package/docs/framework/middleware.md +154 -0
  12. package/docs/framework/migration-from-0-3.md +108 -0
  13. package/docs/framework/migration-from-dsh-webserver.md +103 -0
  14. package/docs/framework/node-server.md +75 -0
  15. package/docs/framework/plugin-lifecycle.md +111 -0
  16. package/docs/framework/request.md +106 -0
  17. package/docs/framework/routing.md +188 -0
  18. package/docs/framework/surfaces.md +71 -0
  19. package/docs/framework/upstream.json +9 -0
  20. package/docs/framework/validation.md +135 -0
  21. package/package.json +4 -2
  22. package/plugins/acryl-inspect/src/report.ts +15 -10
  23. package/plugins/acryl-inspect/tests/report.spec.ts +9 -3
  24. package/plugins/acryl-inspect/upstream.json +2 -2
  25. package/plugins/acryl-node-server/upstream.json +1 -1
  26. package/plugins/acryl-router/src/middleware/index.ts +1 -1
  27. package/plugins/acryl-router/src/middleware/loopback-only.ts +34 -1
  28. package/plugins/acryl-router/src/service.ts +9 -3
  29. package/plugins/acryl-router/tests/loopback-only.spec.ts +80 -0
  30. package/plugins/acryl-router/tests/service.spec.ts +17 -0
  31. package/plugins/acryl-router/upstream.json +2 -2
  32. package/plugins/acryl-router-http/upstream.json +1 -1
  33. package/plugins/acryl-state/upstream.json +1 -1
  34. package/plugins/builder/script.ts +1 -1
  35. package/plugins/extensions/index.ts +4 -3
  36. package/plugins/openai/prompt.ts +2 -2
  37. package/shared/AGENTS.core.md +54 -0
  38. package/templates/agent/AGENTS.md +6 -0
  39. package/templates/agent/README.md +2 -2
  40. package/templates/agent/blend.yaml +1 -1
  41. package/templates/blank/AGENTS.md +5 -0
  42. package/templates/blank/README.md +1 -1
  43. package/templates/todo/AGENTS.md +5 -0
  44. package/templates/todo/src/todo.ts +4 -2
  45. package/templates/todo/upstream.json +2 -2
package/dist/index.js CHANGED
@@ -133,7 +133,7 @@ function copyTemplate(source, target, name) {
133
133
  const written = [];
134
134
  const walk = (from, to, prefix) => {
135
135
  for (const entry of readdirSync(from, { withFileTypes: true })) {
136
- if (entry.name === "node_modules" || prefix === "" && (entry.name === "template.json" || entry.name === "upstream.json")) continue;
136
+ if (entry.name === "node_modules" || prefix === "" && (entry.name === "template.json" || entry.name === "upstream.json" || entry.name === "AGENTS.md")) continue;
137
137
  const sourcePath = join(from, entry.name);
138
138
  const targetName = prefix === "" && (entry.name === "gitignore" || entry.name === "dockerignore") ? `.${entry.name}` : entry.name;
139
139
  const targetPath = join(to, targetName);
@@ -151,7 +151,7 @@ function copyTemplate(source, target, name) {
151
151
  walk(source, target, "");
152
152
  return written;
153
153
  }
154
- /** A template's own manifest, `template.json`: the library plugins it is built from. Never copied (nor is `upstream.json`, the record of what was synced from ACRYL). */
154
+ /** A template's own manifest, `template.json`: the library plugins it is built from. Never copied (nor is `upstream.json`, the record of what was synced from ACRYL, nor the template's `AGENTS.md`, which `writeAgentFiles` joins to the shared core). */
155
155
  const TEMPLATE_MANIFEST = "template.json";
156
156
  function readTemplatePlugins(templateDir) {
157
157
  const file = join(templateDir, TEMPLATE_MANIFEST);
@@ -173,6 +173,40 @@ function mergeDependencies(appDir, dependencies) {
173
173
  writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
174
174
  return added;
175
175
  }
176
+ /** The generated app's guide for a coding agent: the shared core, then the template's own part. `CLAUDE.md` is only an import of it. */
177
+ function writeAgentFiles(sharedDir, templateDir, target, name) {
178
+ const core = readFileSync(join(sharedDir, "AGENTS.core.md"), "utf8");
179
+ const own = existsSync(join(templateDir, "AGENTS.md")) ? readFileSync(join(templateDir, "AGENTS.md"), "utf8") : "";
180
+ writeFileSync(join(target, "AGENTS.md"), `${core}${own}`.replaceAll("__APP_NAME__", name));
181
+ writeFileSync(join(target, "CLAUDE.md"), "@AGENTS.md\n");
182
+ return ["AGENTS.md", "CLAUDE.md"];
183
+ }
184
+ /**
185
+ * Copy the framework's docs (a generated copy of ACRYL's `docs/framework`, already checked against its digest) into `<app>/docs/framework/`,
186
+ * so a coding agent working in the app reads the API from the folder it is in. The generated-copy notes and the digest record stay behind.
187
+ *
188
+ * @returns the copied files, relative to `target`
189
+ */
190
+ function copyFrameworkDocs(source, target) {
191
+ const written = [];
192
+ const walk = (from, to, prefix) => {
193
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
194
+ if (entry.name === "upstream.json" || entry.name === "UPSTREAM.md") continue;
195
+ const sourcePath = join(from, entry.name);
196
+ const targetPath = join(to, entry.name);
197
+ if (entry.isDirectory()) {
198
+ mkdirSync(targetPath, { recursive: true });
199
+ walk(sourcePath, targetPath, `${prefix}${entry.name}/`);
200
+ continue;
201
+ }
202
+ cpSync(sourcePath, targetPath);
203
+ written.push(`docs/framework/${prefix}${entry.name}`);
204
+ }
205
+ };
206
+ mkdirSync(join(target, "docs", "framework"), { recursive: true });
207
+ walk(source, join(target, "docs", "framework"), "");
208
+ return written;
209
+ }
176
210
  //#endregion
177
211
  //#region src/templates.ts
178
212
  /** The templates: a blank Cordis app on the router, a todo list built only on the router and state, and the blank app with the offline agent. */
@@ -202,6 +236,24 @@ function templatesRoot() {
202
236
  function templateDir(template) {
203
237
  return join(templatesRoot(), template);
204
238
  }
239
+ /** The text every generated app's `AGENTS.md` starts with (`shared/`, beside `templates/`). */
240
+ function sharedDir() {
241
+ return join(dirname(templatesRoot()), "shared");
242
+ }
243
+ /**
244
+ * The framework's docs, as ACRYL wrote them: `docs/framework` of the repository, or the copy `prepack` puts inside the package. The module sits at
245
+ * different depths in source and in the build, so walk up until a `docs/framework` folder holds the README.
246
+ */
247
+ function frameworkDocsRoot() {
248
+ let dir = dirname(fileURLToPath(import.meta.url));
249
+ for (;;) {
250
+ const candidate = join(dir, "docs", "framework");
251
+ if (existsSync(join(candidate, "README.md"))) return candidate;
252
+ const parent = dirname(dir);
253
+ if (parent === dir) throw new Error("acrylblends cannot find the framework docs (docs/framework)");
254
+ dir = parent;
255
+ }
256
+ }
205
257
  //#endregion
206
258
  //#region src/init.ts
207
259
  /** A command line mistake. The CLI prints it with the help text and exits 2. */
@@ -266,6 +318,8 @@ async function init(options, io) {
266
318
  let files;
267
319
  try {
268
320
  files = copyTemplate(templateDir(template), directory, name);
321
+ files.push(...writeAgentFiles(sharedDir(), templateDir(template), directory, name));
322
+ files.push(...copyFrameworkDocs(frameworkDocsRoot(), directory));
269
323
  } catch (error) {
270
324
  if (!(error instanceof TargetNotEmptyError)) throw error;
271
325
  io.stderr.write(`error: ${error.message}\n`);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["parse"],"sources":["../src/library.ts","../src/project.ts","../src/templates.ts","../src/init.ts","../src/add.ts","../src/io.ts","../src/cli.ts"],"sourcesContent":["import { cpSync, existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/** One entry of the plugin library: `plugins/<name>/plugin.json` in this repository. */\nexport interface LibraryPlugin {\n readonly name: string\n readonly description: string\n /** Library plugins this one imports; they are copied with it. */\n readonly requires: readonly string[]\n /** npm packages an app needs at run time for this plugin. */\n readonly dependencies: Readonly<Record<string, string>>\n /** How to mount it, shown after `acrylblends add`. */\n readonly mount?: string\n /**\n * Paths inside the plugin folder that are not copied into an app (for example a React hook an app without React cannot type-check, or colocated test\n * files). A pattern is an exact path or a glob: `*` matches within one folder name, `**` across folders, `?` one character; `**` followed by a slash also matches no folder.\n */\n readonly omit?: readonly string[]\n}\n\n/** A glob as a regular expression: `*` within a name, `**` across folders, `**` plus a slash may match no folder, `?` one character. */\nfunction globToRegExp(glob: string): RegExp {\n let source = ''\n for (let i = 0; i < glob.length; i += 1) {\n const c = glob[i] as string\n if (c === '*' && glob[i + 1] === '*') {\n if (glob[i + 2] === '/') {\n source += '(?:.*/)?'\n i += 2\n } else {\n source += '.*'\n i += 1\n }\n } else if (c === '*') source += '[^/]*'\n else if (c === '?') source += '[^/]'\n else source += c.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${source}$`)\n}\n\n/** Whether a path inside a plugin folder (relative, any separator) is named by one of its `omit` patterns. */\nexport function isOmitted(inside: string, patterns: readonly string[] | undefined): boolean {\n if (!patterns || patterns.length === 0) return false\n const path = inside.split(sep).join('/')\n return patterns.some((pattern) =>\n /[*?]/.test(pattern) ? globToRegExp(pattern).test(path) : pattern === path,\n )\n}\n\n/** Files of a library plugin that stay in the repository: its manifest and its tests. */\nconst LIBRARY_ONLY = new Set(['plugin.json', 'test', 'tests'])\n\nexport class LibraryError extends Error {\n override name = 'LibraryError'\n}\n\n/** The library folder (`plugins/` at the repository root): walk up from this module, in source or in the build. */\nexport function libraryRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'plugins')\n if (existsSync(join(candidate, 'instance', 'plugin.json'))) return candidate\n const parent = dirname(dir)\n if (parent === dir)\n throw new LibraryError('acrylblends cannot find its plugin library (plugins/)')\n dir = parent\n }\n}\n\n/** Every plugin in the library, by name. */\nexport function readLibrary(root: string = libraryRoot()): Map<string, LibraryPlugin> {\n const plugins = new Map<string, LibraryPlugin>()\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n const manifest = join(root, entry.name, 'plugin.json')\n if (!entry.isDirectory() || !existsSync(manifest)) continue\n const plugin = JSON.parse(readFileSync(manifest, 'utf8')) as LibraryPlugin\n if (plugin.name !== entry.name)\n throw new LibraryError(`plugins/${entry.name}/plugin.json names \"${plugin.name}\"`)\n plugins.set(plugin.name, {\n ...plugin,\n requires: plugin.requires ?? [],\n dependencies: plugin.dependencies ?? {},\n })\n }\n return plugins\n}\n\n/** The plugins to copy for `names`: each one after everything it requires, no duplicates. */\nexport function closure(\n names: readonly string[],\n library: Map<string, LibraryPlugin>,\n): LibraryPlugin[] {\n const ordered: LibraryPlugin[] = []\n const visiting = new Set<string>()\n const visit = (name: string, trail: string[]) => {\n if (ordered.some((plugin) => plugin.name === name)) return\n const plugin = library.get(name)\n if (!plugin)\n throw new LibraryError(\n `no plugin \"${name}\" in the library${trail.length ? ` (required by ${trail.at(-1)})` : ''}`,\n )\n if (visiting.has(name))\n throw new LibraryError(\n `plugins require each other in a cycle: ${[...trail, name].join(' -> ')}`,\n )\n visiting.add(name)\n for (const required of plugin.requires) visit(required, [...trail, name])\n visiting.delete(name)\n ordered.push(plugin)\n }\n for (const name of names) visit(name, [])\n return ordered\n}\n\n/** Copy one library plugin's source into `<app>/src/plugins/<name>/`. Throws when that folder already exists. */\nexport function copyPlugin(\n plugin: LibraryPlugin,\n appDir: string,\n root: string = libraryRoot(),\n): string {\n const target = join(appDir, 'src', 'plugins', plugin.name)\n if (existsSync(target))\n throw new LibraryError(\n `src/plugins/${plugin.name} already exists; remove it first to copy a fresh one`,\n )\n const source = join(root, plugin.name)\n cpSync(source, target, {\n recursive: true,\n filter: (path) => {\n const inside = path.slice(source.length + 1)\n return (\n !(dirname(path) === source && LIBRARY_ONLY.has(inside)) && !isOmitted(inside, plugin.omit)\n )\n },\n })\n return target\n}\n","import {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from 'node:fs'\nimport { join } from 'node:path'\n\n/** Thrown when the target directory exists and is not empty. Nothing has been written. */\nexport class TargetNotEmptyError extends Error {\n constructor(readonly target: string) {\n super(\n `refusing to write into \"${target}\": the directory is not empty. Choose an empty or new directory.`,\n )\n this.name = 'TargetNotEmptyError'\n }\n}\n\n/** A project name npm accepts: lower case letters, digits and dashes, starting with a letter. */\nexport function isValidName(name: string): boolean {\n return /^[a-z][a-z0-9-]*$/.test(name) && name.length <= 214\n}\n\n/** A valid default project name derived from a directory path. */\nexport function nameFrom(directory: string): string {\n const base = directory.split(/[\\\\/]/).filter(Boolean).pop() ?? ''\n const cleaned = base\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/^[^a-z]+/, '')\n .replace(/-+$/, '')\n return cleaned || 'my-app'\n}\n\n/** Refuse a target that exists and is not an empty directory. A missing target is fine: it is created. */\nexport function assertTargetFree(target: string): void {\n if (!existsSync(target)) return\n if (!statSync(target).isDirectory() || readdirSync(target).length > 0)\n throw new TargetNotEmptyError(target)\n}\n\n/**\n * Copy a template into `target`, replacing `__APP_NAME__` with `name`. Checks the target before writing anything.\n *\n * @returns the copied files, relative to `target`\n */\nexport function copyTemplate(source: string, target: string, name: string): string[] {\n assertTargetFree(target)\n mkdirSync(target, { recursive: true })\n const written: string[] = []\n const walk = (from: string, to: string, prefix: string) => {\n for (const entry of readdirSync(from, { withFileTypes: true })) {\n if (\n entry.name === 'node_modules' ||\n (prefix === '' && (entry.name === TEMPLATE_MANIFEST || entry.name === 'upstream.json'))\n )\n continue\n const sourcePath = join(from, entry.name)\n // npm and git treat a file named .gitignore specially, so templates keep it as `gitignore` (and `dockerignore`).\n const targetName =\n prefix === '' && (entry.name === 'gitignore' || entry.name === 'dockerignore')\n ? `.${entry.name}`\n : entry.name\n const targetPath = join(to, targetName)\n if (entry.isDirectory()) {\n mkdirSync(targetPath, { recursive: true })\n walk(sourcePath, targetPath, `${prefix}${entry.name}/`)\n continue\n }\n const text = readFileSync(sourcePath, 'utf8')\n if (text.includes('__APP_NAME__'))\n writeFileSync(targetPath, text.replaceAll('__APP_NAME__', name))\n else cpSync(sourcePath, targetPath)\n written.push(`${prefix}${targetName}`)\n }\n }\n walk(source, target, '')\n return written\n}\n\n/** A template's own manifest, `template.json`: the library plugins it is built from. Never copied (nor is `upstream.json`, the record of what was synced from ACRYL). */\nexport const TEMPLATE_MANIFEST = 'template.json'\n\nexport function readTemplatePlugins(templateDir: string): string[] {\n const file = join(templateDir, TEMPLATE_MANIFEST)\n if (!existsSync(file)) return []\n const { plugins } = JSON.parse(readFileSync(file, 'utf8')) as { plugins?: string[] }\n return plugins ?? []\n}\n\n/** Add run-time dependencies to an app's package.json, keeping what is there and sorting the keys. Returns the names added. */\nexport function mergeDependencies(\n appDir: string,\n dependencies: Readonly<Record<string, string>>,\n): string[] {\n const file = join(appDir, 'package.json')\n const manifest = JSON.parse(readFileSync(file, 'utf8')) as {\n dependencies?: Record<string, string>\n }\n const current = manifest.dependencies ?? {}\n const added = Object.keys(dependencies).filter((name) => !(name in current))\n const merged = { ...dependencies, ...current }\n manifest.dependencies = Object.fromEntries(\n Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)),\n )\n writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\\n`)\n return added\n}\n","import { existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/** The templates: a blank Cordis app on the router, a todo list built only on the router and state, and the blank app with the offline agent. */\nexport const TEMPLATES = ['blank', 'todo', 'agent'] as const\nexport type Template = (typeof TEMPLATES)[number]\n\nexport function isTemplate(value: string): value is Template {\n return (TEMPLATES as readonly string[]).includes(value)\n}\n\n/**\n * The `templates/` folder of this package. The module sits at different depths in source (`src/`) and in the build\n * (`dist/`), so walk up until a `templates` folder holds the `blank` template.\n */\nexport function templatesRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'templates')\n if (existsSync(join(candidate, 'blank'))) return candidate\n const parent = dirname(dir)\n if (parent === dir) throw new Error('acrylblends cannot find its templates folder')\n dir = parent\n }\n}\n\n/** Absolute path of a template folder. */\nexport function templateDir(template: Template): string {\n return join(templatesRoot(), template)\n}\n","import { resolve } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport type { Io } from './io'\nimport { closure, copyPlugin, readLibrary } from './library'\nimport {\n copyTemplate,\n isValidName,\n mergeDependencies,\n nameFrom,\n readTemplatePlugins,\n TargetNotEmptyError,\n} from './project'\nimport { isTemplate, TEMPLATES, type Template, templateDir } from './templates'\n\n/** A command line mistake. The CLI prints it with the help text and exits 2. */\nexport class UsageError extends Error {\n override name = 'UsageError'\n}\n\n/** What `init` was asked for. Anything left undefined is asked for on a terminal, or defaulted. */\nexport interface InitOptions {\n directory: string | undefined\n name: string | undefined\n template: Template | undefined\n install: boolean | undefined\n yes: boolean\n}\n\n/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */\nexport function parseInitOptions(argv: readonly string[]): InitOptions {\n let parsed: ReturnType<typeof parse>\n try {\n parsed = parse(argv)\n } catch (error) {\n throw new UsageError((error as Error).message)\n }\n const { values, positionals } = parsed\n if (positionals.length > 1) throw new UsageError('init takes at most one directory')\n if (values.agent && values['no-agent'])\n throw new UsageError('use --agent or --no-agent, not both')\n if (values.template !== undefined && !isTemplate(values.template))\n throw new UsageError(`unknown template \"${values.template}\" (${TEMPLATES.join(', ')})`)\n const alias: Template | undefined = values.agent\n ? 'agent'\n : values['no-agent']\n ? 'blank'\n : undefined\n if (values.template !== undefined && alias !== undefined && values.template !== alias)\n throw new UsageError(\n `--template ${values.template} contradicts --${alias === 'agent' ? 'agent' : 'no-agent'}`,\n )\n return {\n directory: positionals[0],\n name: values.name,\n template: (values.template as Template | undefined) ?? alias,\n install: values.install,\n yes: values.yes === true,\n }\n}\n\nconst parse = (argv: readonly string[]) =>\n parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: {\n name: { type: 'string' },\n template: { type: 'string' },\n agent: { type: 'boolean' },\n 'no-agent': { type: 'boolean' },\n install: { type: 'boolean' },\n yes: { type: 'boolean', short: 'y' },\n },\n })\n\nconst yes = (answer: string) => ['y', 'yes'].includes(answer.trim().toLowerCase())\n\nasync function askTemplate(ask: (question: string) => Promise<string>): Promise<Template> {\n const answer = (await ask(`Template (${TEMPLATES.join(', ')}) [blank]: `)).trim().toLowerCase()\n if (answer === '') return 'blank'\n if (isTemplate(answer)) return answer\n throw new UsageError(`unknown template \"${answer}\" (${TEMPLATES.join(', ')})`)\n}\n\n/** Scaffold a Cordis app. Returns the exit code: 0 done, 1 refused or failed. Never overwrites anything. */\nexport async function init(options: InitOptions, io: Io): Promise<number> {\n const relative = options.directory ?? '.'\n const directory = resolve(io.cwd, relative)\n const ask = options.yes ? undefined : io.ask\n\n let name = options.name ?? nameFrom(directory)\n if (options.name === undefined && ask)\n name = (await ask(`Project name (${name}): `)).trim() || name\n if (!isValidName(name)) {\n throw new UsageError(\n `\"${name}\" is not a valid project name (lower case letters, digits and dashes, starting with a letter)`,\n )\n }\n const template = options.template ?? (ask ? await askTemplate(ask) : 'blank')\n const install =\n options.install ?? (ask ? yes(await ask('Install dependencies with npm now? (y/N): ')) : false)\n\n let files: string[]\n try {\n files = copyTemplate(templateDir(template), directory, name)\n } catch (error) {\n if (!(error instanceof TargetNotEmptyError)) throw error\n io.stderr.write(`error: ${error.message}\\n`)\n return 1\n }\n const plugins = closure(readTemplatePlugins(templateDir(template)), readLibrary())\n for (const plugin of plugins) {\n copyPlugin(plugin, directory)\n mergeDependencies(directory, plugin.dependencies)\n }\n io.stdout.write(\n `created ${name} (${template}) in ${directory}: ${files.length} files, plugins ${plugins.map((plugin) => plugin.name).join(', ')}\\n`,\n )\n\n if (install && (await io.install(directory)) !== 0) {\n io.stderr.write('error: npm install failed; run it yourself in the new directory\\n')\n return 1\n }\n const steps = [\n relative === '.' ? '' : ` cd ${relative}\\n`,\n install ? '' : ' npm install\\n',\n ' npm run dev\\n npm test\\n',\n ]\n io.stdout.write(`\\nNext:\\n${steps.join('')}`)\n return 0\n}\n","import { cpSync, existsSync, readFileSync, statSync } from 'node:fs'\nimport { basename, join, resolve } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { UsageError } from './init'\nimport type { Io } from './io'\nimport { closure, copyPlugin, type LibraryPlugin, readLibrary } from './library'\nimport { mergeDependencies } from './project'\n\nexport interface AddOptions {\n readonly names: readonly string[]\n readonly directory: string | undefined\n}\n\n/** Parse `add <plugin...> [--dir <app>]`. */\nexport function parseAddOptions(argv: readonly string[]): AddOptions {\n let parsed: ReturnType<typeof parse>\n try {\n parsed = parse(argv)\n } catch (error) {\n throw new UsageError((error as Error).message)\n }\n if (parsed.positionals.length === 0)\n throw new UsageError('add needs at least one plugin name (see \"acrylblends plugins\")')\n return { names: parsed.positionals, directory: parsed.values.dir }\n}\n\nconst parse = (argv: readonly string[]) =>\n parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: { dir: { type: 'string' } },\n })\n\nconst present = (appDir: string, name: string) => existsSync(join(appDir, 'src', 'plugins', name))\n\n/**\n * Copy library plugins into an existing app, with the plugins they require, and add their npm packages to its package.json.\n * Refuses a plugin the app already has. Returns the exit code.\n */\nexport async function add(options: AddOptions, io: Io): Promise<number> {\n const appDir = resolve(io.cwd, options.directory ?? '.')\n if (!existsSync(join(appDir, 'package.json')) || !existsSync(join(appDir, 'src'))) {\n io.stderr.write(\n `error: ${appDir} is not an app (no package.json and src/); run \"acrylblends init\" first\\n`,\n )\n return 1\n }\n // A path is an external plugin folder; anything else is a library plugin name.\n const external = options.names.filter((name) => /[\\\\/]/.test(name) || name.startsWith('.'))\n for (const folder of external) {\n const code = addExternal(resolve(io.cwd, folder), appDir, io)\n if (code !== 0) return code\n }\n const names = options.names.filter((name) => !external.includes(name))\n if (names.length === 0) return 0\n const already = names.filter((name) => present(appDir, name))\n if (already.length > 0) {\n io.stderr.write(\n `error: the app already has ${already.map((name) => `src/plugins/${name}`).join(', ')}\\n`,\n )\n return 1\n }\n const toCopy: LibraryPlugin[] = closure(names, readLibrary()).filter(\n (plugin) => !present(appDir, plugin.name),\n )\n const addedDependencies: string[] = []\n for (const plugin of toCopy) {\n copyPlugin(plugin, appDir)\n addedDependencies.push(...mergeDependencies(appDir, plugin.dependencies))\n }\n io.stdout.write(`added ${toCopy.map((plugin) => plugin.name).join(', ')} to ${appDir}\\n`)\n if (addedDependencies.length > 0)\n io.stdout.write(`new dependencies: ${addedDependencies.join(', ')} (run npm install)\\n`)\n for (const plugin of toCopy.filter((plugin) => names.includes(plugin.name))) {\n io.stdout.write(\n `\\n${plugin.name}: ${plugin.mount ?? `import it from './plugins/${plugin.name}/index.ts' and mount it in src/app.ts`}\\n`,\n )\n }\n return 0\n}\n\nconst PLUGIN_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/\n\n/**\n * Bring an external plugin folder (a package made with the builder's package_plugin, or any folder with an index.ts that exports a\n * Cordis plugin) into the app's `extensions/`, where the extensions plugin installs it like any other: checked, staged, started,\n * verified. Its runtime dependencies are not installed here; they are reported so the user adds them deliberately.\n */\nfunction addExternal(folder: string, appDir: string, io: Io): number {\n if (!existsSync(folder) || !statSync(folder).isDirectory()) {\n io.stderr.write(`error: ${folder} is not a folder\\n`)\n return 1\n }\n if (!['index.ts', 'index.js', 'index.mjs'].some((entry) => existsSync(join(folder, entry)))) {\n io.stderr.write(`error: ${folder} has no index.ts that exports a Cordis plugin\\n`)\n return 1\n }\n const manifestFile = join(folder, 'package.json')\n const manifest = existsSync(manifestFile)\n ? (JSON.parse(readFileSync(manifestFile, 'utf8')) as {\n name?: string\n dependencies?: Record<string, string>\n })\n : {}\n const name = (manifest.name ?? basename(folder)).replace(/^@[^/]+\\//, '')\n if (!PLUGIN_NAME.test(name)) {\n io.stderr.write(\n `error: \"${name}\" is not a plugin name (lowercase letters, digits and dashes)\\n`,\n )\n return 1\n }\n const target = join(appDir, 'extensions', name)\n if (existsSync(target)) {\n io.stderr.write(`error: the app already has extensions/${name}\\n`)\n return 1\n }\n cpSync(folder, target, {\n recursive: true,\n filter: (path) => !/[\\\\/](node_modules|\\.git)([\\\\/]|$)/.test(path.slice(folder.length)),\n })\n io.stdout.write(\n `added external plugin ${name} to ${join(appDir, 'extensions')}; it installs at the next start\\n`,\n )\n const dependencies = Object.keys(manifest.dependencies ?? {})\n if (dependencies.length > 0)\n io.stdout.write(\n `it uses ${dependencies.join(', ')}: add them to the app with npm install first\\n`,\n )\n return 0\n}\n\n/** `acrylblends plugins`: the library, one line per plugin. */\nexport function listPlugins(io: Io): number {\n for (const plugin of readLibrary().values()) {\n const requires = plugin.requires.length > 0 ? ` (requires ${plugin.requires.join(', ')})` : ''\n io.stdout.write(`${plugin.name.padEnd(12)} ${plugin.description}${requires}\\n`)\n }\n return 0\n}\n","import { spawn } from 'node:child_process'\nimport { createInterface } from 'node:readline/promises'\n\n/** Everything the CLI touches outside the filesystem. Tests pass their own. */\nexport interface Io {\n stdout: { write(text: string): unknown }\n stderr: { write(text: string): unknown }\n cwd: string\n /** Ask one question; `undefined` when there is no terminal to ask on. */\n ask: ((question: string) => Promise<string>) | undefined\n /** Install dependencies in a directory; resolves to the exit code. */\n install(directory: string): Promise<number>\n}\n\n/** The real terminal and `npm install`. */\nexport function processIo(): Io {\n const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY)\n return {\n stdout: process.stdout,\n stderr: process.stderr,\n cwd: process.cwd(),\n ask: interactive\n ? async (question) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout })\n try {\n return await rl.question(question)\n } finally {\n rl.close()\n }\n }\n : undefined,\n install: (directory) =>\n new Promise((done) => {\n const child = spawn('npm', ['install'], {\n cwd: directory,\n stdio: 'inherit',\n shell: process.platform === 'win32',\n })\n child.on('error', () => done(1))\n child.on('exit', (code) => done(code ?? 1))\n }),\n }\n}\n","import { createRequire } from 'node:module'\nimport { add, listPlugins, parseAddOptions } from './add'\nimport { init, parseInitOptions, UsageError } from './init'\nimport { type Io, processIo } from './io'\n\n/** The version of this package. */\nexport const VERSION: string = (\n createRequire(import.meta.url)('../package.json') as { version: string }\n).version\n\nexport const HELP = `acrylblends ${VERSION} - scaffold Cordis apps\n\nUsage:\n acrylblends init [dir] [options] Create a Cordis app in an empty or new directory\n acrylblends add <plugin...> [--dir d] Copy library plugins (and what they require) into an app;\n a folder path brings an external plugin into its extensions/\n acrylblends plugins List the plugin library\n\nOptions for init:\n --template <name> blank or agent (asked when not given); --agent means --template agent,\n --no-agent means --template blank\n --name <name> the project name (default: the directory name)\n --install run npm install after copying\n -y, --yes ask nothing; use defaults (the blank template, no install)\n\n -h, --help show this help\n -v, --version show the version\n`\n\n/**\n * Run the CLI and return the exit code: 0 done, 1 failed or refused, 2 a usage error. Never calls `process.exit`.\n */\nexport async function runCli(argv: readonly string[], io: Io = processIo()): Promise<number> {\n const [command, ...args] = argv\n try {\n if (command === undefined || command === 'help' || command === '-h' || command === '--help') {\n io.stdout.write(HELP)\n return command === undefined ? 2 : 0\n }\n if (command === 'version' || command === '-v' || command === '--version') {\n io.stdout.write(`${VERSION}\\n`)\n return 0\n }\n if (command === 'init') return await init(parseInitOptions(args), io)\n if (command === 'add') return await add(parseAddOptions(args), io)\n if (command === 'plugins') return listPlugins(io)\n throw new UsageError(`unknown command \"${command}\"`)\n } catch (error) {\n if (error instanceof UsageError) {\n io.stderr.write(`error: ${error.message}\\n\\n${HELP}`)\n return 2\n }\n io.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["parse"],"sources":["../src/library.ts","../src/project.ts","../src/templates.ts","../src/init.ts","../src/add.ts","../src/io.ts","../src/cli.ts"],"sourcesContent":["import { cpSync, existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/** One entry of the plugin library: `plugins/<name>/plugin.json` in this repository. */\nexport interface LibraryPlugin {\n readonly name: string\n readonly description: string\n /** Library plugins this one imports; they are copied with it. */\n readonly requires: readonly string[]\n /** npm packages an app needs at run time for this plugin. */\n readonly dependencies: Readonly<Record<string, string>>\n /** How to mount it, shown after `acrylblends add`. */\n readonly mount?: string\n /**\n * Paths inside the plugin folder that are not copied into an app (for example a React hook an app without React cannot type-check, or colocated test\n * files). A pattern is an exact path or a glob: `*` matches within one folder name, `**` across folders, `?` one character; `**` followed by a slash also matches no folder.\n */\n readonly omit?: readonly string[]\n}\n\n/** A glob as a regular expression: `*` within a name, `**` across folders, `**` plus a slash may match no folder, `?` one character. */\nfunction globToRegExp(glob: string): RegExp {\n let source = ''\n for (let i = 0; i < glob.length; i += 1) {\n const c = glob[i] as string\n if (c === '*' && glob[i + 1] === '*') {\n if (glob[i + 2] === '/') {\n source += '(?:.*/)?'\n i += 2\n } else {\n source += '.*'\n i += 1\n }\n } else if (c === '*') source += '[^/]*'\n else if (c === '?') source += '[^/]'\n else source += c.replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${source}$`)\n}\n\n/** Whether a path inside a plugin folder (relative, any separator) is named by one of its `omit` patterns. */\nexport function isOmitted(inside: string, patterns: readonly string[] | undefined): boolean {\n if (!patterns || patterns.length === 0) return false\n const path = inside.split(sep).join('/')\n return patterns.some((pattern) =>\n /[*?]/.test(pattern) ? globToRegExp(pattern).test(path) : pattern === path,\n )\n}\n\n/** Files of a library plugin that stay in the repository: its manifest and its tests. */\nconst LIBRARY_ONLY = new Set(['plugin.json', 'test', 'tests'])\n\nexport class LibraryError extends Error {\n override name = 'LibraryError'\n}\n\n/** The library folder (`plugins/` at the repository root): walk up from this module, in source or in the build. */\nexport function libraryRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'plugins')\n if (existsSync(join(candidate, 'instance', 'plugin.json'))) return candidate\n const parent = dirname(dir)\n if (parent === dir)\n throw new LibraryError('acrylblends cannot find its plugin library (plugins/)')\n dir = parent\n }\n}\n\n/** Every plugin in the library, by name. */\nexport function readLibrary(root: string = libraryRoot()): Map<string, LibraryPlugin> {\n const plugins = new Map<string, LibraryPlugin>()\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n const manifest = join(root, entry.name, 'plugin.json')\n if (!entry.isDirectory() || !existsSync(manifest)) continue\n const plugin = JSON.parse(readFileSync(manifest, 'utf8')) as LibraryPlugin\n if (plugin.name !== entry.name)\n throw new LibraryError(`plugins/${entry.name}/plugin.json names \"${plugin.name}\"`)\n plugins.set(plugin.name, {\n ...plugin,\n requires: plugin.requires ?? [],\n dependencies: plugin.dependencies ?? {},\n })\n }\n return plugins\n}\n\n/** The plugins to copy for `names`: each one after everything it requires, no duplicates. */\nexport function closure(\n names: readonly string[],\n library: Map<string, LibraryPlugin>,\n): LibraryPlugin[] {\n const ordered: LibraryPlugin[] = []\n const visiting = new Set<string>()\n const visit = (name: string, trail: string[]) => {\n if (ordered.some((plugin) => plugin.name === name)) return\n const plugin = library.get(name)\n if (!plugin)\n throw new LibraryError(\n `no plugin \"${name}\" in the library${trail.length ? ` (required by ${trail.at(-1)})` : ''}`,\n )\n if (visiting.has(name))\n throw new LibraryError(\n `plugins require each other in a cycle: ${[...trail, name].join(' -> ')}`,\n )\n visiting.add(name)\n for (const required of plugin.requires) visit(required, [...trail, name])\n visiting.delete(name)\n ordered.push(plugin)\n }\n for (const name of names) visit(name, [])\n return ordered\n}\n\n/** Copy one library plugin's source into `<app>/src/plugins/<name>/`. Throws when that folder already exists. */\nexport function copyPlugin(\n plugin: LibraryPlugin,\n appDir: string,\n root: string = libraryRoot(),\n): string {\n const target = join(appDir, 'src', 'plugins', plugin.name)\n if (existsSync(target))\n throw new LibraryError(\n `src/plugins/${plugin.name} already exists; remove it first to copy a fresh one`,\n )\n const source = join(root, plugin.name)\n cpSync(source, target, {\n recursive: true,\n filter: (path) => {\n const inside = path.slice(source.length + 1)\n return (\n !(dirname(path) === source && LIBRARY_ONLY.has(inside)) && !isOmitted(inside, plugin.omit)\n )\n },\n })\n return target\n}\n","import {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from 'node:fs'\nimport { join } from 'node:path'\n\n/** Thrown when the target directory exists and is not empty. Nothing has been written. */\nexport class TargetNotEmptyError extends Error {\n constructor(readonly target: string) {\n super(\n `refusing to write into \"${target}\": the directory is not empty. Choose an empty or new directory.`,\n )\n this.name = 'TargetNotEmptyError'\n }\n}\n\n/** A project name npm accepts: lower case letters, digits and dashes, starting with a letter. */\nexport function isValidName(name: string): boolean {\n return /^[a-z][a-z0-9-]*$/.test(name) && name.length <= 214\n}\n\n/** A valid default project name derived from a directory path. */\nexport function nameFrom(directory: string): string {\n const base = directory.split(/[\\\\/]/).filter(Boolean).pop() ?? ''\n const cleaned = base\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/^[^a-z]+/, '')\n .replace(/-+$/, '')\n return cleaned || 'my-app'\n}\n\n/** Refuse a target that exists and is not an empty directory. A missing target is fine: it is created. */\nexport function assertTargetFree(target: string): void {\n if (!existsSync(target)) return\n if (!statSync(target).isDirectory() || readdirSync(target).length > 0)\n throw new TargetNotEmptyError(target)\n}\n\n/**\n * Copy a template into `target`, replacing `__APP_NAME__` with `name`. Checks the target before writing anything.\n *\n * @returns the copied files, relative to `target`\n */\nexport function copyTemplate(source: string, target: string, name: string): string[] {\n assertTargetFree(target)\n mkdirSync(target, { recursive: true })\n const written: string[] = []\n const walk = (from: string, to: string, prefix: string) => {\n for (const entry of readdirSync(from, { withFileTypes: true })) {\n if (\n entry.name === 'node_modules' ||\n (prefix === '' &&\n (entry.name === TEMPLATE_MANIFEST ||\n entry.name === 'upstream.json' ||\n entry.name === 'AGENTS.md'))\n )\n continue\n const sourcePath = join(from, entry.name)\n // npm and git treat a file named .gitignore specially, so templates keep it as `gitignore` (and `dockerignore`).\n const targetName =\n prefix === '' && (entry.name === 'gitignore' || entry.name === 'dockerignore')\n ? `.${entry.name}`\n : entry.name\n const targetPath = join(to, targetName)\n if (entry.isDirectory()) {\n mkdirSync(targetPath, { recursive: true })\n walk(sourcePath, targetPath, `${prefix}${entry.name}/`)\n continue\n }\n const text = readFileSync(sourcePath, 'utf8')\n if (text.includes('__APP_NAME__'))\n writeFileSync(targetPath, text.replaceAll('__APP_NAME__', name))\n else cpSync(sourcePath, targetPath)\n written.push(`${prefix}${targetName}`)\n }\n }\n walk(source, target, '')\n return written\n}\n\n/** A template's own manifest, `template.json`: the library plugins it is built from. Never copied (nor is `upstream.json`, the record of what was synced from ACRYL, nor the template's `AGENTS.md`, which `writeAgentFiles` joins to the shared core). */\nexport const TEMPLATE_MANIFEST = 'template.json'\n\nexport function readTemplatePlugins(templateDir: string): string[] {\n const file = join(templateDir, TEMPLATE_MANIFEST)\n if (!existsSync(file)) return []\n const { plugins } = JSON.parse(readFileSync(file, 'utf8')) as { plugins?: string[] }\n return plugins ?? []\n}\n\n/** Add run-time dependencies to an app's package.json, keeping what is there and sorting the keys. Returns the names added. */\nexport function mergeDependencies(\n appDir: string,\n dependencies: Readonly<Record<string, string>>,\n): string[] {\n const file = join(appDir, 'package.json')\n const manifest = JSON.parse(readFileSync(file, 'utf8')) as {\n dependencies?: Record<string, string>\n }\n const current = manifest.dependencies ?? {}\n const added = Object.keys(dependencies).filter((name) => !(name in current))\n const merged = { ...dependencies, ...current }\n manifest.dependencies = Object.fromEntries(\n Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)),\n )\n writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\\n`)\n return added\n}\n\n/** The generated app's guide for a coding agent: the shared core, then the template's own part. `CLAUDE.md` is only an import of it. */\nexport function writeAgentFiles(\n sharedDir: string,\n templateDir: string,\n target: string,\n name: string,\n): string[] {\n const core = readFileSync(join(sharedDir, 'AGENTS.core.md'), 'utf8')\n const own = existsSync(join(templateDir, 'AGENTS.md'))\n ? readFileSync(join(templateDir, 'AGENTS.md'), 'utf8')\n : ''\n writeFileSync(join(target, 'AGENTS.md'), `${core}${own}`.replaceAll('__APP_NAME__', name))\n writeFileSync(join(target, 'CLAUDE.md'), '@AGENTS.md\\n')\n return ['AGENTS.md', 'CLAUDE.md']\n}\n\n/**\n * Copy the framework's docs (a generated copy of ACRYL's `docs/framework`, already checked against its digest) into `<app>/docs/framework/`,\n * so a coding agent working in the app reads the API from the folder it is in. The generated-copy notes and the digest record stay behind.\n *\n * @returns the copied files, relative to `target`\n */\nexport function copyFrameworkDocs(source: string, target: string): string[] {\n const written: string[] = []\n const walk = (from: string, to: string, prefix: string) => {\n for (const entry of readdirSync(from, { withFileTypes: true })) {\n if (entry.name === 'upstream.json' || entry.name === 'UPSTREAM.md') continue\n const sourcePath = join(from, entry.name)\n const targetPath = join(to, entry.name)\n if (entry.isDirectory()) {\n mkdirSync(targetPath, { recursive: true })\n walk(sourcePath, targetPath, `${prefix}${entry.name}/`)\n continue\n }\n cpSync(sourcePath, targetPath)\n written.push(`docs/framework/${prefix}${entry.name}`)\n }\n }\n mkdirSync(join(target, 'docs', 'framework'), { recursive: true })\n walk(source, join(target, 'docs', 'framework'), '')\n return written\n}\n","import { existsSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/** The templates: a blank Cordis app on the router, a todo list built only on the router and state, and the blank app with the offline agent. */\nexport const TEMPLATES = ['blank', 'todo', 'agent'] as const\nexport type Template = (typeof TEMPLATES)[number]\n\nexport function isTemplate(value: string): value is Template {\n return (TEMPLATES as readonly string[]).includes(value)\n}\n\n/**\n * The `templates/` folder of this package. The module sits at different depths in source (`src/`) and in the build\n * (`dist/`), so walk up until a `templates` folder holds the `blank` template.\n */\nexport function templatesRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'templates')\n if (existsSync(join(candidate, 'blank'))) return candidate\n const parent = dirname(dir)\n if (parent === dir) throw new Error('acrylblends cannot find its templates folder')\n dir = parent\n }\n}\n\n/** Absolute path of a template folder. */\nexport function templateDir(template: Template): string {\n return join(templatesRoot(), template)\n}\n\n/** The text every generated app's `AGENTS.md` starts with (`shared/`, beside `templates/`). */\nexport function sharedDir(): string {\n return join(dirname(templatesRoot()), 'shared')\n}\n\n/**\n * The framework's docs, as ACRYL wrote them: `docs/framework` of the repository, or the copy `prepack` puts inside the package. The module sits at\n * different depths in source and in the build, so walk up until a `docs/framework` folder holds the README.\n */\nexport function frameworkDocsRoot(): string {\n let dir = dirname(fileURLToPath(import.meta.url))\n for (;;) {\n const candidate = join(dir, 'docs', 'framework')\n if (existsSync(join(candidate, 'README.md'))) return candidate\n const parent = dirname(dir)\n if (parent === dir)\n throw new Error('acrylblends cannot find the framework docs (docs/framework)')\n dir = parent\n }\n}\n","import { resolve } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport type { Io } from './io'\nimport { closure, copyPlugin, readLibrary } from './library'\nimport {\n copyFrameworkDocs,\n copyTemplate,\n isValidName,\n mergeDependencies,\n nameFrom,\n readTemplatePlugins,\n TargetNotEmptyError,\n writeAgentFiles,\n} from './project'\nimport {\n frameworkDocsRoot,\n isTemplate,\n sharedDir,\n TEMPLATES,\n type Template,\n templateDir,\n} from './templates'\n\n/** A command line mistake. The CLI prints it with the help text and exits 2. */\nexport class UsageError extends Error {\n override name = 'UsageError'\n}\n\n/** What `init` was asked for. Anything left undefined is asked for on a terminal, or defaulted. */\nexport interface InitOptions {\n directory: string | undefined\n name: string | undefined\n template: Template | undefined\n install: boolean | undefined\n yes: boolean\n}\n\n/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */\nexport function parseInitOptions(argv: readonly string[]): InitOptions {\n let parsed: ReturnType<typeof parse>\n try {\n parsed = parse(argv)\n } catch (error) {\n throw new UsageError((error as Error).message)\n }\n const { values, positionals } = parsed\n if (positionals.length > 1) throw new UsageError('init takes at most one directory')\n if (values.agent && values['no-agent'])\n throw new UsageError('use --agent or --no-agent, not both')\n if (values.template !== undefined && !isTemplate(values.template))\n throw new UsageError(`unknown template \"${values.template}\" (${TEMPLATES.join(', ')})`)\n const alias: Template | undefined = values.agent\n ? 'agent'\n : values['no-agent']\n ? 'blank'\n : undefined\n if (values.template !== undefined && alias !== undefined && values.template !== alias)\n throw new UsageError(\n `--template ${values.template} contradicts --${alias === 'agent' ? 'agent' : 'no-agent'}`,\n )\n return {\n directory: positionals[0],\n name: values.name,\n template: (values.template as Template | undefined) ?? alias,\n install: values.install,\n yes: values.yes === true,\n }\n}\n\nconst parse = (argv: readonly string[]) =>\n parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: {\n name: { type: 'string' },\n template: { type: 'string' },\n agent: { type: 'boolean' },\n 'no-agent': { type: 'boolean' },\n install: { type: 'boolean' },\n yes: { type: 'boolean', short: 'y' },\n },\n })\n\nconst yes = (answer: string) => ['y', 'yes'].includes(answer.trim().toLowerCase())\n\nasync function askTemplate(ask: (question: string) => Promise<string>): Promise<Template> {\n const answer = (await ask(`Template (${TEMPLATES.join(', ')}) [blank]: `)).trim().toLowerCase()\n if (answer === '') return 'blank'\n if (isTemplate(answer)) return answer\n throw new UsageError(`unknown template \"${answer}\" (${TEMPLATES.join(', ')})`)\n}\n\n/** Scaffold a Cordis app. Returns the exit code: 0 done, 1 refused or failed. Never overwrites anything. */\nexport async function init(options: InitOptions, io: Io): Promise<number> {\n const relative = options.directory ?? '.'\n const directory = resolve(io.cwd, relative)\n const ask = options.yes ? undefined : io.ask\n\n let name = options.name ?? nameFrom(directory)\n if (options.name === undefined && ask)\n name = (await ask(`Project name (${name}): `)).trim() || name\n if (!isValidName(name)) {\n throw new UsageError(\n `\"${name}\" is not a valid project name (lower case letters, digits and dashes, starting with a letter)`,\n )\n }\n const template = options.template ?? (ask ? await askTemplate(ask) : 'blank')\n const install =\n options.install ?? (ask ? yes(await ask('Install dependencies with npm now? (y/N): ')) : false)\n\n let files: string[]\n try {\n files = copyTemplate(templateDir(template), directory, name)\n files.push(...writeAgentFiles(sharedDir(), templateDir(template), directory, name))\n files.push(...copyFrameworkDocs(frameworkDocsRoot(), directory))\n } catch (error) {\n if (!(error instanceof TargetNotEmptyError)) throw error\n io.stderr.write(`error: ${error.message}\\n`)\n return 1\n }\n const plugins = closure(readTemplatePlugins(templateDir(template)), readLibrary())\n for (const plugin of plugins) {\n copyPlugin(plugin, directory)\n mergeDependencies(directory, plugin.dependencies)\n }\n io.stdout.write(\n `created ${name} (${template}) in ${directory}: ${files.length} files, plugins ${plugins.map((plugin) => plugin.name).join(', ')}\\n`,\n )\n\n if (install && (await io.install(directory)) !== 0) {\n io.stderr.write('error: npm install failed; run it yourself in the new directory\\n')\n return 1\n }\n const steps = [\n relative === '.' ? '' : ` cd ${relative}\\n`,\n install ? '' : ' npm install\\n',\n ' npm run dev\\n npm test\\n',\n ]\n io.stdout.write(`\\nNext:\\n${steps.join('')}`)\n return 0\n}\n","import { cpSync, existsSync, readFileSync, statSync } from 'node:fs'\nimport { basename, join, resolve } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { UsageError } from './init'\nimport type { Io } from './io'\nimport { closure, copyPlugin, type LibraryPlugin, readLibrary } from './library'\nimport { mergeDependencies } from './project'\n\nexport interface AddOptions {\n readonly names: readonly string[]\n readonly directory: string | undefined\n}\n\n/** Parse `add <plugin...> [--dir <app>]`. */\nexport function parseAddOptions(argv: readonly string[]): AddOptions {\n let parsed: ReturnType<typeof parse>\n try {\n parsed = parse(argv)\n } catch (error) {\n throw new UsageError((error as Error).message)\n }\n if (parsed.positionals.length === 0)\n throw new UsageError('add needs at least one plugin name (see \"acrylblends plugins\")')\n return { names: parsed.positionals, directory: parsed.values.dir }\n}\n\nconst parse = (argv: readonly string[]) =>\n parseArgs({\n args: [...argv],\n allowPositionals: true,\n strict: true,\n options: { dir: { type: 'string' } },\n })\n\nconst present = (appDir: string, name: string) => existsSync(join(appDir, 'src', 'plugins', name))\n\n/**\n * Copy library plugins into an existing app, with the plugins they require, and add their npm packages to its package.json.\n * Refuses a plugin the app already has. Returns the exit code.\n */\nexport async function add(options: AddOptions, io: Io): Promise<number> {\n const appDir = resolve(io.cwd, options.directory ?? '.')\n if (!existsSync(join(appDir, 'package.json')) || !existsSync(join(appDir, 'src'))) {\n io.stderr.write(\n `error: ${appDir} is not an app (no package.json and src/); run \"acrylblends init\" first\\n`,\n )\n return 1\n }\n // A path is an external plugin folder; anything else is a library plugin name.\n const external = options.names.filter((name) => /[\\\\/]/.test(name) || name.startsWith('.'))\n for (const folder of external) {\n const code = addExternal(resolve(io.cwd, folder), appDir, io)\n if (code !== 0) return code\n }\n const names = options.names.filter((name) => !external.includes(name))\n if (names.length === 0) return 0\n const already = names.filter((name) => present(appDir, name))\n if (already.length > 0) {\n io.stderr.write(\n `error: the app already has ${already.map((name) => `src/plugins/${name}`).join(', ')}\\n`,\n )\n return 1\n }\n const toCopy: LibraryPlugin[] = closure(names, readLibrary()).filter(\n (plugin) => !present(appDir, plugin.name),\n )\n const addedDependencies: string[] = []\n for (const plugin of toCopy) {\n copyPlugin(plugin, appDir)\n addedDependencies.push(...mergeDependencies(appDir, plugin.dependencies))\n }\n io.stdout.write(`added ${toCopy.map((plugin) => plugin.name).join(', ')} to ${appDir}\\n`)\n if (addedDependencies.length > 0)\n io.stdout.write(`new dependencies: ${addedDependencies.join(', ')} (run npm install)\\n`)\n for (const plugin of toCopy.filter((plugin) => names.includes(plugin.name))) {\n io.stdout.write(\n `\\n${plugin.name}: ${plugin.mount ?? `import it from './plugins/${plugin.name}/index.ts' and mount it in src/app.ts`}\\n`,\n )\n }\n return 0\n}\n\nconst PLUGIN_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/\n\n/**\n * Bring an external plugin folder (a package made with the builder's package_plugin, or any folder with an index.ts that exports a\n * Cordis plugin) into the app's `extensions/`, where the extensions plugin installs it like any other: checked, staged, started,\n * verified. Its runtime dependencies are not installed here; they are reported so the user adds them deliberately.\n */\nfunction addExternal(folder: string, appDir: string, io: Io): number {\n if (!existsSync(folder) || !statSync(folder).isDirectory()) {\n io.stderr.write(`error: ${folder} is not a folder\\n`)\n return 1\n }\n if (!['index.ts', 'index.js', 'index.mjs'].some((entry) => existsSync(join(folder, entry)))) {\n io.stderr.write(`error: ${folder} has no index.ts that exports a Cordis plugin\\n`)\n return 1\n }\n const manifestFile = join(folder, 'package.json')\n const manifest = existsSync(manifestFile)\n ? (JSON.parse(readFileSync(manifestFile, 'utf8')) as {\n name?: string\n dependencies?: Record<string, string>\n })\n : {}\n const name = (manifest.name ?? basename(folder)).replace(/^@[^/]+\\//, '')\n if (!PLUGIN_NAME.test(name)) {\n io.stderr.write(\n `error: \"${name}\" is not a plugin name (lowercase letters, digits and dashes)\\n`,\n )\n return 1\n }\n const target = join(appDir, 'extensions', name)\n if (existsSync(target)) {\n io.stderr.write(`error: the app already has extensions/${name}\\n`)\n return 1\n }\n cpSync(folder, target, {\n recursive: true,\n filter: (path) => !/[\\\\/](node_modules|\\.git)([\\\\/]|$)/.test(path.slice(folder.length)),\n })\n io.stdout.write(\n `added external plugin ${name} to ${join(appDir, 'extensions')}; it installs at the next start\\n`,\n )\n const dependencies = Object.keys(manifest.dependencies ?? {})\n if (dependencies.length > 0)\n io.stdout.write(\n `it uses ${dependencies.join(', ')}: add them to the app with npm install first\\n`,\n )\n return 0\n}\n\n/** `acrylblends plugins`: the library, one line per plugin. */\nexport function listPlugins(io: Io): number {\n for (const plugin of readLibrary().values()) {\n const requires = plugin.requires.length > 0 ? ` (requires ${plugin.requires.join(', ')})` : ''\n io.stdout.write(`${plugin.name.padEnd(12)} ${plugin.description}${requires}\\n`)\n }\n return 0\n}\n","import { spawn } from 'node:child_process'\nimport { createInterface } from 'node:readline/promises'\n\n/** Everything the CLI touches outside the filesystem. Tests pass their own. */\nexport interface Io {\n stdout: { write(text: string): unknown }\n stderr: { write(text: string): unknown }\n cwd: string\n /** Ask one question; `undefined` when there is no terminal to ask on. */\n ask: ((question: string) => Promise<string>) | undefined\n /** Install dependencies in a directory; resolves to the exit code. */\n install(directory: string): Promise<number>\n}\n\n/** The real terminal and `npm install`. */\nexport function processIo(): Io {\n const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY)\n return {\n stdout: process.stdout,\n stderr: process.stderr,\n cwd: process.cwd(),\n ask: interactive\n ? async (question) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout })\n try {\n return await rl.question(question)\n } finally {\n rl.close()\n }\n }\n : undefined,\n install: (directory) =>\n new Promise((done) => {\n const child = spawn('npm', ['install'], {\n cwd: directory,\n stdio: 'inherit',\n shell: process.platform === 'win32',\n })\n child.on('error', () => done(1))\n child.on('exit', (code) => done(code ?? 1))\n }),\n }\n}\n","import { createRequire } from 'node:module'\nimport { add, listPlugins, parseAddOptions } from './add'\nimport { init, parseInitOptions, UsageError } from './init'\nimport { type Io, processIo } from './io'\n\n/** The version of this package. */\nexport const VERSION: string = (\n createRequire(import.meta.url)('../package.json') as { version: string }\n).version\n\nexport const HELP = `acrylblends ${VERSION} - scaffold Cordis apps\n\nUsage:\n acrylblends init [dir] [options] Create a Cordis app in an empty or new directory\n acrylblends add <plugin...> [--dir d] Copy library plugins (and what they require) into an app;\n a folder path brings an external plugin into its extensions/\n acrylblends plugins List the plugin library\n\nOptions for init:\n --template <name> blank or agent (asked when not given); --agent means --template agent,\n --no-agent means --template blank\n --name <name> the project name (default: the directory name)\n --install run npm install after copying\n -y, --yes ask nothing; use defaults (the blank template, no install)\n\n -h, --help show this help\n -v, --version show the version\n`\n\n/**\n * Run the CLI and return the exit code: 0 done, 1 failed or refused, 2 a usage error. Never calls `process.exit`.\n */\nexport async function runCli(argv: readonly string[], io: Io = processIo()): Promise<number> {\n const [command, ...args] = argv\n try {\n if (command === undefined || command === 'help' || command === '-h' || command === '--help') {\n io.stdout.write(HELP)\n return command === undefined ? 2 : 0\n }\n if (command === 'version' || command === '-v' || command === '--version') {\n io.stdout.write(`${VERSION}\\n`)\n return 0\n }\n if (command === 'init') return await init(parseInitOptions(args), io)\n if (command === 'add') return await add(parseAddOptions(args), io)\n if (command === 'plugins') return listPlugins(io)\n throw new UsageError(`unknown command \"${command}\"`)\n } catch (error) {\n if (error instanceof UsageError) {\n io.stderr.write(`error: ${error.message}\\n\\n${HELP}`)\n return 2\n }\n io.stderr.write(`error: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ The pages in this folder are adapted in part from the Hono documentation (https://github.com/honojs/website), used under this license:
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2021 - present, Yusuke Wada and Hono contributors
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,63 @@
1
+ # ACRYL Framework
2
+
3
+ Status: 0.4.0 (spec 047, `specs/047-framework-port/plan.md`). The application core in `acryl-router` and the Node adapter in
4
+ `acryl-node-server` are ported from a mature MIT web framework with their test suites; the Cordis plugins around them are ACRYL's own.
5
+
6
+ The ACRYL Framework is the small, readable layer on top of Cordis that turns "a bunch of DSH packages" into building an agentic app. You
7
+ write an ordinary `App` (routes, a `Context`, middleware with `next()`, validators and helpers on Web-standard `Request` and `Response`),
8
+ and a Cordis plugin mounts it with `ctx.router.route(path, app)`.
9
+
10
+ ## The five rules
11
+
12
+ 1. **Everything extensible is a Cordis plugin.**
13
+ 2. **One application, several surfaces** - share capabilities and contracts, not necessarily the transport.
14
+ 3. **One declared contract per plugin** - what it provides, needs, and registers (routes, commands, tools, slots).
15
+ 4. **Every registration is reversible** - unloading a plugin removes what it owns.
16
+ 5. **Conventions are explicit and small** - an ordinary feature needs no knowledge of runtime internals.
17
+
18
+ ## What it gives you
19
+
20
+ One handler, every surface. A plugin writes a handler once; Web and Desktop reach it over HTTP (Desktop's window talks to the app's own
21
+ loopback server), the terminal UI and tests call it in process with `ctx.router.request(path)`, and a standalone process serves it with
22
+ `acryl-node-server`. The universal entry point is `ctx.router.fetch(request)`.
23
+
24
+ ## Documentation map
25
+
26
+ | Topic | Page |
27
+ | --- | --- |
28
+ | First application | [Getting started](./getting-started.md) |
29
+ | Every everyday call on one page | [API summary](./api-summary.md) |
30
+ | Framework model | [Core concepts](./concepts.md) |
31
+ | Routes and mounting | [Routing and mounting](./routing.md) |
32
+ | Handler context | [Context](./context.md) |
33
+ | Reading requests | [Request](./request.md) |
34
+ | Cross-cutting behavior, and the built-in list | [Middleware](./middleware.md) |
35
+ | Input checking | [Validation](./validation.md) |
36
+ | Streaming, cookies, testing and the rest | [Helpers](./helpers.md) |
37
+ | Serving without the ACRYL Host | [Node server](./node-server.md) |
38
+ | Cordis ownership and service state | [Plugin lifecycle and disposal](./plugin-lifecycle.md) |
39
+ | The application surfaces | [Surfaces and adapters](./surfaces.md) |
40
+ | Leaving the DSH server | [Migration from the DSH webServer](./migration-from-dsh-webserver.md) |
41
+ | Moving from 0.3 | [Migration from 0.3](./migration-from-0-3.md) |
42
+
43
+ [ACRYL Blends](https://acrylblends.github.io/) is the product-facing guide for building and publishing complete applications. The
44
+ framework pages here are the API and integration reference behind those guides. The mirrored developer documentation is published at
45
+ [acryl.dev/docs](https://acryl.dev/docs).
46
+
47
+ ## The packages
48
+
49
+ | Package | What it is |
50
+ | --- | --- |
51
+ | `acryl-router` | `App` and the whole application API; the Cordis `router` service that composes the mounted apps; `acryl-router/middleware`, `acryl-router/helpers`, `acryl-router/client` |
52
+ | `acryl-router-http` | inside the ACRYL Host: `ctx.routerHttp.mount(prefix)` serves a prefix of the router on the Host's web server |
53
+ | `acryl-node-server` | without the Host: one `node:http` server for the whole router, `ctx.nodeServer.url`, and `serveStatic` |
54
+ | `acryl-state` | observable cells shared by every surface, with read-only HTTP and SSE routes and `useCell` for React |
55
+ | `acryl-inspect` | what each plugin requires, provides and mounts, read from the running system |
56
+
57
+ An app's `tsconfig.json` can extend `acryl-router/tsconfig.app.json` (ES2024 with the DOM types, strict).
58
+
59
+ ## Why this exists
60
+
61
+ DSH changes fast and stays one optional agent inside ACRYL. Cordis is the lifecycle ACRYL keeps across Web (browser), Desktop (Electron)
62
+ and the terminal UI. HTTP used to go through DSH's `webServer` - a `node:http` route table with no middleware and no Web-standard
63
+ `Request`/`Response` - which the terminal UI cannot reuse. The framework removes that coupling.
@@ -0,0 +1,3 @@
1
+ # Generated copy
2
+
3
+ This folder is a copy of `docs/framework` of acryldev/acryl at commit `64e7c9d2e787f74b2da82492644fbbb46a601074`, made by `scripts/sync-framework.mjs`. Do not edit it here: change the text in ACRYL and sync again. `pnpm run verify:framework` fails when a page differs from the recorded digest.
@@ -0,0 +1,138 @@
1
+ # API summary
2
+
3
+ One page with the calls an ordinary app uses, each linked to the page that explains it. Imports are from `acryl-router` unless noted.
4
+
5
+ ## A plugin that serves an app
6
+
7
+ ```ts
8
+ import type { Context } from '@deepseek-ai/cordis'
9
+ import { App } from 'acryl-router'
10
+ import { trimTrailingSlash } from 'acryl-router/middleware'
11
+ import { schemaValidator, streamSSE, type StandardSchema } from 'acryl-router/helpers'
12
+ import type {} from 'acryl-state'
13
+
14
+ export const name = 'orders' // the package name and the Loader row id
15
+ export const inject = ['router', 'state'] // the plugin waits (PENDING) until both exist
16
+
17
+ interface Order { id: string, item: string }
18
+ const NewOrder: StandardSchema<{ item: string }> = {
19
+ '~standard': {
20
+ validate: value => {
21
+ const item = (value as { item?: unknown } | null)?.item
22
+ return typeof item === 'string' && item !== '' ? { value: { item } } : { issues: [{ message: 'item is required', path: ['item'] }] }
23
+ },
24
+ },
25
+ }
26
+
27
+ export function apply(ctx: Context): void {
28
+ const orders = ctx.state.cell<Order[]>('orders.list', [])
29
+ const app = new App()
30
+ .use(trimTrailingSlash())
31
+ .get('/', c => c.json(orders.get()))
32
+ .post('/', schemaValidator('json', NewOrder), c => {
33
+ const order = { id: crypto.randomUUID(), ...c.req.valid('json') }
34
+ orders.update(list => [...list, order])
35
+ return c.json(order, 201)
36
+ })
37
+ .get('/events', c => streamSSE(c, async stream => {
38
+ const ended = new Promise<void>(resolve => { stream.onAbort(resolve) })
39
+ const stop = orders.subscribe(list => { void stream.writeSSE({ data: JSON.stringify(list) }) })
40
+ await stream.writeSSE({ data: JSON.stringify(orders.get()) })
41
+ try { await ended } finally { stop() }
42
+ }))
43
+ ctx.router.route('/orders', app)
44
+ ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/orders') })
45
+ }
46
+ ```
47
+
48
+ ## `App` ([Routing](./routing.md))
49
+
50
+ | Call | Does |
51
+ | --- | --- |
52
+ | `new App()` | an application: a route table plus middleware |
53
+ | `.get / .post / .put / .patch / .delete / .options (path, ...handlers)` | a route; handlers before the last are middleware for that route |
54
+ | `.all(path, ...)`, `.on(methods, path, ...)` | any method, or a list of methods |
55
+ | `.use([path], ...middleware)` | middleware for every route below `path` (all routes without it), registered before the routes it wraps |
56
+ | `.route(path, otherApp)` | copy another app's routes below `path` |
57
+ | `.basePath(path)` | the same app with a prefix on every route registered through it |
58
+ | `.onError((err, c) => ...)`, `.notFound(c => ...)` | the app's error and not-found answers |
59
+ | `.request(pathOrRequest, init?)` | call the app in process (tests) |
60
+ | `.fetch(request)` | the app as a fetch handler |
61
+
62
+ Paths: `/posts/:id` (parameter), `/posts/:id?` (optional), `/files/*` (wildcard), `/:name{[a-z]+}` (pattern). `/a` and `/a/` are different
63
+ paths: see [Trailing slashes](./routing.md#trailing-slashes).
64
+
65
+ ## The handler context `c` ([Context](./context.md))
66
+
67
+ | Call | Does |
68
+ | --- | --- |
69
+ | `c.json(value, status?, headers?)` | a JSON answer |
70
+ | `c.text(text, status?)`, `c.html(html, status?)` | a text or HTML answer |
71
+ | `c.body(data, status?, headers?)` | a raw answer (`c.body(null, 204)`) |
72
+ | `c.redirect(location, status = 302)` | a redirect |
73
+ | `c.notFound()` | the app's not-found answer |
74
+ | `c.header(name, value)`, `c.status(code)` | set a header or the status (also after `await next()` in middleware) |
75
+ | `c.set(key, value)`, `c.get(key)`, `c.var.key` | values for this request only, passed from middleware to handlers |
76
+ | `c.req.raw.signal` | aborts when the caller leaves, the serving plugin unloads, or the router is disposed |
77
+ | `c.executionCtx.waitUntil(promise)` | keep the request in flight until background work settles |
78
+ | `connectionOf(c)` / `getConnInfo(c)` | the caller's address and the local port, when a transport supplied them |
79
+ | `throw new HTTPException(status, { message })` | stop with that status from anywhere in the chain |
80
+
81
+ ## The request `c.req` ([Request](./request.md))
82
+
83
+ | Call | Gives |
84
+ | --- | --- |
85
+ | `c.req.param('id')`, `c.req.param()` | route parameters |
86
+ | `c.req.query('page')`, `c.req.query()`, `c.req.queries('tag')` | query string values (`queries` for repeated keys) |
87
+ | `c.req.header('x-name')`, `c.req.header()` | request headers |
88
+ | `await c.req.json()`, `c.req.text()`, `c.req.formData()`, `c.req.parseBody()`, `c.req.arrayBuffer()`, `c.req.blob()` | the body, once |
89
+ | `c.req.valid(target)` | the value a validator checked, typed by its schema |
90
+ | `c.req.method`, `c.req.path`, `c.req.url`, `c.req.raw` | the method, path, full URL and the Web `Request` |
91
+
92
+ ## Validation ([Validation](./validation.md))
93
+
94
+ ```ts
95
+ import { schemaValidator, validator } from 'acryl-router/helpers'
96
+ app.post('/', schemaValidator('json', Schema), c => c.json(c.req.valid('json')))
97
+ ```
98
+
99
+ - Targets: `json`, `form`, `query`, `param`, `header`, `cookie`. `Schema` is any Standard Schema (Schemastery, Zod, Valibot, or an
100
+ object with `'~standard': { validate }`).
101
+ - A failure is 400 `{ error: 'invalid <target>', issues: [{ message, path }] }`; `path` is dotted, and `""` when the issue has none.
102
+ - The `json` target reads the body only with a JSON `Content-Type`; otherwise the schema sees `{}`.
103
+ - `validator(target, (value, c) => valueOrResponse)` is the same middleware with a function instead of a schema.
104
+ - Put `bodyLimit({ maxSize })` from `acryl-router/middleware` in front on routes open to untrusted callers.
105
+
106
+ ## Streaming ([Helpers](./helpers.md))
107
+
108
+ | Call | Does |
109
+ | --- | --- |
110
+ | `streamSSE(c, async stream => { ... })` | a server-sent event answer |
111
+ | `stream.writeSSE({ data, event?, id?, retry? })` | send one event (`data` is a string) |
112
+ | `stream.write(text)`, `stream.writeln(text)` | raw bytes or a line (a comment line such as `: keep-alive\n\n` keeps a proxy from closing the stream) |
113
+ | `stream.onAbort(fn)`, `stream.aborted` | the client left or the plugin unloaded: stop work and release subscriptions |
114
+ | `stream(c, ...)`, `streamText(c, ...)` | a byte stream or a text stream instead of events |
115
+
116
+ Do not start timers inside a handler for keep-alives; let the plugin own one timer and write to the open streams.
117
+
118
+ ## Cordis services
119
+
120
+ | Call | Service | Does |
121
+ | --- | --- | --- |
122
+ | `ctx.router.route(path, app)` | `router` (`acryl-router`) | serve `app` below `path` for as long as the calling plugin is loaded; returns a function that removes it sooner |
123
+ | `ctx.router.request(path, init?)`, `ctx.router.fetch(request)` | `router` | call the mounted routes in process (terminal UI, tests, other plugins) |
124
+ | `ctx.router.onError(handler)`, `ctx.router.notFound(handler)` | `router` | router-wide error and not-found answers, owned by the caller |
125
+ | `ctx.routerHttp.mount(prefix)` | `routerHttp` (`acryl-router-http`) | inside the ACRYL Host: serve a router prefix on the Host's web server (Web and Desktop). Use it through `ctx.inject(['routerHttp'], ...)` so the app also runs where there is no Host |
126
+ | `ctx.nodeServer.url`, `ctx.nodeServer.port` | `nodeServer` (`acryl-node-server`) | without the Host: one Node server for the whole router; options `hostname` (`127.0.0.1`), `port` (`0`), `scan` (`20`). See [Node server](./node-server.md) |
127
+ | `ctx.state.cell(key, initial)` | `state` (`acryl-state`) | a writable cell owned by the plugin: `get()`, `set(value)`, `update(fn)`, `subscribe(fn)` returning an unsubscribe |
128
+ | `ctx.state.derive(key, [cells], fn)` | `state` | a read-only cell computed from others |
129
+ | `GET /api/state/:key`, `GET /api/state/:key/events` | `state` | each cell over the router, read-only; `useCell(cell)` from `acryl-state/react` in a React page |
130
+
131
+ Everything a plugin registers this way (mounts, cells, handlers) goes away when the plugin unloads, and comes back once when it reloads:
132
+ see [Plugin lifecycle and disposal](./plugin-lifecycle.md).
133
+
134
+ ## Middleware ([Middleware](./middleware.md))
135
+
136
+ From `acryl-router/middleware`: `bodyLimit`, `cors`, `csrf`, `secureHeaders`, `etag`, `compress`, `logger`, `requestId`, `timeout`,
137
+ `bearerAuth`, `basicAuth`, `jwt`, `jwk`, `ipRestriction`, `methodNotAllowed`, `trimTrailingSlash`, `appendTrailingSlash`, `some` /
138
+ `every` / `except`, and ACRYL's `loopbackOnly` for private loopback routes.
@@ -0,0 +1,105 @@
1
+ # Core concepts
2
+
3
+ The framework's request-handling model is designed so every extension point belongs to a Cordis plugin. This page explains the pieces
4
+ before the API reference.
5
+
6
+ ## App
7
+
8
+ An `App` is a self-contained ACRYL application. It owns:
9
+
10
+ - routes;
11
+ - middleware;
12
+ - an error handler (`app.onError`);
13
+ - a request entry point (`app.fetch`, and `app.request` for tests);
14
+ - sub-applications placed below a path with `app.route(path, sub)`.
15
+
16
+ An app can be tested without a server:
17
+
18
+ ```ts
19
+ const response = await app.request('/todos/42')
20
+ ```
21
+
22
+ An app is not itself a Cordis service. That separation lets the same app be mounted by different hosts, at different paths, or not
23
+ mounted at all.
24
+
25
+ ## Router service
26
+
27
+ The Cordis `router` service (`ctx.router`, provided by `acryl-router`) composes the mounted apps into one route space. A plugin builds an
28
+ app and mounts it:
29
+
30
+ ```ts
31
+ ctx.router.route('/todo', app)
32
+ ```
33
+
34
+ The mount copies the app's routes at that call. When any mount is added or removed, the next request composes a fresh app from the mounts
35
+ that are live, in the order they were made; requests already running keep the app they started with. This matches Cordis: plugins can
36
+ load, unload, reload and be replaced while the host stays up. The core's route tables are add-only, so rebuilding is the only correct way
37
+ to remove a route; it costs about 1 ms per 1000 routes, once per change.
38
+
39
+ The router also takes one router-wide `onError` and one `notFound` (each owned by the plugin that set it), and runs requests in process:
40
+ `ctx.router.fetch(request, connection?)` and `ctx.router.request(path, init?, connection?)`.
41
+
42
+ ## Handler
43
+
44
+ A handler receives one context object, conventionally named `c`:
45
+
46
+ ```ts
47
+ app.get('/todos/:id', c => {
48
+ return c.json({ id: c.req.param('id') })
49
+ })
50
+ ```
51
+
52
+ Handlers return a Web `Response`, either directly or through context helpers such as `c.text`, `c.json` and `c.html`.
53
+
54
+ ## Middleware
55
+
56
+ Middleware wraps handlers and other middleware:
57
+
58
+ ```ts
59
+ app.use('/admin/*', async (c, next) => {
60
+ if (!isAllowed(c)) return c.text('Forbidden', 403)
61
+ await next()
62
+ })
63
+ ```
64
+
65
+ Code before `await next()` runs on the way in. Code after it can inspect or change the response (`c.res`) on the way out.
66
+
67
+ ## Surface
68
+
69
+ A surface is the place a person or another program uses the application:
70
+
71
+ | Surface | Transport |
72
+ | --- | --- |
73
+ | Web | the ACRYL Host's web server, through `acryl-router-http` |
74
+ | Desktop | the same: the Electron window talks to the app's own loopback web server |
75
+ | Terminal UI | in process: `ctx.router.request(...)`, no server |
76
+ | A standalone process | `acryl-node-server` serves the whole router |
77
+ | Tests | `ctx.router.request(...)` or `app.request(...)` |
78
+
79
+ Surfaces share capabilities and contracts. They do not need to share a transport or a user interface.
80
+
81
+ ## Plugin contract
82
+
83
+ Following the ACRYL framework rules, a plugin declares what it:
84
+
85
+ - provides as services;
86
+ - requires through injection;
87
+ - registers as routes, commands, tools or interface slots;
88
+ - owns until disposal.
89
+
90
+ The declaration is what lets tooling answer two questions without reading all the code: what does this plugin do, and what disappears
91
+ when it unloads? `acryl-inspect` reads it from the running system: each mount is labeled with its path and has one labeled child per route it serves.
92
+
93
+ ## State
94
+
95
+ Request state belongs to the context. Application state shared between surfaces belongs in `acryl-state` or an application-owned Cordis
96
+ service. Do not hide shared mutable state in module globals: Cordis can reload plugins and run several surfaces from one host, and a
97
+ module global outlives the plugin that wrote it.
98
+
99
+ ## Design rules
100
+
101
+ 1. Everything extensible is a Cordis plugin.
102
+ 2. One application can serve several surfaces.
103
+ 3. Each plugin has one declared contract.
104
+ 4. Every registration is reversible.
105
+ 5. Normal application code should not need runtime internals.