@fougere/oclif 0.10.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fougere contributors
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,27 @@
1
+ /**
2
+ * An operation's input contract, as oclif's args and flags.
3
+ *
4
+ * The same derivation `@fougere/cli` does for citty, against another target: what an entity
5
+ * states is read once — a closed set becomes an `enum`, a `default(v)` is shown in `--help`,
6
+ * and a field's own sentence is its description. Nothing is written down twice.
7
+ *
8
+ * The ARGUMENTS come from the operation's own input, never from the entity: `list` accepts
9
+ * nothing while `create` accepts a draft, and an entity-wide derivation would demand a price
10
+ * to read a list.
11
+ */
12
+ import { type Fields } from '@fougere/schema';
13
+ import type { ArgInput, FlagInput } from '@oclif/core/interfaces';
14
+ export interface Shape {
15
+ args: ArgInput;
16
+ flags: FlagInput;
17
+ }
18
+ export declare function paramsToShape(params: readonly {
19
+ name: string;
20
+ type: {
21
+ name?: string;
22
+ };
23
+ optional?: boolean;
24
+ }[]): Shape;
25
+ /** One operation's input, as the pair oclif parses a command line into. */
26
+ export declare function inputToShape(fields: Fields): Shape;
27
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAuC,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEnF,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAclE,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,SAAS,CAAC;CAClB;AAcD,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,GAAG,KAAK,CAwBrH;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAmClD"}
package/dist/bridge.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * An operation's input contract, as oclif's args and flags.
3
+ *
4
+ * The same derivation `@fougere/cli` does for citty, against another target: what an entity
5
+ * states is read once — a closed set becomes an `enum`, a `default(v)` is shown in `--help`,
6
+ * and a field's own sentence is its description. Nothing is written down twice.
7
+ *
8
+ * The ARGUMENTS come from the operation's own input, never from the entity: `list` accepts
9
+ * nothing while `create` accepts a draft, and an entity-wide derivation would demand a price
10
+ * to read a list.
11
+ */
12
+ import { Lifecycle, Role, Shapes, Visibility } from '@fougere/schema';
13
+ import { Args, Flags } from '@oclif/core';
14
+ /** What a caller supplies: the fields the axes admit, or the PRIMARY when they admit nothing. */
15
+ function suppliedIn(fields) {
16
+ const written = Visibility.of(fields).input;
17
+ if (Object.keys(written).length > 0)
18
+ return written;
19
+ return Object.fromEntries(Object.entries(fields).filter(([, field]) => Role.of(field).isPrimary));
20
+ }
21
+ const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
22
+ /**
23
+ * A parameter the scan read off the signature, when the operation names no view.
24
+ *
25
+ * `Crud(Product).findById(id: string)` takes a bare string — there is no entity to read axes
26
+ * from, and the terminal would otherwise offer nothing at all. The signature is what remains,
27
+ * and it says the name, the type, and whether it is optional.
28
+ *
29
+ * A parameter the framework fills is skipped: `user?: User` is resolved from the session, and
30
+ * `invocation` is the envelope. Neither is a caller's to type.
31
+ */
32
+ const RESOLVED = new Set(['user', 'invocation', 'ctx', 'context']);
33
+ export function paramsToShape(params) {
34
+ const args = {};
35
+ const flags = {};
36
+ let positional = false;
37
+ for (const param of params) {
38
+ if (RESOLVED.has(param.name))
39
+ continue;
40
+ const type = param.type.name ?? 'string';
41
+ if (!/^(string|number|boolean)$/.test(type))
42
+ continue;
43
+ const required = param.optional !== true;
44
+ if (!positional && required && type === 'string') {
45
+ args[param.name] = Args.string({ required: true });
46
+ positional = true;
47
+ continue;
48
+ }
49
+ const at = kebab(param.name);
50
+ flags[at] = type === 'boolean' ? Flags.boolean({})
51
+ : type === 'number' ? Flags.integer({ required })
52
+ : Flags.string({ required });
53
+ }
54
+ return { args: args, flags: flags };
55
+ }
56
+ /** One operation's input, as the pair oclif parses a command line into. */
57
+ export function inputToShape(fields) {
58
+ const args = {};
59
+ const flags = {};
60
+ let positional = false;
61
+ for (const [key, field] of Object.entries(suppliedIn(fields))) {
62
+ if (Role.of(field).isRelation)
63
+ continue;
64
+ const { base: shape, nullable } = Shapes.of(field.shape);
65
+ const type = Shapes.typeOf(field.shape);
66
+ const description = field.meta?.description;
67
+ const required = Role.of(field).isPrimary || (!nullable && Lifecycle.of(field).requiredAtCreate);
68
+ const options = shape?.type === 'string' && shape.enum?.length
69
+ ? shape.enum.filter((value) => typeof value === 'string')
70
+ : undefined;
71
+ // The first required plain string becomes positional, the rule `fougere explain --root`
72
+ // and `fougere graph <root>` already follow. A closed set stays a flag: its legal values
73
+ // read better named than guessed by position.
74
+ if (!positional && required && type === 'text' && !options) {
75
+ args[key] = Args.string({ description, required: true });
76
+ positional = true;
77
+ continue;
78
+ }
79
+ const at = kebab(key);
80
+ const fallback = Lifecycle.of(field).literal?.value;
81
+ flags[at] = options
82
+ ? Flags.string({ description, options, required, default: fallback })
83
+ : type === 'boolean' ? Flags.boolean({ description, default: fallback })
84
+ : type === 'number' ? Flags.integer({ description, required, default: fallback })
85
+ : Flags.string({ description, required, default: fallback });
86
+ }
87
+ return { args: args, flags: flags };
88
+ }
89
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * A frond's operations, as a terminal.
3
+ *
4
+ * oclif finds its commands in FILES at build time; a frond's are known only after the scan, so
5
+ * they are built here and handed over as one plugin. What that buys, beside a command per
6
+ * operation: topics (`product:list` groups under `product` on its own), `--help` per topic and
7
+ * per command, `--json`, and a parser that refuses a missing flag or a value outside a closed
8
+ * set before anything runs.
9
+ *
10
+ * It is an HOST, at the same rank as `@fougere/nuxt` — the frond decides nothing about it, and
11
+ * `@fougere/cli` stays on citty: its fifteen commands are flat, one op each, and want none of
12
+ * the topics that make this worth its weight.
13
+ */
14
+ import { Command } from '@oclif/core';
15
+ import type { App } from '@fougere/core';
16
+ import { type Shape } from './bridge.js';
17
+ /**
18
+ * What oclif runs: a command's shape in its cache, plus the way to reach the class.
19
+ *
20
+ * Written here rather than imported: `Command.Loadable` lives behind an entry `@oclif/core`
21
+ * does not export, and what this package builds is exactly these fields.
22
+ */
23
+ interface Loadable {
24
+ id: string;
25
+ aliases: string[];
26
+ args: Shape['args'];
27
+ flags: Shape['flags'];
28
+ description?: string;
29
+ hidden: boolean;
30
+ pluginAlias: string;
31
+ pluginName: string;
32
+ pluginType: string;
33
+ load(): Promise<typeof Command>;
34
+ }
35
+ /**
36
+ * Every operation of every frond, as oclif sees them.
37
+ *
38
+ * The identifier is `address:op`, which is what makes a topic: oclif groups by the part before
39
+ * the colon, so `product:list` and `product:create` answer under `product` without anything
40
+ * saying so.
41
+ */
42
+ export declare function commandsOf(app: App): Loadable[];
43
+ /**
44
+ * Run the app's operations as a CLI.
45
+ *
46
+ * The commands are inserted through the door oclif keeps for plugins that produce theirs at
47
+ * runtime. Its name says `legacy` — it exists for Heroku's older plugins — and it is the only
48
+ * entry that takes commands nobody read from disk; the alternative is writing a file per
49
+ * operation, which is the fact this package exists to derive.
50
+ */
51
+ export declare function serve(app: App, argv?: string[]): Promise<void>;
52
+ export {};
53
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,OAAO,EAAmC,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,GAAG,EAAmB,MAAM,eAAe,CAAC;AAC1D,OAAO,EAA+B,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEtE;;;;;GAKG;AACH,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,IAAI,OAAO,CAAC,OAAO,OAAO,CAAC,CAAC;CACjC;AA8BD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,QAAQ,EAAE,CAyC/C;AAED;;;;;;;GAOG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC3F"}
package/dist/index.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * A frond's operations, as a terminal.
3
+ *
4
+ * oclif finds its commands in FILES at build time; a frond's are known only after the scan, so
5
+ * they are built here and handed over as one plugin. What that buys, beside a command per
6
+ * operation: topics (`product:list` groups under `product` on its own), `--help` per topic and
7
+ * per command, `--json`, and a parser that refuses a missing flag or a value outside a closed
8
+ * set before anything runs.
9
+ *
10
+ * It is an HOST, at the same rank as `@fougere/nuxt` — the frond decides nothing about it, and
11
+ * `@fougere/cli` stays on citty: its fifteen commands are flat, one op each, and want none of
12
+ * the topics that make this worth its weight.
13
+ */
14
+ import { Command, Config, handle, run as runOclif } from '@oclif/core';
15
+ import { inputToShape, paramsToShape } from './bridge.js';
16
+ const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).replace(/^-/, '');
17
+ /** Each entry carries the key it is filed under — what a class-on-disk gets for free. */
18
+ function named(entries) {
19
+ return Object.fromEntries(Object.entries(entries).map(([name, entry]) => [name, { ...entry, name }]));
20
+ }
21
+ /** One operation, as the class oclif runs. */
22
+ function commandFor(id, call, shape, describe) {
23
+ const Built = class extends Command {
24
+ static args = shape.args;
25
+ static flags = shape.flags;
26
+ static description = describe;
27
+ static enableJsonFlag = true;
28
+ async run() {
29
+ const { args, flags } = await this.parse(Built);
30
+ return this.logJson(await call({ ...args, ...flags }));
31
+ }
32
+ };
33
+ Built.id = id;
34
+ return Built;
35
+ }
36
+ /**
37
+ * Every operation of every frond, as oclif sees them.
38
+ *
39
+ * The identifier is `address:op`, which is what makes a topic: oclif groups by the part before
40
+ * the colon, so `product:list` and `product:create` answer under `product` without anything
41
+ * saying so.
42
+ */
43
+ export function commandsOf(app) {
44
+ const loadables = [];
45
+ for (const frond of app.fronds) {
46
+ for (const handler of frond.handlers) {
47
+ const facade = app.facadeFor(handler.address);
48
+ for (const [name, contract] of handler.operations ?? []) {
49
+ // A view when the op names one, its signature otherwise: `findById(id: string)` takes
50
+ // a bare parameter, and an entity-shaped derivation would offer nothing at all for it.
51
+ const fields = contract.input?.getFields?.();
52
+ const shape = fields
53
+ ? inputToShape(fields)
54
+ : paramsToShape(contract.signature?.params ?? []);
55
+ const Built = commandFor(`${handler.address}:${kebab(name)}`, (parsed) => facade[name](fields ? { input: parsed } : { params: parsed }), shape, contract.description);
56
+ loadables.push({
57
+ id: Built.id,
58
+ aliases: [],
59
+ // `--help` renders from the CACHED shapes, and oclif fills each entry's `name` when
60
+ // it reads a class off disk. Nothing reads these off disk, so they are named here —
61
+ // without it the renderer meets `undefined.toUpperCase()`.
62
+ args: named(Built.args),
63
+ flags: named(Built.flags),
64
+ description: Built.description,
65
+ hidden: false,
66
+ pluginAlias: 'fougere',
67
+ pluginName: 'fougere',
68
+ pluginType: 'core',
69
+ load: async () => Built,
70
+ });
71
+ }
72
+ }
73
+ }
74
+ return loadables;
75
+ }
76
+ /**
77
+ * Run the app's operations as a CLI.
78
+ *
79
+ * The commands are inserted through the door oclif keeps for plugins that produce theirs at
80
+ * runtime. Its name says `legacy` — it exists for Heroku's older plugins — and it is the only
81
+ * entry that takes commands nobody read from disk; the alternative is writing a file per
82
+ * operation, which is the fact this package exists to derive.
83
+ */
84
+ export async function serve(app, argv = process.argv.slice(2)) {
85
+ const config = await Config.load({ root: process.cwd() });
86
+ const commands = commandsOf(app);
87
+ // `insertLegacyPlugins` is typed private: it exists for Heroku's older plugins, and it is the
88
+ // only entry that takes commands nobody read from disk. The cast is the whole risk this
89
+ // package carries — the alternative is a file per operation, which is the fact it derives.
90
+ const insert = config;
91
+ insert.insertLegacyPlugins([{
92
+ name: 'fougere',
93
+ alias: 'fougere',
94
+ commands,
95
+ commandIDs: commands.map((one) => one.id),
96
+ topics: [],
97
+ hooks: {},
98
+ isRoot: false,
99
+ moduleType: 'module',
100
+ pjson: { name: 'fougere', version: '0.0.0', oclif: {} },
101
+ root: process.cwd(),
102
+ type: 'core',
103
+ valid: true,
104
+ version: '0.0.0',
105
+ _base: '',
106
+ hasManifest: false,
107
+ options: {},
108
+ commandsDir: undefined,
109
+ load: async () => { },
110
+ findCommand: async (id) => commands.find((one) => one.id === id)?.load(),
111
+ }]);
112
+ await runOclif(argv, config).catch(handle);
113
+ }
114
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@fougere/oclif",
3
+ "version": "0.10.0-alpha.0",
4
+ "description": "A frond's operations, as a terminal: topics, flags and help derived from what it declares.",
5
+ "keywords": [
6
+ "fougere",
7
+ "oclif",
8
+ "cli"
9
+ ],
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/chok/fougere.git",
14
+ "directory": "packages/app/oclif"
15
+ },
16
+ "type": "module",
17
+ "main": "dist/index.js",
18
+ "types": "dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "template"
29
+ ],
30
+ "dependencies": {
31
+ "@fougere/core": "0.10.0-alpha.0",
32
+ "@fougere/schema": "0.10.0-alpha.0"
33
+ },
34
+ "devDependencies": {
35
+ "@oclif/core": "^5.0.0",
36
+ "vitest": "^4.1.2"
37
+ },
38
+ "peerDependencies": {
39
+ "@oclif/core": "^4.0.0 || ^5.0.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "rm -rf dist && tsc",
46
+ "test": "vitest run",
47
+ "typecheck": "tsc --noEmit -p tsconfig.test.json"
48
+ }
49
+ }
package/src/bridge.ts ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * An operation's input contract, as oclif's args and flags.
3
+ *
4
+ * The same derivation `@fougere/cli` does for citty, against another target: what an entity
5
+ * states is read once — a closed set becomes an `enum`, a `default(v)` is shown in `--help`,
6
+ * and a field's own sentence is its description. Nothing is written down twice.
7
+ *
8
+ * The ARGUMENTS come from the operation's own input, never from the entity: `list` accepts
9
+ * nothing while `create` accepts a draft, and an entity-wide derivation would demand a price
10
+ * to read a list.
11
+ */
12
+ import { Lifecycle, Role, Shapes, Visibility, type Fields } from '@fougere/schema';
13
+ import { Args, Flags } from '@oclif/core';
14
+ import type { ArgInput, FlagInput } from '@oclif/core/interfaces';
15
+
16
+ /** What a caller supplies: the fields the axes admit, or the PRIMARY when they admit nothing. */
17
+ function suppliedIn(fields: Fields): Fields {
18
+ const written = Visibility.of(fields).input;
19
+ if (Object.keys(written).length > 0) return written;
20
+
21
+ return Object.fromEntries(
22
+ Object.entries(fields).filter(([, field]) => Role.of(field).isPrimary),
23
+ ) as Fields;
24
+ }
25
+
26
+ const kebab = (name: string): string => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
27
+
28
+ export interface Shape {
29
+ args: ArgInput;
30
+ flags: FlagInput;
31
+ }
32
+
33
+ /**
34
+ * A parameter the scan read off the signature, when the operation names no view.
35
+ *
36
+ * `Crud(Product).findById(id: string)` takes a bare string — there is no entity to read axes
37
+ * from, and the terminal would otherwise offer nothing at all. The signature is what remains,
38
+ * and it says the name, the type, and whether it is optional.
39
+ *
40
+ * A parameter the framework fills is skipped: `user?: User` is resolved from the session, and
41
+ * `invocation` is the envelope. Neither is a caller's to type.
42
+ */
43
+ const RESOLVED = new Set(['user', 'invocation', 'ctx', 'context']);
44
+
45
+ export function paramsToShape(params: readonly { name: string; type: { name?: string }; optional?: boolean }[]): Shape {
46
+ const args: Record<string, unknown> = {};
47
+ const flags: Record<string, unknown> = {};
48
+ let positional = false;
49
+
50
+ for (const param of params) {
51
+ if (RESOLVED.has(param.name)) continue;
52
+ const type = param.type.name ?? 'string';
53
+ if (!/^(string|number|boolean)$/.test(type)) continue;
54
+
55
+ const required = param.optional !== true;
56
+ if (!positional && required && type === 'string') {
57
+ args[param.name] = Args.string({ required: true });
58
+ positional = true;
59
+ continue;
60
+ }
61
+
62
+ const at = kebab(param.name);
63
+ flags[at] = type === 'boolean' ? Flags.boolean({})
64
+ : type === 'number' ? Flags.integer({ required })
65
+ : Flags.string({ required });
66
+ }
67
+
68
+ return { args: args as ArgInput, flags: flags as FlagInput };
69
+ }
70
+
71
+ /** One operation's input, as the pair oclif parses a command line into. */
72
+ export function inputToShape(fields: Fields): Shape {
73
+ const args: Record<string, unknown> = {};
74
+ const flags: Record<string, unknown> = {};
75
+ let positional = false;
76
+
77
+ for (const [key, field] of Object.entries(suppliedIn(fields))) {
78
+ if (Role.of(field).isRelation) continue;
79
+
80
+ const { base: shape, nullable } = Shapes.of(field.shape);
81
+ const type = Shapes.typeOf(field.shape);
82
+ const description = field.meta?.description;
83
+ const required = Role.of(field).isPrimary || (!nullable && Lifecycle.of(field).requiredAtCreate);
84
+ const options = shape?.type === 'string' && shape.enum?.length
85
+ ? shape.enum.filter((value): value is string => typeof value === 'string')
86
+ : undefined;
87
+
88
+ // The first required plain string becomes positional, the rule `fougere explain --root`
89
+ // and `fougere graph <root>` already follow. A closed set stays a flag: its legal values
90
+ // read better named than guessed by position.
91
+ if (!positional && required && type === 'text' && !options) {
92
+ args[key] = Args.string({ description, required: true });
93
+ positional = true;
94
+ continue;
95
+ }
96
+
97
+ const at = kebab(key);
98
+ const fallback = Lifecycle.of(field).literal?.value;
99
+ flags[at] = options
100
+ ? Flags.string({ description, options, required, default: fallback as string | undefined })
101
+ : type === 'boolean' ? Flags.boolean({ description, default: fallback as boolean | undefined })
102
+ : type === 'number' ? Flags.integer({ description, required, default: fallback as number | undefined })
103
+ : Flags.string({ description, required, default: fallback as string | undefined });
104
+ }
105
+
106
+ return { args: args as ArgInput, flags: flags as FlagInput };
107
+ }
package/src/index.ts ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * A frond's operations, as a terminal.
3
+ *
4
+ * oclif finds its commands in FILES at build time; a frond's are known only after the scan, so
5
+ * they are built here and handed over as one plugin. What that buys, beside a command per
6
+ * operation: topics (`product:list` groups under `product` on its own), `--help` per topic and
7
+ * per command, `--json`, and a parser that refuses a missing flag or a value outside a closed
8
+ * set before anything runs.
9
+ *
10
+ * It is an HOST, at the same rank as `@fougere/nuxt` — the frond decides nothing about it, and
11
+ * `@fougere/cli` stays on citty: its fifteen commands are flat, one op each, and want none of
12
+ * the topics that make this worth its weight.
13
+ */
14
+ import { Command, Config, handle, run as runOclif } from '@oclif/core';
15
+ import type { App, FrondDescriptor } from '@fougere/core';
16
+ import { inputToShape, paramsToShape, type Shape } from './bridge.js';
17
+
18
+ /**
19
+ * What oclif runs: a command's shape in its cache, plus the way to reach the class.
20
+ *
21
+ * Written here rather than imported: `Command.Loadable` lives behind an entry `@oclif/core`
22
+ * does not export, and what this package builds is exactly these fields.
23
+ */
24
+ interface Loadable {
25
+ id: string;
26
+ aliases: string[];
27
+ args: Shape['args'];
28
+ flags: Shape['flags'];
29
+ description?: string;
30
+ hidden: boolean;
31
+ pluginAlias: string;
32
+ pluginName: string;
33
+ pluginType: string;
34
+ load(): Promise<typeof Command>;
35
+ }
36
+
37
+ const kebab = (name: string): string => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).replace(/^-/, '');
38
+
39
+ /** Each entry carries the key it is filed under — what a class-on-disk gets for free. */
40
+ function named<T extends Record<string, object>>(entries: T): T {
41
+ return Object.fromEntries(
42
+ Object.entries(entries).map(([name, entry]) => [name, { ...entry, name }]),
43
+ ) as T;
44
+ }
45
+
46
+ /** One operation, as the class oclif runs. */
47
+ function commandFor(id: string, call: (input: Record<string, unknown>) => Promise<unknown>, shape: Shape, describe?: string) {
48
+ const Built = class extends Command {
49
+ static override args = shape.args;
50
+ static override flags = shape.flags;
51
+ static override description = describe;
52
+ static override enableJsonFlag = true;
53
+
54
+ async run(): Promise<unknown> {
55
+ const { args, flags } = await this.parse(Built);
56
+
57
+ return this.logJson(await call({ ...args, ...flags }));
58
+ }
59
+ };
60
+ Built.id = id;
61
+
62
+ return Built;
63
+ }
64
+
65
+ /**
66
+ * Every operation of every frond, as oclif sees them.
67
+ *
68
+ * The identifier is `address:op`, which is what makes a topic: oclif groups by the part before
69
+ * the colon, so `product:list` and `product:create` answer under `product` without anything
70
+ * saying so.
71
+ */
72
+ export function commandsOf(app: App): Loadable[] {
73
+ const loadables: Loadable[] = [];
74
+
75
+ for (const frond of app.fronds as readonly FrondDescriptor[]) {
76
+ for (const handler of frond.handlers) {
77
+ const facade = app.facadeFor(handler.address) as Record<string, (input?: unknown) => Promise<unknown>>;
78
+
79
+ for (const [name, contract] of handler.operations ?? []) {
80
+ // A view when the op names one, its signature otherwise: `findById(id: string)` takes
81
+ // a bare parameter, and an entity-shaped derivation would offer nothing at all for it.
82
+ const fields = contract.input?.getFields?.();
83
+ const shape = fields
84
+ ? inputToShape(fields)
85
+ : paramsToShape(contract.signature?.params ?? []);
86
+ const Built = commandFor(
87
+ `${handler.address}:${kebab(name)}`,
88
+ (parsed) => facade[name]!(fields ? { input: parsed } : { params: parsed }),
89
+ shape,
90
+ contract.description,
91
+ );
92
+
93
+ loadables.push({
94
+ id: Built.id,
95
+ aliases: [],
96
+ // `--help` renders from the CACHED shapes, and oclif fills each entry's `name` when
97
+ // it reads a class off disk. Nothing reads these off disk, so they are named here —
98
+ // without it the renderer meets `undefined.toUpperCase()`.
99
+ args: named(Built.args),
100
+ flags: named(Built.flags),
101
+ description: Built.description,
102
+ hidden: false,
103
+ pluginAlias: 'fougere',
104
+ pluginName: 'fougere',
105
+ pluginType: 'core',
106
+ load: async () => Built,
107
+ } as Loadable);
108
+ }
109
+ }
110
+ }
111
+
112
+ return loadables;
113
+ }
114
+
115
+ /**
116
+ * Run the app's operations as a CLI.
117
+ *
118
+ * The commands are inserted through the door oclif keeps for plugins that produce theirs at
119
+ * runtime. Its name says `legacy` — it exists for Heroku's older plugins — and it is the only
120
+ * entry that takes commands nobody read from disk; the alternative is writing a file per
121
+ * operation, which is the fact this package exists to derive.
122
+ */
123
+ export async function serve(app: App, argv: string[] = process.argv.slice(2)): Promise<void> {
124
+ const config = await Config.load({ root: process.cwd() });
125
+ const commands = commandsOf(app);
126
+
127
+ // `insertLegacyPlugins` is typed private: it exists for Heroku's older plugins, and it is the
128
+ // only entry that takes commands nobody read from disk. The cast is the whole risk this
129
+ // package carries — the alternative is a file per operation, which is the fact it derives.
130
+ const insert = config as unknown as { insertLegacyPlugins(plugins: unknown[]): void };
131
+
132
+ insert.insertLegacyPlugins([{
133
+ name: 'fougere',
134
+ alias: 'fougere',
135
+ commands,
136
+ commandIDs: commands.map((one) => one.id),
137
+ topics: [],
138
+ hooks: {},
139
+ isRoot: false,
140
+ moduleType: 'module',
141
+ pjson: { name: 'fougere', version: '0.0.0', oclif: {} },
142
+ root: process.cwd(),
143
+ type: 'core',
144
+ valid: true,
145
+ version: '0.0.0',
146
+ _base: '',
147
+ hasManifest: false,
148
+ options: {},
149
+ commandsDir: undefined,
150
+ load: async () => {},
151
+ findCommand: async (id: string) => commands.find((one) => one.id === id)?.load(),
152
+ }]);
153
+
154
+ await runOclif(argv, config).catch(handle);
155
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "fougere-app",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx src/main.ts"
7
+ },
8
+ "dependencies": {
9
+ "@fougere/core": "latest",
10
+ "@fougere/defaults": "latest",
11
+ "@fougere/oclif": "latest",
12
+ "@fougere/schema": "latest",
13
+ "@fougere/container": "latest",
14
+ "@oclif/core": "^5.0.0",
15
+ "better-sqlite3": "^13.0.3",
16
+ "kysely": "^0.28.17"
17
+ },
18
+ "devDependencies": {
19
+ "tsx": "^4.21.0"
20
+ }
21
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Every operation of every frond, as a command.
3
+ *
4
+ * Try `--help`: the topics are the addresses, the flags are the entity's fields, and the
5
+ * legal values of a closed set are listed because `oneOf` named them. Nothing below says
6
+ * anything about a terminal.
7
+ */
8
+ import { bootApp } from '@fougere/defaults';
9
+ import { setLogLevel } from '@fougere/core';
10
+ import { serve } from '@fougere/oclif';
11
+ import { join } from 'node:path';
12
+
13
+ // stdout is a protocol here — `--json` is meant to reach `jq`, and the boot writes with
14
+ // `console.info`. A refusal still reaches stderr, where a shell expects it.
15
+ setLogLevel('error');
16
+
17
+ const app = await bootApp(join(import.meta.dirname, '..', '..', '..'));
18
+
19
+ await serve(app);
20
+ await app.dispose();
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "types": [
9
+ "node"
10
+ ],
11
+ "paths": {
12
+ "@fronds/facade": [
13
+ "./.fougere/facade.generated.ts"
14
+ ]
15
+ }
16
+ },
17
+ "include": [
18
+ "src"
19
+ ]
20
+ }