@fougere/cli 0.3.0-alpha.0 → 0.5.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/README.md +1 -1
- package/app/commands/BuildCommand.ts +3 -0
- package/app/commands/CallCommand.ts +3 -3
- package/app/commands/CheckCommand.ts +11 -8
- package/app/commands/DevtoolsCommand.ts +92 -0
- package/app/commands/ExplainCommand.ts +51 -8
- package/app/commands/FreezeCommand.ts +6 -1
- package/app/commands/GraphCommand.ts +3 -0
- package/app/commands/KeysCommand.ts +1 -1
- package/app/commands/MigrateCommand.ts +3 -0
- package/app/commands/ServeCommand.ts +7 -16
- package/dist/bin.js +4 -5
- package/dist/bin.js.map +1 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +11 -4
- package/dist/bridge.js.map +1 -1
- package/dist/completion.d.ts +4 -1
- package/dist/completion.d.ts.map +1 -1
- package/dist/completion.js +54 -20
- package/dist/completion.js.map +1 -1
- package/dist/loader.d.ts +11 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +23 -0
- package/dist/loader.js.map +1 -0
- package/dist/machine.d.ts +16 -0
- package/dist/machine.d.ts.map +1 -0
- package/dist/machine.js +22 -0
- package/dist/machine.js.map +1 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +16 -7
- package/dist/runner.js.map +1 -1
- package/fronds/analysis/entities/Build.ts +2 -1
- package/fronds/analysis/entities/Check.ts +2 -1
- package/fronds/analysis/entities/Devtools.ts +8 -0
- package/fronds/analysis/entities/Explain.ts +2 -1
- package/fronds/analysis/entities/Freeze.ts +2 -1
- package/fronds/analysis/entities/Graph.ts +2 -1
- package/fronds/analysis/entities/Migrate.ts +1 -0
- package/fronds/analysis/handlers/BuildHandler.ts +2 -1
- package/fronds/analysis/handlers/DevtoolsHandler.ts +86 -0
- package/fronds/analysis/handlers/ExplainHandler.ts +52 -12
- package/fronds/analysis/handlers/FreezeHandler.ts +18 -13
- package/fronds/analysis/handlers/MigrateHandler.ts +4 -3
- package/fronds/scaffold/entities/BuildFrond.ts +1 -1
- package/fronds/scaffold/entities/Call.ts +1 -1
- package/fronds/scaffold/entities/Sync.ts +1 -1
- package/fronds/scaffold/handlers/BuildFrondHandler.ts +5 -5
- package/fronds/scaffold/handlers/SyncHandler.ts +17 -17
- package/package.json +9 -8
- package/src/bin.ts +83 -0
- package/src/bridge.ts +70 -0
- package/src/completion.ts +152 -0
- package/src/index.ts +3 -0
- package/src/loader.ts +28 -0
- package/src/machine.ts +23 -0
- package/src/runner.ts +146 -0
- package/src/theme.ts +19 -0
- package/src/ui.ts +131 -0
- package/templates/blog/fronds/blog/handlers/PostHandler.ts +1 -1
- package/templates/frond/fronds/__name__/handlers/PostHandler.ts +1 -1
- package/templates/fronds/blog/handlers/PostHandler.ts +1 -1
package/README.md
CHANGED
|
@@ -23,4 +23,4 @@ pnpm add @fougere/cli
|
|
|
23
23
|
|
|
24
24
|
Part of [Fougere](https://github.com/chok/fougere) — one schema, a gradient from
|
|
25
25
|
monolith to distributed, the same user code.
|
|
26
|
-
Reference documentation: [the site](https://
|
|
26
|
+
Reference documentation: [the site](https://fougere.dev/) (en/fr).
|
|
@@ -3,6 +3,7 @@ import type { App } from '@fougere/core';
|
|
|
3
3
|
import type { ui as createUi } from '../../src/ui.js';
|
|
4
4
|
import type { BuildReport } from '../../fronds/analysis/handlers/BuildHandler.js';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
+
import { machineWanted, printMachine } from '../../src/machine.js';
|
|
6
7
|
|
|
7
8
|
type Ui = ReturnType<typeof createUi>;
|
|
8
9
|
|
|
@@ -22,6 +23,8 @@ export default class BuildCommand {
|
|
|
22
23
|
{ params: {}, query: {}, body: raw, state: {} },
|
|
23
24
|
)) as BuildReport;
|
|
24
25
|
|
|
26
|
+
if (machineWanted(raw)) return printMachine(built);
|
|
27
|
+
|
|
25
28
|
if (built.fronds.length === 0) {
|
|
26
29
|
this.ui.warn('No fronds found. Run this from a Fougere project root.');
|
|
27
30
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createAppRunner } from '@fougere/core';
|
|
2
|
-
import {
|
|
2
|
+
import { lowerFirst } from '@fougere/core/contract';
|
|
3
3
|
import { bootAppFromConfig } from '@fougere/defaults';
|
|
4
4
|
import type { App } from '@fougere/core';
|
|
5
5
|
import type { ui as createUi } from '../../src/ui.js';
|
|
@@ -36,7 +36,7 @@ export default class CallCommand {
|
|
|
36
36
|
constructor(private app: App, private ui: Ui) {}
|
|
37
37
|
|
|
38
38
|
async run(raw: Record<string, unknown>) {
|
|
39
|
-
const target = raw.
|
|
39
|
+
const target = raw.operation as string | undefined;
|
|
40
40
|
if (!target || !target.includes('.')) {
|
|
41
41
|
this.ui.error('Usage: fougere call <entity>.<op> [--field value …]');
|
|
42
42
|
return;
|
|
@@ -57,7 +57,7 @@ export default class CallCommand {
|
|
|
57
57
|
const app = await bootAppFromConfig(process.cwd(), {});
|
|
58
58
|
try {
|
|
59
59
|
const result = await createAppRunner(app)(
|
|
60
|
-
{ entity:
|
|
60
|
+
{ entity: lowerFirst(entityName), op },
|
|
61
61
|
{ params, query: {}, body, state: {} },
|
|
62
62
|
);
|
|
63
63
|
this.ui.note(JSON.stringify(result, null, 2), target);
|
|
@@ -4,6 +4,7 @@ import { createAppRunner } from '@fougere/core';
|
|
|
4
4
|
import type { ui as createUi } from '../../src/ui.js';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { relative } from 'node:path';
|
|
7
|
+
import { machineWanted, printMachine } from '../../src/machine.js';
|
|
7
8
|
|
|
8
9
|
type Ui = ReturnType<typeof createUi>;
|
|
9
10
|
|
|
@@ -17,6 +18,13 @@ export default class CheckCommand {
|
|
|
17
18
|
{ params: {}, query: {}, body: raw, state: {} },
|
|
18
19
|
) as CheckResult;
|
|
19
20
|
|
|
21
|
+
// A non-zero exit is what makes this usable in CI, so it is decided before the two
|
|
22
|
+
// renderings rather than inside the terminal one.
|
|
23
|
+
const blocking = result.findings.filter((f) => f.severity === 'blocking').length;
|
|
24
|
+
if (blocking > 0) process.exitCode = 1;
|
|
25
|
+
|
|
26
|
+
if (machineWanted(raw)) return printMachine(result);
|
|
27
|
+
|
|
20
28
|
if (result.fronds === 0) {
|
|
21
29
|
this.ui.warn('No fronds found. Run this from a Fougere project root.');
|
|
22
30
|
return;
|
|
@@ -31,14 +39,9 @@ export default class CheckCommand {
|
|
|
31
39
|
|
|
32
40
|
this.ui.note(result.findings.map(render).join('\n\n'), 'Findings');
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.ui.info(`${blocking} blocking, ${
|
|
37
|
-
|
|
38
|
-
// A non-zero exit is what makes this usable in CI. Warnings do not fail:
|
|
39
|
-
// an unresolvable base class with no operation is ordinary, and a check that
|
|
40
|
-
// cries wolf stops being read.
|
|
41
|
-
if (blocking > 0) process.exitCode = 1;
|
|
42
|
+
// Warnings do not fail: an unresolvable base class with no operation is ordinary,
|
|
43
|
+
// and a check that cries wolf stops being read.
|
|
44
|
+
this.ui.info(`${blocking} blocking, ${result.findings.length - blocking} warning(s)`);
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createAppRunner } from '@fougere/core';
|
|
2
|
+
import type { App, CallRecord } from '@fougere/core';
|
|
3
|
+
import type { CallsView } from '../../fronds/analysis/handlers/DevtoolsHandler.js';
|
|
4
|
+
import type { ui as createUi } from '../../src/ui.js';
|
|
5
|
+
import { machineWanted, printMachine } from '../../src/machine.js';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
|
|
8
|
+
type Ui = ReturnType<typeof createUi>;
|
|
9
|
+
|
|
10
|
+
/** How long between two reads. Fast enough to read as a stream, cheap enough to ignore. */
|
|
11
|
+
const EVERY_MS = 500;
|
|
12
|
+
|
|
13
|
+
const ROUTE = { local: pc.green, remote: pc.cyan, system: pc.magenta } as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `fougere devtools` — the call log of every running app of this project, followed.
|
|
17
|
+
*
|
|
18
|
+
* A cursor per address, because each app numbers its own ring: asking for what this reader
|
|
19
|
+
* has not seen is the whole protocol, and it is per source or nothing.
|
|
20
|
+
*/
|
|
21
|
+
export default class DevtoolsCommand {
|
|
22
|
+
constructor(private app: App, private ui: Ui) {}
|
|
23
|
+
|
|
24
|
+
async run(raw: Record<string, unknown>) {
|
|
25
|
+
const since: Record<string, number> = {};
|
|
26
|
+
|
|
27
|
+
if (machineWanted(raw)) return printMachine(await this.view(raw, since));
|
|
28
|
+
|
|
29
|
+
const first = await this.view(raw, since);
|
|
30
|
+
this.announce(first);
|
|
31
|
+
|
|
32
|
+
const told: Record<string, number> = {};
|
|
33
|
+
for (let view = first; ; view = await this.view(raw, since)) {
|
|
34
|
+
for (const source of view.sources) {
|
|
35
|
+
since[source.url] = source.cursor;
|
|
36
|
+
|
|
37
|
+
// Said once per address, and only when it grows: a ring that dropped is not a
|
|
38
|
+
// quiet period, and repeating it every half second would bury the calls.
|
|
39
|
+
if (source.dropped > (told[source.url] ?? 0)) {
|
|
40
|
+
this.ui.warn(`${source.url} dropped ${source.dropped - (told[source.url] ?? 0)} call(s) — its ring is smaller than this traffic`);
|
|
41
|
+
told[source.url] = source.dropped;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const call of view.calls) process.stdout.write(render(call, view.sources.length > 1) + '\n');
|
|
46
|
+
await new Promise((wake) => setTimeout(wake, EVERY_MS));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What is being watched, and what refused — stated before the first line scrolls. */
|
|
51
|
+
private announce(view: CallsView): void {
|
|
52
|
+
for (const source of view.sources) {
|
|
53
|
+
const name = source.frond ? `${pc.bold(source.frond)} ${pc.dim(source.url)}` : pc.bold(source.url);
|
|
54
|
+
if (source.refusal) this.ui.warn(`${name} — ${source.refusal}`);
|
|
55
|
+
else this.ui.step(`following ${name}`);
|
|
56
|
+
}
|
|
57
|
+
this.ui.info(pc.dim('ctrl-c to stop'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private view(raw: Record<string, unknown>, since: Record<string, number>): Promise<CallsView> {
|
|
61
|
+
return createAppRunner(this.app)(
|
|
62
|
+
{ entity: 'devtools', op: 'execute' },
|
|
63
|
+
{ params: {}, query: {}, body: { ...raw, since }, state: {} },
|
|
64
|
+
) as Promise<CallsView>;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function render(call: CallRecord & { source: string }, many: boolean): string {
|
|
69
|
+
// Padded BEFORE colouring: an escape sequence counts in a string's length, so padding a
|
|
70
|
+
// coloured value moves the column by however many bytes the colour took.
|
|
71
|
+
const at = many ? pc.dim(pad(new URL(call.source).port || call.source, 5)) : '';
|
|
72
|
+
const frond = pc.dim(pad(call.frond ?? '—', 9));
|
|
73
|
+
const address = pc.bold(pad(`${call.entity}.${call.operation}${call.surface ? `/${call.surface}` : ''}`, 22));
|
|
74
|
+
const route = call.route
|
|
75
|
+
? (ROUTE[call.route] ?? pc.dim)(pad(call.route, 9))
|
|
76
|
+
: pc.red(pad('unrouted', 9));
|
|
77
|
+
const took = pc.dim(lead(call.ms === undefined ? '' : `${call.ms}ms`, 7));
|
|
78
|
+
const verdict = call.verdict === 'failed'
|
|
79
|
+
? pc.red(call.refusal?.code ?? 'failed')
|
|
80
|
+
: call.verdict === 'ok' ? pc.dim('ok') : pc.yellow('running');
|
|
81
|
+
|
|
82
|
+
return [at, frond, address, route, took, verdict, call.trace ? pc.dim(call.trace.slice(3, 11)) : '']
|
|
83
|
+
.filter(Boolean).join(' ');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pad(value: string, width: number): string {
|
|
87
|
+
return value.length >= width ? value : value + ' '.repeat(width - value.length);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function lead(value: string, width: number): string {
|
|
91
|
+
return value.length >= width ? value : ' '.repeat(width - value.length) + value;
|
|
92
|
+
}
|
|
@@ -3,9 +3,11 @@ import { createAppRunner } from '@fougere/core';
|
|
|
3
3
|
import type { ui as createUi } from '../../src/ui.js';
|
|
4
4
|
import type {
|
|
5
5
|
ExplainResult,
|
|
6
|
+
ExplainListing,
|
|
6
7
|
ExplainedBinding,
|
|
7
8
|
} from '../../fronds/analysis/handlers/ExplainHandler.js';
|
|
8
9
|
import pc from 'picocolors';
|
|
10
|
+
import { machineText, printMachine } from '../../src/machine.js';
|
|
9
11
|
|
|
10
12
|
type Ui = ReturnType<typeof createUi>;
|
|
11
13
|
|
|
@@ -13,22 +15,48 @@ export default class ExplainCommand {
|
|
|
13
15
|
constructor(private app: App, private ui: Ui) {}
|
|
14
16
|
|
|
15
17
|
async run(raw: Record<string, unknown>) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const names = raw.names as 'operations' | 'fronds' | undefined;
|
|
19
|
+
const operation = (raw.operation as string | undefined)?.trim();
|
|
20
|
+
|
|
21
|
+
// No operation named — the question is what this project serves at all. The same
|
|
22
|
+
// model answers both, so the door is the one that was asked, never a guess.
|
|
23
|
+
if (names || !operation) return this.listing(raw, names);
|
|
24
|
+
|
|
25
|
+
const result = await this.ask('execute', raw) as ExplainResult;
|
|
26
|
+
|
|
27
|
+
if (raw.json === true) return printMachine(result);
|
|
28
|
+
|
|
29
|
+
this.ui.note(renderExplain(result), result.operation);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async listing(raw: Record<string, unknown>, names?: 'operations' | 'fronds') {
|
|
33
|
+
const listing = await this.ask('list', raw) as ExplainListing;
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
// A completion script has no JSON parser: one name per line, nothing else on stdout.
|
|
36
|
+
if (names) {
|
|
37
|
+
const values = names === 'fronds'
|
|
38
|
+
? listing.fronds.map((frond) => frond.name)
|
|
39
|
+
: listing.operations;
|
|
40
|
+
if (values.length > 0) process.stdout.write(values.join('\n') + '\n');
|
|
23
41
|
return;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
|
-
|
|
44
|
+
if (raw.json === true) return printMachine(listing);
|
|
45
|
+
|
|
46
|
+
this.ui.note(renderListing(listing), 'what this project serves');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private ask(op: string, raw: Record<string, unknown>) {
|
|
50
|
+
return createAppRunner(this.app)(
|
|
51
|
+
{ entity: 'explain', op },
|
|
52
|
+
{ params: {}, query: {}, body: raw, state: {} },
|
|
53
|
+
);
|
|
27
54
|
}
|
|
28
55
|
}
|
|
29
56
|
|
|
57
|
+
/** Kept as the name `tests/explain.test.ts` pins the key order through. */
|
|
30
58
|
export function renderExplainJson(result: ExplainResult): string {
|
|
31
|
-
return
|
|
59
|
+
return machineText(result);
|
|
32
60
|
}
|
|
33
61
|
|
|
34
62
|
export function renderExplain(result: ExplainResult): string {
|
|
@@ -66,6 +94,21 @@ export function renderExplain(result: ExplainResult): string {
|
|
|
66
94
|
return lines.join('\n');
|
|
67
95
|
}
|
|
68
96
|
|
|
97
|
+
export function renderListing(listing: ExplainListing): string {
|
|
98
|
+
const lines = [pc.bold('Fronds')];
|
|
99
|
+
if (listing.fronds.length === 0) lines.push(' —');
|
|
100
|
+
for (const frond of listing.fronds) {
|
|
101
|
+
const where = frond.remote ? `remote ${pc.dim(frond.remote)}` : 'local';
|
|
102
|
+
lines.push(` ${frond.name} ${pc.dim('→')} ${where}, ${frond.operations} operation(s)`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lines.push('', pc.bold('Operations'));
|
|
106
|
+
if (listing.operations.length === 0) lines.push(' —');
|
|
107
|
+
for (const operation of listing.operations) lines.push(` ${operation}`);
|
|
108
|
+
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
69
112
|
function bindingName(binding: ExplainedBinding | null): string {
|
|
70
113
|
if (!binding) return 'unbound';
|
|
71
114
|
switch (binding.kind) {
|
|
@@ -4,6 +4,7 @@ import type { App } from '@fougere/core';
|
|
|
4
4
|
import type { ui as createUi } from '../../src/ui.js';
|
|
5
5
|
import type { FreezeInspection } from '../../fronds/analysis/handlers/FreezeHandler.js';
|
|
6
6
|
import pc from 'picocolors';
|
|
7
|
+
import { machineWanted, printMachine } from '../../src/machine.js';
|
|
7
8
|
|
|
8
9
|
type Ui = ReturnType<typeof createUi>;
|
|
9
10
|
|
|
@@ -30,6 +31,10 @@ export default class FreezeCommand {
|
|
|
30
31
|
// otherwise — so the first call is both the inspection and the happy path.
|
|
31
32
|
const seen = await freeze(raw);
|
|
32
33
|
|
|
34
|
+
// The question below cannot be put to a machine, so the inspection is the answer:
|
|
35
|
+
// `ambiguous` says what a human still has to settle, and nothing was written.
|
|
36
|
+
if (machineWanted(raw)) return printMachine(seen);
|
|
37
|
+
|
|
33
38
|
if (seen.entities.length === 0) {
|
|
34
39
|
this.ui.warn('No entities found. Run this from a Fougere project root.');
|
|
35
40
|
return;
|
|
@@ -74,7 +79,7 @@ export default class FreezeCommand {
|
|
|
74
79
|
this.ui.warn(`${entity}.${removed} treated as dropped — say so explicitly if that is right.`);
|
|
75
80
|
return undefined;
|
|
76
81
|
}
|
|
77
|
-
renamed[entity] = { ...
|
|
82
|
+
renamed[entity] = { ...renamed[entity], [removed]: answer };
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
return renamed;
|
|
@@ -3,6 +3,7 @@ import type { EntityNode, DomainCluster, App } from '@fougere/core';
|
|
|
3
3
|
import { createAppRunner } from '@fougere/core';
|
|
4
4
|
import type { ui as createUi } from '../../src/ui.js';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
+
import { machineWanted, printMachine } from '../../src/machine.js';
|
|
6
7
|
|
|
7
8
|
type Ui = ReturnType<typeof createUi>;
|
|
8
9
|
|
|
@@ -16,6 +17,8 @@ export default class GraphCommand {
|
|
|
16
17
|
{ params: {}, query: {}, body: raw, state: {} },
|
|
17
18
|
) as GraphResult;
|
|
18
19
|
|
|
20
|
+
if (machineWanted(raw)) return printMachine(result);
|
|
21
|
+
|
|
19
22
|
if (result.fronds.length === 0) {
|
|
20
23
|
this.ui.warn('No fronds found. Run this from a Fougere project root.');
|
|
21
24
|
return;
|
|
@@ -36,7 +36,7 @@ export default class KeysCommand {
|
|
|
36
36
|
|
|
37
37
|
this.ui.success(`root created — ${ROOT_KEY} (never commit it; added to .gitignore)`);
|
|
38
38
|
this.ui.note(
|
|
39
|
-
`
|
|
39
|
+
`FOUGERE_ROOT_KEY=${packed(publicKey)}`,
|
|
40
40
|
'Every frond that ANSWERS — public, safe in an image or a manifest',
|
|
41
41
|
);
|
|
42
42
|
this.ui.info('Then `fougere grant <frond>` for each frond that CALLS.');
|
|
@@ -3,6 +3,7 @@ import type { App } from '@fougere/core';
|
|
|
3
3
|
import type { ui as createUi } from '../../src/ui.js';
|
|
4
4
|
import type { MigrationPlan } from '../../fronds/analysis/handlers/MigrateHandler.js';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
+
import { machineWanted, printMachine } from '../../src/machine.js';
|
|
6
7
|
|
|
7
8
|
type Ui = ReturnType<typeof createUi>;
|
|
8
9
|
|
|
@@ -21,6 +22,8 @@ export default class MigrateCommand {
|
|
|
21
22
|
{ params: {}, query: {}, body: raw, state: {} },
|
|
22
23
|
)) as MigrationPlan;
|
|
23
24
|
|
|
25
|
+
if (machineWanted(raw)) return printMachine(result);
|
|
26
|
+
|
|
24
27
|
if (result.chain.length === 0) {
|
|
25
28
|
this.ui.warn('No frozen step to apply. `fougere freeze <version>` records one.');
|
|
26
29
|
return;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createLocalRunner, identityFromEnv } from '@fougere/core';
|
|
2
|
-
import {
|
|
2
|
+
import { watchPathsOf } from '@fougere/core/node';
|
|
3
|
+
import { installLoader } from '../../src/loader.js';
|
|
4
|
+
import type { Conventions } from '@fougere/core/node';
|
|
3
5
|
import { bootAppFromConfig } from '@fougere/defaults';
|
|
4
6
|
import { serve } from '@fougere/transport-http';
|
|
5
7
|
import { watch } from 'node:fs';
|
|
@@ -14,16 +16,6 @@ const DRAIN_MS = 5_000;
|
|
|
14
16
|
/** One save fires several events; the boot must not start once per event. */
|
|
15
17
|
const SETTLE_MS = 60;
|
|
16
18
|
|
|
17
|
-
/**
|
|
18
|
-
* A loader that re-reads. Every loader caches by specifier, so a second boot in this
|
|
19
|
-
* process would be handed the modules the first one read — a reload that changes nothing.
|
|
20
|
-
*/
|
|
21
|
-
async function rereadingLoader(): Promise<void> {
|
|
22
|
-
const { createJiti } = await import('jiti');
|
|
23
|
-
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
|
|
24
|
-
setModuleLoader((filePath) => jiti.import(filePath) as Promise<Record<string, unknown>>);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
/**
|
|
28
20
|
* The host end of the gradient: one frond, alone in this process, reachable
|
|
29
21
|
* over HTTP. `topology: false` — a served frond IS the host, it never routes
|
|
@@ -40,8 +32,9 @@ export default class ServeCommand {
|
|
|
40
32
|
const root = process.cwd();
|
|
41
33
|
const watching = raw.watch === true;
|
|
42
34
|
|
|
43
|
-
// Cache-free from the FIRST boot, so every boot in this process reads
|
|
44
|
-
|
|
35
|
+
// Cache-free from the FIRST boot when watching, so every boot in this process reads
|
|
36
|
+
// the same way rather than the first one being special.
|
|
37
|
+
const conventions = await installLoader(root, watching);
|
|
45
38
|
|
|
46
39
|
let hosted = await bootAppFromConfig(root, { fronds: [frond], topology: false });
|
|
47
40
|
if (!hosted.fronds.some((f) => f.name === frond)) {
|
|
@@ -59,7 +52,7 @@ export default class ServeCommand {
|
|
|
59
52
|
const { verify, requireIdentity } = await identityFromEnv();
|
|
60
53
|
const { port: bound } = await serve((call, inv) => current(call, inv), { port, verify, requireIdentity });
|
|
61
54
|
this.ui.step(`frond ${frond} servie — POST http://127.0.0.1:${bound}/_fougere/call`);
|
|
62
|
-
this.ui.info(requireIdentity ? 'signed calls only (
|
|
55
|
+
this.ui.info(requireIdentity ? 'signed calls only (FOUGERE_ROOT_KEY is set)' : 'unsigned calls accepted — no FOUGERE_ROOT_KEY');
|
|
63
56
|
|
|
64
57
|
if (!watching) {
|
|
65
58
|
this.ui.info('Ctrl-C pour arrêter.');
|
|
@@ -88,8 +81,6 @@ export default class ServeCommand {
|
|
|
88
81
|
await previous.dispose();
|
|
89
82
|
};
|
|
90
83
|
|
|
91
|
-
const config = await loadConfig(root);
|
|
92
|
-
const conventions = resolveConventions(config.conventions);
|
|
93
84
|
let settling: NodeJS.Timeout | undefined;
|
|
94
85
|
let watched = 0;
|
|
95
86
|
|
package/dist/bin.js
CHANGED
|
@@ -7,15 +7,14 @@
|
|
|
7
7
|
* app/ → loaded at runtime by jiti (presentation)
|
|
8
8
|
*/
|
|
9
9
|
import { createApp, setLogLevel, envLevel } from '@fougere/core';
|
|
10
|
-
import { scanProject,
|
|
10
|
+
import { scanProject, getModuleLoader, frondDirsOf, DEFAULT_CONVENTIONS } from '@fougere/core/node';
|
|
11
11
|
import { readdir, stat } from 'node:fs/promises';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { createContainer } from '@fougere/container';
|
|
14
14
|
import { ui } from './ui.js';
|
|
15
15
|
import { run } from './runner.js';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
setModuleLoader((filePath) => jiti.import(filePath));
|
|
16
|
+
import { installLoader } from './loader.js';
|
|
17
|
+
await installLoader(process.cwd());
|
|
19
18
|
const cliRoot = new URL('..', import.meta.url).pathname;
|
|
20
19
|
const container = createContainer();
|
|
21
20
|
const terminal = ui();
|
|
@@ -57,7 +56,7 @@ async function scanOf(root) {
|
|
|
57
56
|
const written = join(root, '.fougere/scan.generated.ts');
|
|
58
57
|
const writtenAt = await stat(written).then((s) => s.mtimeMs).catch(() => 0);
|
|
59
58
|
if (writtenAt > 0 && writtenAt >= await newestDeclaration(root)) {
|
|
60
|
-
return (await
|
|
59
|
+
return (await getModuleLoader()(written)).scan;
|
|
61
60
|
}
|
|
62
61
|
// The only slow phase, and it announced nothing: the boot states it at `info`, which the
|
|
63
62
|
// threshold above lowers to `warn`. The terminal says it instead, and a pipe keeps its
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAmB,MAAM,eAAe,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACpG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":"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"}
|
package/dist/bridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,EAAE,OAAO,EAAU,MAAM,OAAO,CAAC;AAM7C,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,EAAE,OAAO,EAAU,MAAM,OAAO,CAAC;AAM7C,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAqDpD"}
|
package/dist/bridge.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Lifecycle, Role } from '@fougere/schema';
|
|
2
|
-
import { Anatomy,
|
|
2
|
+
import { Anatomy, Visibility } from '@fougere/schema';
|
|
3
3
|
function toKebab(name) {
|
|
4
4
|
return name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
5
5
|
}
|
|
@@ -9,7 +9,7 @@ export function entityToArgs(fields) {
|
|
|
9
9
|
let positionalIndex = 0;
|
|
10
10
|
// Axes-derived ingress membership; the CLI additionally skips ALL relations
|
|
11
11
|
// (a ref is not a flag — supplying related rows is not a CLI gesture).
|
|
12
|
-
for (const [key, field] of Object.entries(
|
|
12
|
+
for (const [key, field] of Object.entries(Visibility.of(fields).input)) {
|
|
13
13
|
if (Role.of(field).relation)
|
|
14
14
|
continue;
|
|
15
15
|
// A `default(v)` travels as the create rule `{ value }` — citty shows it.
|
|
@@ -29,8 +29,15 @@ export function entityToArgs(fields) {
|
|
|
29
29
|
case 'number':
|
|
30
30
|
case 'integer':
|
|
31
31
|
case 'string':
|
|
32
|
-
// A
|
|
33
|
-
|
|
32
|
+
// A closed set is citty's `enum`: the shape already names the legal values, so the
|
|
33
|
+
// refusal and the `--help` listing come from the declaration rather than a check
|
|
34
|
+
// written beside it.
|
|
35
|
+
if (shape.type === 'string' && shape.enum?.length) {
|
|
36
|
+
def.type = 'enum';
|
|
37
|
+
def.options = shape.enum.filter((v) => v !== null);
|
|
38
|
+
// A date-time string stays a named string — never a positional arg.
|
|
39
|
+
}
|
|
40
|
+
else if (shape.type === 'string' && shape.format === 'date-time') {
|
|
34
41
|
def.type = 'string';
|
|
35
42
|
}
|
|
36
43
|
else if (positionalIndex === 0 && def.required && key !== 'force') {
|
package/dist/bridge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAQlD,OAAO,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/completion.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shell completion generator — outputs a script for bash/zsh.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Commands and flags are the CLI's own entities, read once at generation. The VALUES a
|
|
5
|
+
* positional accepts belong to the project the shell sits in, so they are not written
|
|
6
|
+
* down here: the script asks `fougere explain --names` at the moment of the TAB. A list
|
|
7
|
+
* frozen into a script is stale the first time a handler is added.
|
|
5
8
|
*/
|
|
6
9
|
import type { App } from '@fougere/core';
|
|
7
10
|
export declare function generateZshCompletion(app: App, binName?: string): string;
|
package/dist/completion.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../src/completion.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../src/completion.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAkDzC,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,SAAY,GAAG,MAAM,CAgD3E;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,SAAY,GAAG,MAAM,CA2C5E"}
|
package/dist/completion.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { entityToArgs } from './bridge.js';
|
|
2
2
|
function toKebab(name) {
|
|
3
3
|
return name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()).replace(/^-/, '');
|
|
4
4
|
}
|
|
5
|
+
/** What a positional names, said by the field's own name — no table to keep in step. */
|
|
6
|
+
const NAMES = {
|
|
7
|
+
operation: 'operations',
|
|
8
|
+
frond: 'fronds',
|
|
9
|
+
};
|
|
5
10
|
function extractCommandMeta(app) {
|
|
6
11
|
const commands = [];
|
|
7
12
|
for (const frond of app.fronds) {
|
|
@@ -19,25 +24,37 @@ function extractCommandMeta(app) {
|
|
|
19
24
|
}
|
|
20
25
|
if (typeof facade.execute !== 'function')
|
|
21
26
|
continue;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
// The same function the runner builds citty's args from, so the positional and the
|
|
28
|
+
// flags are the ones the command actually has.
|
|
29
|
+
const args = Object.entries(entityToArgs(entity.entityClass.getFields()));
|
|
30
|
+
const positional = args.find(([, def]) => def.type === 'positional')?.[0];
|
|
31
|
+
commands.push({
|
|
32
|
+
name: toKebab(entity.name),
|
|
33
|
+
flags: args.filter(([, def]) => def.type !== 'positional').map(([key]) => `--${key}`),
|
|
34
|
+
...(positional && NAMES[positional] ? { names: NAMES[positional] } : {}),
|
|
35
|
+
});
|
|
26
36
|
}
|
|
27
37
|
}
|
|
28
38
|
return commands;
|
|
29
39
|
}
|
|
30
40
|
export function generateZshCompletion(app, binName = 'fougere') {
|
|
31
41
|
const commands = extractCommandMeta(app);
|
|
32
|
-
const
|
|
42
|
+
const valueCases = commands
|
|
43
|
+
.filter((cmd) => cmd.names)
|
|
44
|
+
.map((cmd) => ` ${cmd.name}) _${binName}_names ${cmd.names} ;;`)
|
|
45
|
+
.join('\n');
|
|
33
46
|
const flagCases = commands
|
|
34
|
-
.map((cmd) => {
|
|
35
|
-
const flags = cmd.flags.join(' ');
|
|
36
|
-
return ` ${cmd.name}) flags="${flags}" ;;`;
|
|
37
|
-
})
|
|
47
|
+
.map((cmd) => ` ${cmd.name}) flags="${cmd.flags.join(' ')}" ;;`)
|
|
38
48
|
.join('\n');
|
|
39
49
|
return `# Auto-generated by @fougere/cli
|
|
40
50
|
|
|
51
|
+
# The project answers; a stale list in this file would not.
|
|
52
|
+
_${binName}_names() {
|
|
53
|
+
local -a values
|
|
54
|
+
values=(\${(f)"$(${binName} explain --names $1 2>/dev/null)"})
|
|
55
|
+
(( \${#values} )) && compadd -- \${values}
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
_${binName}() {
|
|
42
59
|
local -a commands flags
|
|
43
60
|
commands=(${commands.map((c) => `'${c.name}'`).join(' ')})
|
|
@@ -48,14 +65,19 @@ _${binName}() {
|
|
|
48
65
|
fi
|
|
49
66
|
|
|
50
67
|
local cmd=\${words[2]}
|
|
68
|
+
|
|
69
|
+
if [[ "\${words[CURRENT]}" != --* ]]; then
|
|
70
|
+
case "$cmd" in
|
|
71
|
+
${valueCases}
|
|
72
|
+
esac
|
|
73
|
+
return
|
|
74
|
+
fi
|
|
75
|
+
|
|
51
76
|
case "$cmd" in
|
|
52
77
|
${flagCases}
|
|
53
78
|
*) flags="" ;;
|
|
54
79
|
esac
|
|
55
|
-
|
|
56
|
-
if [[ "\${words[CURRENT]}" == --* ]]; then
|
|
57
|
-
_values 'flags' \${(s: :)flags}
|
|
58
|
-
fi
|
|
80
|
+
_values 'flags' \${(s: :)flags}
|
|
59
81
|
}
|
|
60
82
|
|
|
61
83
|
compdef _${binName} ${binName}
|
|
@@ -64,17 +86,17 @@ compdef _${binName} ${binName}
|
|
|
64
86
|
export function generateBashCompletion(app, binName = 'fougere') {
|
|
65
87
|
const commands = extractCommandMeta(app);
|
|
66
88
|
const cmdNames = commands.map((c) => c.name).join(' ');
|
|
89
|
+
const valueCases = commands
|
|
90
|
+
.filter((cmd) => cmd.names)
|
|
91
|
+
.map((cmd) => ` ${cmd.name}) names="${cmd.names}" ;;`)
|
|
92
|
+
.join('\n');
|
|
67
93
|
const flagCases = commands
|
|
68
|
-
.map((cmd) => {
|
|
69
|
-
const flags = cmd.flags.join(' ');
|
|
70
|
-
return ` ${cmd.name}) COMPREPLY=( $(compgen -W "${flags}" -- "\${cur}") ) ;;`;
|
|
71
|
-
})
|
|
94
|
+
.map((cmd) => ` ${cmd.name}) COMPREPLY=( $(compgen -W "${cmd.flags.join(' ')}" -- "\${cur}") ) ;;`)
|
|
72
95
|
.join('\n');
|
|
73
96
|
return `# Auto-generated by @fougere/cli
|
|
74
97
|
_${binName}_completions() {
|
|
75
|
-
local cur
|
|
98
|
+
local cur cmd names
|
|
76
99
|
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
77
|
-
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
78
100
|
cmd="\${COMP_WORDS[1]}"
|
|
79
101
|
|
|
80
102
|
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
@@ -82,6 +104,18 @@ _${binName}_completions() {
|
|
|
82
104
|
return
|
|
83
105
|
fi
|
|
84
106
|
|
|
107
|
+
if [[ "\${cur}" != --* ]]; then
|
|
108
|
+
names=""
|
|
109
|
+
case "$cmd" in
|
|
110
|
+
${valueCases}
|
|
111
|
+
esac
|
|
112
|
+
if [[ -n "\${names}" ]]; then
|
|
113
|
+
# The project answers; a stale list in this file would not.
|
|
114
|
+
COMPREPLY=( $(compgen -W "$(${binName} explain --names "\${names}" 2>/dev/null | tr '\\n' ' ')" -- "\${cur}") )
|
|
115
|
+
return
|
|
116
|
+
fi
|
|
117
|
+
fi
|
|
118
|
+
|
|
85
119
|
case "$cmd" in
|
|
86
120
|
${flagCases}
|
|
87
121
|
esac
|