@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 +21 -0
- package/dist/bridge.d.ts +27 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +89 -0
- package/dist/bridge.js.map +1 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +114 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/bridge.ts +107 -0
- package/src/index.ts +155 -0
- package/template/package.json +21 -0
- package/template/src/main.ts +20 -0
- package/template/tsconfig.json +20 -0
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.
|
package/dist/bridge.d.ts
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|