acrylblends 0.2.0 → 0.3.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/dist/index.d.ts +17 -16
- package/dist/index.js +39 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugins/acryl-inspect/LICENSE +21 -0
- package/plugins/acryl-inspect/plugin.json +11 -0
- package/plugins/acryl-inspect/src/index.ts +3 -0
- package/plugins/acryl-inspect/src/report.ts +123 -0
- package/plugins/acryl-inspect/src/service.ts +66 -0
- package/plugins/acryl-inspect/tests/report.spec.ts +64 -0
- package/plugins/acryl-inspect/tests/service.spec.ts +120 -0
- package/plugins/acryl-inspect/upstream.json +18 -0
- package/plugins/acryl-node-server/LICENSE +21 -0
- package/plugins/acryl-node-server/plugin.json +12 -0
- package/plugins/acryl-node-server/src/index.ts +94 -0
- package/plugins/acryl-node-server/tests/server.spec.ts +105 -0
- package/plugins/acryl-node-server/upstream.json +18 -0
- package/plugins/acryl-router/LICENSE +21 -0
- package/plugins/acryl-router/plugin.json +12 -0
- package/plugins/acryl-router/src/compose.ts +21 -0
- package/plugins/acryl-router/src/context.ts +59 -0
- package/plugins/acryl-router/src/helpers/index.ts +3 -0
- package/plugins/acryl-router/src/helpers/read-body.ts +59 -0
- package/plugins/acryl-router/src/helpers/sse.ts +83 -0
- package/plugins/acryl-router/src/helpers/validate.ts +44 -0
- package/plugins/acryl-router/src/http-error.ts +21 -0
- package/plugins/acryl-router/src/index.ts +6 -0
- package/plugins/acryl-router/src/middleware/body-limit.ts +25 -0
- package/plugins/acryl-router/src/middleware/cors.ts +46 -0
- package/plugins/acryl-router/src/middleware/index.ts +6 -0
- package/plugins/acryl-router/src/middleware/logger.ts +21 -0
- package/plugins/acryl-router/src/middleware/loopback-only.ts +82 -0
- package/plugins/acryl-router/src/middleware/request-id.ts +24 -0
- package/plugins/acryl-router/src/middleware/secure-headers.ts +23 -0
- package/plugins/acryl-router/src/pattern.ts +89 -0
- package/plugins/acryl-router/src/registrar.ts +90 -0
- package/plugins/acryl-router/src/service.ts +164 -0
- package/plugins/acryl-router/src/table.ts +156 -0
- package/plugins/acryl-router/src/types.ts +73 -0
- package/plugins/acryl-router/src/util.ts +23 -0
- package/plugins/acryl-router/tests/api.spec.ts +223 -0
- package/plugins/acryl-router/tests/conformance.ts +170 -0
- package/plugins/acryl-router/tests/helpers.spec.ts +172 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +64 -0
- package/plugins/acryl-router/tests/loader.spec.ts +71 -0
- package/plugins/acryl-router/tests/middleware.spec.ts +183 -0
- package/plugins/acryl-router/tests/service.spec.ts +211 -0
- package/plugins/acryl-router/tests/table.spec.ts +47 -0
- package/plugins/acryl-router/upstream.json +20 -0
- package/plugins/acryl-router-http/LICENSE +21 -0
- package/plugins/acryl-router-http/plugin.json +11 -0
- package/plugins/acryl-router-http/src/bridge.ts +52 -0
- package/plugins/acryl-router-http/src/index.ts +63 -0
- package/plugins/acryl-router-http/src/serve.ts +24 -0
- package/plugins/acryl-router-http/tests/adapter.spec.ts +170 -0
- package/plugins/acryl-router-http/tests/stream.spec.ts +133 -0
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +99 -0
- package/plugins/acryl-router-http/upstream.json +22 -0
- package/plugins/acryl-state/LICENSE +21 -0
- package/plugins/acryl-state/plugin.json +12 -0
- package/plugins/acryl-state/src/cell.ts +67 -0
- package/plugins/acryl-state/src/index.ts +3 -0
- package/plugins/acryl-state/src/react.ts +11 -0
- package/plugins/acryl-state/src/service.ts +101 -0
- package/plugins/acryl-state/tests/cell.spec.ts +71 -0
- package/plugins/acryl-state/tests/react.spec.ts +39 -0
- package/plugins/acryl-state/tests/service.spec.ts +132 -0
- package/plugins/acryl-state/upstream.json +20 -0
- package/plugins/builder/script.ts +8 -9
- package/plugins/extensions/index.ts +18 -1
- package/plugins/openai/prompt.ts +9 -5
- package/plugins/tasks/index.ts +14 -0
- package/plugins/tsconfig.json +9 -1
- package/templates/agent/blend.yaml +5 -7
- package/templates/agent/src/app.ts +8 -8
- package/templates/agent/src/routes.ts +32 -41
- package/templates/agent/template.json +3 -3
- package/templates/agent/test/builder.test.ts +22 -14
- package/templates/agent/test/model.test.ts +2 -3
- package/templates/{app → blank}/blend.yaml +3 -3
- package/templates/{app → blank}/src/app.ts +5 -5
- package/templates/blank/src/routes.ts +14 -0
- package/templates/blank/template.json +1 -0
- package/templates/{app → blank}/test/app.test.ts +1 -1
- package/templates/todo/README.md +27 -0
- package/templates/todo/blend.yaml +25 -0
- package/templates/todo/gitignore +2 -0
- package/templates/todo/package.json +23 -0
- package/templates/todo/src/app.ts +71 -0
- package/templates/todo/src/main.ts +19 -0
- package/templates/todo/src/page.ts +28 -0
- package/templates/todo/src/todo.ts +96 -0
- package/templates/todo/template.json +1 -0
- package/templates/todo/test/todo.test.ts +145 -0
- package/templates/todo/tsconfig.json +19 -0
- package/templates/todo/upstream.json +12 -0
- package/plugins/events/index.ts +0 -42
- package/plugins/events/plugin.json +0 -9
- package/plugins/http/index.ts +0 -64
- package/plugins/http/plugin.json +0 -10
- package/plugins/server/index.ts +0 -138
- package/plugins/server/plugin.json +0 -10
- package/templates/app/src/routes.ts +0 -14
- package/templates/app/template.json +0 -1
- /package/templates/{app → blank}/README.md +0 -0
- /package/templates/{app → blank}/gitignore +0 -0
- /package/templates/{app → blank}/package.json +0 -0
- /package/templates/{app → blank}/src/main.ts +0 -0
- /package/templates/{app → blank}/tsconfig.json +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,19 @@ export declare const HELP: string;
|
|
|
38
38
|
*/
|
|
39
39
|
export declare function runCli(argv: readonly string[], io?: Io): Promise<number>;
|
|
40
40
|
//#endregion
|
|
41
|
+
//#region src/templates.d.ts
|
|
42
|
+
/** 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. */
|
|
43
|
+
export declare const TEMPLATES: readonly ["blank", "todo", "agent"];
|
|
44
|
+
type Template = (typeof TEMPLATES)[number];
|
|
45
|
+
export declare function isTemplate(value: string): value is Template;
|
|
46
|
+
/**
|
|
47
|
+
* The `templates/` folder of this package. The module sits at different depths in source (`src/`) and in the build
|
|
48
|
+
* (`dist/`), so walk up until a `templates` folder holds the `blank` template.
|
|
49
|
+
*/
|
|
50
|
+
export declare function templatesRoot(): string;
|
|
51
|
+
/** Absolute path of a template folder. */
|
|
52
|
+
export declare function templateDir(template: Template): string;
|
|
53
|
+
//#endregion
|
|
41
54
|
//#region src/init.d.ts
|
|
42
55
|
/** A command line mistake. The CLI prints it with the help text and exits 2. */
|
|
43
56
|
export declare class UsageError extends Error {
|
|
@@ -47,11 +60,11 @@ export declare class UsageError extends Error {
|
|
|
47
60
|
interface InitOptions {
|
|
48
61
|
directory: string | undefined;
|
|
49
62
|
name: string | undefined;
|
|
50
|
-
|
|
63
|
+
template: Template | undefined;
|
|
51
64
|
install: boolean | undefined;
|
|
52
65
|
yes: boolean;
|
|
53
66
|
}
|
|
54
|
-
/** Parse `init [dir] [--agent|--no-agent] [--name n] [--install] [-y]`. */
|
|
67
|
+
/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */
|
|
55
68
|
export declare function parseInitOptions(argv: readonly string[]): InitOptions;
|
|
56
69
|
/** Scaffold a Cordis app. Returns the exit code: 0 done, 1 refused or failed. Never overwrites anything. */
|
|
57
70
|
export declare function init(options: InitOptions, io: Io): Promise<number>;
|
|
@@ -67,6 +80,8 @@ interface LibraryPlugin {
|
|
|
67
80
|
readonly dependencies: Readonly<Record<string, string>>;
|
|
68
81
|
/** How to mount it, shown after `acrylblends add`. */
|
|
69
82
|
readonly mount?: string;
|
|
83
|
+
/** Paths inside the plugin folder that are not copied into an app (for example a React hook an app without React cannot type-check). */
|
|
84
|
+
readonly omit?: readonly string[];
|
|
70
85
|
}
|
|
71
86
|
/** The library folder (`plugins/` at the repository root): walk up from this module, in source or in the build. */
|
|
72
87
|
export declare function libraryRoot(): string;
|
|
@@ -99,19 +114,5 @@ export declare function readTemplatePlugins(templateDir: string): string[];
|
|
|
99
114
|
/** Add run-time dependencies to an app's package.json, keeping what is there and sorting the keys. Returns the names added. */
|
|
100
115
|
export declare function mergeDependencies(appDir: string, dependencies: Readonly<Record<string, string>>): string[];
|
|
101
116
|
//#endregion
|
|
102
|
-
//#region src/templates.d.ts
|
|
103
|
-
/** The two templates: a Cordis app, and the same app with the offline agent. Both depend on `cordis` only. */
|
|
104
|
-
export declare const TEMPLATES: readonly ["app", "agent"];
|
|
105
|
-
type Template = (typeof TEMPLATES)[number];
|
|
106
|
-
/** The template for an agent choice. */
|
|
107
|
-
export declare function templateFor(agent: boolean): Template;
|
|
108
|
-
/**
|
|
109
|
-
* The `templates/` folder of this package. The module sits at different depths in source (`src/`) and in the build
|
|
110
|
-
* (`dist/`), so walk up until a `templates` folder holds the `app` template.
|
|
111
|
-
*/
|
|
112
|
-
export declare function templatesRoot(): string;
|
|
113
|
-
/** Absolute path of a template folder. */
|
|
114
|
-
export declare function templateDir(template: Template): string;
|
|
115
|
-
//#endregion
|
|
116
117
|
export type { AddOptions, InitOptions, Io, LibraryPlugin, Template };
|
|
117
118
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,11 @@ import { spawn } from "node:child_process";
|
|
|
7
7
|
import { createInterface } from "node:readline/promises";
|
|
8
8
|
//#region src/library.ts
|
|
9
9
|
/** Files of a library plugin that stay in the repository: its manifest and its tests. */
|
|
10
|
-
const LIBRARY_ONLY = /* @__PURE__ */ new Set([
|
|
10
|
+
const LIBRARY_ONLY = /* @__PURE__ */ new Set([
|
|
11
|
+
"plugin.json",
|
|
12
|
+
"test",
|
|
13
|
+
"tests"
|
|
14
|
+
]);
|
|
11
15
|
var LibraryError = class extends Error {
|
|
12
16
|
name = "LibraryError";
|
|
13
17
|
};
|
|
@@ -16,7 +20,7 @@ function libraryRoot() {
|
|
|
16
20
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
17
21
|
for (;;) {
|
|
18
22
|
const candidate = join(dir, "plugins");
|
|
19
|
-
if (existsSync(join(candidate, "
|
|
23
|
+
if (existsSync(join(candidate, "instance", "plugin.json"))) return candidate;
|
|
20
24
|
const parent = dirname(dir);
|
|
21
25
|
if (parent === dir) throw new LibraryError("acrylblends cannot find its plugin library (plugins/)");
|
|
22
26
|
dir = parent;
|
|
@@ -62,7 +66,10 @@ function copyPlugin(plugin, appDir, root = libraryRoot()) {
|
|
|
62
66
|
const source = join(root, plugin.name);
|
|
63
67
|
cpSync(source, target, {
|
|
64
68
|
recursive: true,
|
|
65
|
-
filter: (path) =>
|
|
69
|
+
filter: (path) => {
|
|
70
|
+
const inside = path.slice(source.length + 1);
|
|
71
|
+
return !(dirname(path) === source && LIBRARY_ONLY.has(inside)) && !plugin.omit?.includes(inside);
|
|
72
|
+
}
|
|
66
73
|
});
|
|
67
74
|
return target;
|
|
68
75
|
}
|
|
@@ -101,7 +108,7 @@ function copyTemplate(source, target, name) {
|
|
|
101
108
|
const written = [];
|
|
102
109
|
const walk = (from, to, prefix) => {
|
|
103
110
|
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
104
|
-
if (entry.name === "node_modules" || prefix === "" && entry.name === "template.json") continue;
|
|
111
|
+
if (entry.name === "node_modules" || prefix === "" && (entry.name === "template.json" || entry.name === "upstream.json")) continue;
|
|
105
112
|
const sourcePath = join(from, entry.name);
|
|
106
113
|
const targetName = prefix === "" && (entry.name === "gitignore" || entry.name === "dockerignore") ? `.${entry.name}` : entry.name;
|
|
107
114
|
const targetPath = join(to, targetName);
|
|
@@ -119,7 +126,7 @@ function copyTemplate(source, target, name) {
|
|
|
119
126
|
walk(source, target, "");
|
|
120
127
|
return written;
|
|
121
128
|
}
|
|
122
|
-
/** A template's own manifest, `template.json`: the library plugins it is built from. Never copied. */
|
|
129
|
+
/** 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). */
|
|
123
130
|
const TEMPLATE_MANIFEST = "template.json";
|
|
124
131
|
function readTemplatePlugins(templateDir) {
|
|
125
132
|
const file = join(templateDir, TEMPLATE_MANIFEST);
|
|
@@ -143,21 +150,24 @@ function mergeDependencies(appDir, dependencies) {
|
|
|
143
150
|
}
|
|
144
151
|
//#endregion
|
|
145
152
|
//#region src/templates.ts
|
|
146
|
-
/** The
|
|
147
|
-
const TEMPLATES = [
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
/** 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. */
|
|
154
|
+
const TEMPLATES = [
|
|
155
|
+
"blank",
|
|
156
|
+
"todo",
|
|
157
|
+
"agent"
|
|
158
|
+
];
|
|
159
|
+
function isTemplate(value) {
|
|
160
|
+
return TEMPLATES.includes(value);
|
|
151
161
|
}
|
|
152
162
|
/**
|
|
153
163
|
* The `templates/` folder of this package. The module sits at different depths in source (`src/`) and in the build
|
|
154
|
-
* (`dist/`), so walk up until a `templates` folder holds the `
|
|
164
|
+
* (`dist/`), so walk up until a `templates` folder holds the `blank` template.
|
|
155
165
|
*/
|
|
156
166
|
function templatesRoot() {
|
|
157
167
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
158
168
|
for (;;) {
|
|
159
169
|
const candidate = join(dir, "templates");
|
|
160
|
-
if (existsSync(join(candidate, "
|
|
170
|
+
if (existsSync(join(candidate, "blank"))) return candidate;
|
|
161
171
|
const parent = dirname(dir);
|
|
162
172
|
if (parent === dir) throw new Error("acrylblends cannot find its templates folder");
|
|
163
173
|
dir = parent;
|
|
@@ -173,7 +183,7 @@ function templateDir(template) {
|
|
|
173
183
|
var UsageError = class extends Error {
|
|
174
184
|
name = "UsageError";
|
|
175
185
|
};
|
|
176
|
-
/** Parse `init [dir] [--agent|--no-agent] [--name n] [--install] [-y]`. */
|
|
186
|
+
/** Parse `init [dir] [--template t] [--agent|--no-agent] [--name n] [--install] [-y]`. `--agent` means `--template agent`, `--no-agent` means `--template blank`. */
|
|
177
187
|
function parseInitOptions(argv) {
|
|
178
188
|
let parsed;
|
|
179
189
|
try {
|
|
@@ -184,10 +194,13 @@ function parseInitOptions(argv) {
|
|
|
184
194
|
const { values, positionals } = parsed;
|
|
185
195
|
if (positionals.length > 1) throw new UsageError("init takes at most one directory");
|
|
186
196
|
if (values.agent && values["no-agent"]) throw new UsageError("use --agent or --no-agent, not both");
|
|
197
|
+
if (values.template !== void 0 && !isTemplate(values.template)) throw new UsageError(`unknown template "${values.template}" (${TEMPLATES.join(", ")})`);
|
|
198
|
+
const alias = values.agent ? "agent" : values["no-agent"] ? "blank" : void 0;
|
|
199
|
+
if (values.template !== void 0 && alias !== void 0 && values.template !== alias) throw new UsageError(`--template ${values.template} contradicts --${alias === "agent" ? "agent" : "no-agent"}`);
|
|
187
200
|
return {
|
|
188
201
|
directory: positionals[0],
|
|
189
202
|
name: values.name,
|
|
190
|
-
|
|
203
|
+
template: values.template ?? alias,
|
|
191
204
|
install: values.install,
|
|
192
205
|
yes: values.yes === true
|
|
193
206
|
};
|
|
@@ -198,6 +211,7 @@ const parse$1 = (argv) => parseArgs({
|
|
|
198
211
|
strict: true,
|
|
199
212
|
options: {
|
|
200
213
|
name: { type: "string" },
|
|
214
|
+
template: { type: "string" },
|
|
201
215
|
agent: { type: "boolean" },
|
|
202
216
|
"no-agent": { type: "boolean" },
|
|
203
217
|
install: { type: "boolean" },
|
|
@@ -208,6 +222,12 @@ const parse$1 = (argv) => parseArgs({
|
|
|
208
222
|
}
|
|
209
223
|
});
|
|
210
224
|
const yes = (answer) => ["y", "yes"].includes(answer.trim().toLowerCase());
|
|
225
|
+
async function askTemplate(ask) {
|
|
226
|
+
const answer = (await ask(`Template (${TEMPLATES.join(", ")}) [blank]: `)).trim().toLowerCase();
|
|
227
|
+
if (answer === "") return "blank";
|
|
228
|
+
if (isTemplate(answer)) return answer;
|
|
229
|
+
throw new UsageError(`unknown template "${answer}" (${TEMPLATES.join(", ")})`);
|
|
230
|
+
}
|
|
211
231
|
/** Scaffold a Cordis app. Returns the exit code: 0 done, 1 refused or failed. Never overwrites anything. */
|
|
212
232
|
async function init(options, io) {
|
|
213
233
|
const relative = options.directory ?? ".";
|
|
@@ -216,9 +236,8 @@ async function init(options, io) {
|
|
|
216
236
|
let name = options.name ?? nameFrom(directory);
|
|
217
237
|
if (options.name === void 0 && ask) name = (await ask(`Project name (${name}): `)).trim() || name;
|
|
218
238
|
if (!isValidName(name)) throw new UsageError(`"${name}" is not a valid project name (lower case letters, digits and dashes, starting with a letter)`);
|
|
219
|
-
const
|
|
239
|
+
const template = options.template ?? (ask ? await askTemplate(ask) : "blank");
|
|
220
240
|
const install = options.install ?? (ask ? yes(await ask("Install dependencies with npm now? (y/N): ")) : false);
|
|
221
|
-
const template = templateFor(agent);
|
|
222
241
|
let files;
|
|
223
242
|
try {
|
|
224
243
|
files = copyTemplate(templateDir(template), directory, name);
|
|
@@ -393,10 +412,11 @@ Usage:
|
|
|
393
412
|
acrylblends plugins List the plugin library
|
|
394
413
|
|
|
395
414
|
Options for init:
|
|
396
|
-
--
|
|
415
|
+
--template <name> blank or agent (asked when not given); --agent means --template agent,
|
|
416
|
+
--no-agent means --template blank
|
|
397
417
|
--name <name> the project name (default: the directory name)
|
|
398
418
|
--install run npm install after copying
|
|
399
|
-
-y, --yes ask nothing; use defaults (
|
|
419
|
+
-y, --yes ask nothing; use defaults (the blank template, no install)
|
|
400
420
|
|
|
401
421
|
-h, --help show this help
|
|
402
422
|
-v, --version show the version
|
|
@@ -429,6 +449,6 @@ async function runCli(argv, io = processIo()) {
|
|
|
429
449
|
}
|
|
430
450
|
}
|
|
431
451
|
//#endregion
|
|
432
|
-
export { HELP, TEMPLATES, TargetNotEmptyError, UsageError, VERSION, add, assertTargetFree, closure, copyPlugin, copyTemplate, init, isValidName, libraryRoot, listPlugins, mergeDependencies, nameFrom, parseAddOptions, parseInitOptions, readLibrary, readTemplatePlugins, runCli, templateDir,
|
|
452
|
+
export { HELP, TEMPLATES, TargetNotEmptyError, UsageError, VERSION, add, assertTargetFree, closure, copyPlugin, copyTemplate, init, isTemplate, isValidName, libraryRoot, listPlugins, mergeDependencies, nameFrom, parseAddOptions, parseInitOptions, readLibrary, readTemplatePlugins, runCli, templateDir, templatesRoot };
|
|
433
453
|
|
|
434
454
|
//# sourceMappingURL=index.js.map
|
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 } 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\n/** Files of a library plugin that stay in the repository: its manifest and its tests. */\nconst LIBRARY_ONLY = new Set(['plugin.json', 'test'])\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, 'http', '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 !(dirname(path) === source && LIBRARY_ONLY.has(path.slice(source.length + 1))),\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 (entry.name === 'node_modules' || (prefix === '' && entry.name === TEMPLATE_MANIFEST))\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. */\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 two templates: a Cordis app, and the same app with the offline agent. Both depend on `cordis` only. */\nexport const TEMPLATES = ['app', 'agent'] as const\nexport type Template = (typeof TEMPLATES)[number]\n\n/** The template for an agent choice. */\nexport function templateFor(agent: boolean): Template {\n return agent ? 'agent' : 'app'\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 `app` 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, 'app'))) 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 { templateDir, templateFor } 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 agent: boolean | undefined\n install: boolean | undefined\n yes: boolean\n}\n\n/** Parse `init [dir] [--agent|--no-agent] [--name n] [--install] [-y]`. */\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 return {\n directory: positionals[0],\n name: values.name,\n agent: values.agent ? true : values['no-agent'] ? false : undefined,\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 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\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 agent =\n options.agent ?? (ask ? yes(await ask('Include the offline agent? (y/N): ')) : false)\n const install =\n options.install ?? (ask ? yes(await ask('Install dependencies with npm now? (y/N): ')) : false)\n\n const template = templateFor(agent)\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 --agent / --no-agent include the offline agent (asked when not given)\n --name <name> the project name (default: the directory name)\n --install run npm install after copying\n -y, --yes ask nothing; use defaults (no agent, 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 } 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 /** Paths inside the plugin folder that are not copied into an app (for example a React hook an app without React cannot type-check). */\n readonly omit?: readonly string[]\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)) && !plugin.omit?.includes(inside)\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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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WebBoxes s.r.o.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "acryl-inspect",
|
|
3
|
+
"description": "Introspection: what every plugin requires, provides and registers, and what unloads with it. Vendored from ACRYL.",
|
|
4
|
+
"requires": ["acryl-router"],
|
|
5
|
+
"inject": ["router"],
|
|
6
|
+
"provides": ["inspect"],
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@deepseek-ai/cordis": "4.0.4"
|
|
9
|
+
},
|
|
10
|
+
"mount": "root.plugin(inspect); ctx.inspect.plugins(), ctx.inspect.explain('name'), ctx.inspect.render()"
|
|
11
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-plugin contract, read from the running system (spec 046, M4). Cordis already knows a plugin's name, its state, the
|
|
3
|
+
* services it requires (`inject`) and a labeled tree of everything it registered (`getEffects()`), so there is no second
|
|
4
|
+
* declaration to keep in step with the code. The only convention is the label: an effect says what it is, prefixed by its
|
|
5
|
+
* owner (`acryl-router: GET /a`). Pure functions over a structural view of a fiber, so they are testable without Cordis.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The plugin states Cordis defines, by their numeric values (`FiberState`). */
|
|
9
|
+
const STATE_NAMES = ['PENDING', 'LOADING', 'ACTIVE', 'FAILED', 'DISPOSED', 'UNLOADING'] as const
|
|
10
|
+
export type FiberStateName = typeof STATE_NAMES[number]
|
|
11
|
+
|
|
12
|
+
export interface EffectLabel {
|
|
13
|
+
readonly label: string
|
|
14
|
+
readonly children: readonly EffectLabel[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The slice of a Cordis fiber this module reads. */
|
|
18
|
+
export interface FiberView {
|
|
19
|
+
readonly state: number
|
|
20
|
+
readonly inject: Readonly<Record<string, unknown>>
|
|
21
|
+
readonly runtime: { readonly name?: string | undefined } | null
|
|
22
|
+
/** The context the fiber was loaded from; its own `fiber` is the owning plugin when this fiber is an inline `ctx.inject(...)` callback. */
|
|
23
|
+
readonly parent?: { readonly fiber?: FiberView | undefined } | undefined
|
|
24
|
+
getEffects(): readonly EffectLabel[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RouteReport {
|
|
28
|
+
readonly method: string
|
|
29
|
+
readonly path: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PluginReport {
|
|
33
|
+
readonly name: string
|
|
34
|
+
readonly state: FiberStateName
|
|
35
|
+
/** Services the plugin waits for before it starts. */
|
|
36
|
+
readonly requires: readonly string[]
|
|
37
|
+
/** Services it provides. */
|
|
38
|
+
readonly provides: readonly string[]
|
|
39
|
+
/** Routes it registered on the router. */
|
|
40
|
+
readonly routes: readonly RouteReport[]
|
|
41
|
+
/** Router middleware paths it registered. */
|
|
42
|
+
readonly middleware: readonly string[]
|
|
43
|
+
/** Every other labeled registration (event listeners, timers, commands, tools). */
|
|
44
|
+
readonly other: readonly string[]
|
|
45
|
+
/** How many registrations disappear when it unloads: all of them. */
|
|
46
|
+
readonly removedOnUnload: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const PROVIDE = /^ctx\.provide\("(.+)"\)$/
|
|
50
|
+
const ROUTE = /^acryl-router: (GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|\*) (\/\S*)$/
|
|
51
|
+
const USE = /^acryl-router: use (\/\S*)$/
|
|
52
|
+
|
|
53
|
+
function flatten(effects: readonly EffectLabel[]): string[] {
|
|
54
|
+
return effects.flatMap(effect => [effect.label, ...flatten(effect.children)])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function stateName(state: number): FiberStateName {
|
|
58
|
+
return STATE_NAMES[state] ?? 'FAILED'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isNamed = (fiber: FiberView): boolean => (fiber.runtime?.name ?? '') !== ''
|
|
62
|
+
|
|
63
|
+
/** The named plugin a fiber belongs to: itself, or for an unnamed inline callback (`ctx.inject([...], inner => ...)`) the nearest named ancestor. */
|
|
64
|
+
export function ownerOf(fiber: FiberView): FiberView {
|
|
65
|
+
let current = fiber
|
|
66
|
+
while (!isNamed(current)) {
|
|
67
|
+
const parent = current.parent?.fiber
|
|
68
|
+
if (parent === undefined || parent === current) return fiber
|
|
69
|
+
current = parent
|
|
70
|
+
}
|
|
71
|
+
return current
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Describe one fiber, counting the registrations of the unnamed inline callbacks it owns (`adopted`) as its own. */
|
|
75
|
+
export function reportFiber(fiber: FiberView, adopted: readonly FiberView[] = []): PluginReport {
|
|
76
|
+
const provides: string[] = []
|
|
77
|
+
const routes: RouteReport[] = []
|
|
78
|
+
const middleware: string[] = []
|
|
79
|
+
const other: string[] = []
|
|
80
|
+
const labels = [fiber, ...adopted].flatMap(source => flatten(source.getEffects()))
|
|
81
|
+
for (const label of labels) {
|
|
82
|
+
const provided = PROVIDE.exec(label)
|
|
83
|
+
const route = ROUTE.exec(label)
|
|
84
|
+
const used = USE.exec(label)
|
|
85
|
+
if (provided?.[1] !== undefined) provides.push(provided[1])
|
|
86
|
+
else if (route?.[1] !== undefined && route[2] !== undefined) routes.push({ method: route[1], path: route[2] })
|
|
87
|
+
else if (used?.[1] !== undefined) middleware.push(used[1])
|
|
88
|
+
else other.push(label)
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
name: isNamed(fiber) ? fiber.runtime?.name ?? 'anonymous' : 'anonymous',
|
|
92
|
+
state: stateName(fiber.state),
|
|
93
|
+
requires: Object.keys(fiber.inject).sort(),
|
|
94
|
+
provides: provides.sort(),
|
|
95
|
+
routes,
|
|
96
|
+
middleware,
|
|
97
|
+
other,
|
|
98
|
+
removedOnUnload: labels.length,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const list = (items: readonly string[]): string => items.length === 0 ? 'nothing' : items.join(', ')
|
|
103
|
+
|
|
104
|
+
/** A short, human explanation of one plugin: what it needs, what it adds, and what happens when it unloads. */
|
|
105
|
+
export function explain(report: PluginReport): string {
|
|
106
|
+
const lines = [
|
|
107
|
+
`${report.name} (${report.state})`,
|
|
108
|
+
` needs: ${list(report.requires)}`,
|
|
109
|
+
` provides: ${list(report.provides)}`,
|
|
110
|
+
` routes: ${list(report.routes.map(route => `${route.method} ${route.path}`))}`,
|
|
111
|
+
` middleware:${report.middleware.length === 0 ? ' nothing' : ` ${report.middleware.join(', ')}`}`,
|
|
112
|
+
]
|
|
113
|
+
if (report.other.length > 0) lines.push(` also: ${report.other.join('; ')}`)
|
|
114
|
+
lines.push(report.state === 'PENDING'
|
|
115
|
+
? ` waiting: it starts when ${list(report.requires)} ${report.requires.length === 1 ? 'is' : 'are'} available`
|
|
116
|
+
: ` unloading: removes ${String(report.removedOnUnload)} registration${report.removedOnUnload === 1 ? '' : 's'} and nothing else`)
|
|
117
|
+
return lines.join('\n')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One block per plugin, in registration order. */
|
|
121
|
+
export function renderReports(reports: readonly PluginReport[]): string {
|
|
122
|
+
return reports.map(explain).join('\n\n')
|
|
123
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ACRYL inspector (`ctx.inspect`): what every running plugin requires, provides and registers, read from the live
|
|
3
|
+
* registry. It owns no state and registers nothing of its own beyond an optional read-only pair of router routes, which are
|
|
4
|
+
* reachable in process unless some plugin mounts their prefix over HTTP.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Context, Service } from '@deepseek-ai/cordis'
|
|
8
|
+
import { loopbackOnly } from '../../acryl-router/src/middleware/index.ts'
|
|
9
|
+
import type {} from '../../acryl-router/src/index.ts'
|
|
10
|
+
import { explain, ownerOf, renderReports, reportFiber, type FiberView, type PluginReport } from './report.ts'
|
|
11
|
+
|
|
12
|
+
declare module '@deepseek-ai/cordis' {
|
|
13
|
+
interface Context {
|
|
14
|
+
inspect: Inspect
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
19
|
+
export const name = 'acryl-inspect'
|
|
20
|
+
|
|
21
|
+
export class Inspect extends Service {
|
|
22
|
+
constructor(ctx: Context) {
|
|
23
|
+
super(ctx, 'inspect')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A report for every live plugin instance, in registry order. */
|
|
27
|
+
plugins(): PluginReport[] {
|
|
28
|
+
const fibers = [...this.ctx.registry.values()].flatMap(runtime => [...runtime.fibers] as unknown as FiberView[])
|
|
29
|
+
// An unnamed inline callback is part of the plugin that wrote it: its routes and mounts are that plugin's.
|
|
30
|
+
const adopted = new Map<FiberView, FiberView[]>()
|
|
31
|
+
const owners: FiberView[] = []
|
|
32
|
+
for (const fiber of fibers) {
|
|
33
|
+
const owner = ownerOf(fiber)
|
|
34
|
+
if (owner === fiber) owners.push(fiber)
|
|
35
|
+
else adopted.set(owner, [...(adopted.get(owner) ?? []), fiber])
|
|
36
|
+
}
|
|
37
|
+
return owners.map(fiber => reportFiber(fiber, adopted.get(fiber) ?? []))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The text explanation of the plugin called `pluginName`, or `undefined` when none is registered. */
|
|
41
|
+
explain(pluginName: string): string | undefined {
|
|
42
|
+
const found = this.plugins().filter(report => report.name === pluginName)
|
|
43
|
+
return found.length === 0 ? undefined : found.map(explain).join('\n\n')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The whole system as text. */
|
|
47
|
+
render(): string {
|
|
48
|
+
return renderReports(this.plugins())
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function apply(ctx: Context): void {
|
|
53
|
+
const inspect = new Inspect(ctx)
|
|
54
|
+
// The same facts on the router, for any surface; present only where a router is. A request over a socket must come from this machine to this app;
|
|
55
|
+
// the terminal calls the router in process and is the host's own code.
|
|
56
|
+
ctx.inject(['router'], inner => {
|
|
57
|
+
const api = inner.router.group('/api/inspect', loopbackOnly({ inProcess: 'allow' }))
|
|
58
|
+
api.get('/plugins', c => c.json(inspect.plugins()))
|
|
59
|
+
api.get('/plugins/:name', c => {
|
|
60
|
+
const text = inspect.explain(c.param('name') ?? '')
|
|
61
|
+
return text === undefined ? c.json({ error: 'no such plugin' }, 404) : c.text(text)
|
|
62
|
+
})
|
|
63
|
+
// Over HTTP only where an adapter exists (Web and Desktop); the terminal needs none.
|
|
64
|
+
inner.inject(['routerHttp'], http => { http.routerHttp.mount('/api/inspect') })
|
|
65
|
+
})
|
|
66
|
+
}
|