@fougere/cli 0.4.0-alpha.0 → 0.5.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/commands/BuildCommand.ts +3 -0
- package/app/commands/CheckCommand.ts +11 -8
- package/app/commands/DevtoolsCommand.ts +92 -0
- package/app/commands/ExplainCommand.ts +5 -9
- package/app/commands/FreezeCommand.ts +5 -0
- package/app/commands/GraphCommand.ts +3 -0
- package/app/commands/MigrateCommand.ts +3 -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 +14 -6
- 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/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/FreezeHandler.ts +3 -2
- package/fronds/analysis/handlers/MigrateHandler.ts +2 -1
- package/fronds/scaffold/handlers/SyncHandler.ts +2 -2
- package/package.json +7 -7
- package/src/machine.ts +23 -0
- package/src/runner.ts +13 -6
- 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
|
@@ -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;
|
|
@@ -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
|
+
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ExplainedBinding,
|
|
8
8
|
} from '../../fronds/analysis/handlers/ExplainHandler.js';
|
|
9
9
|
import pc from 'picocolors';
|
|
10
|
+
import { machineText, printMachine } from '../../src/machine.js';
|
|
10
11
|
|
|
11
12
|
type Ui = ReturnType<typeof createUi>;
|
|
12
13
|
|
|
@@ -23,10 +24,7 @@ export default class ExplainCommand {
|
|
|
23
24
|
|
|
24
25
|
const result = await this.ask('execute', raw) as ExplainResult;
|
|
25
26
|
|
|
26
|
-
if (raw.json === true)
|
|
27
|
-
process.stdout.write(renderExplainJson(result) + '\n');
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
27
|
+
if (raw.json === true) return printMachine(result);
|
|
30
28
|
|
|
31
29
|
this.ui.note(renderExplain(result), result.operation);
|
|
32
30
|
}
|
|
@@ -43,10 +41,7 @@ export default class ExplainCommand {
|
|
|
43
41
|
return;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
if (raw.json === true)
|
|
47
|
-
process.stdout.write(JSON.stringify(listing, null, 2) + '\n');
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
44
|
+
if (raw.json === true) return printMachine(listing);
|
|
50
45
|
|
|
51
46
|
this.ui.note(renderListing(listing), 'what this project serves');
|
|
52
47
|
}
|
|
@@ -59,8 +54,9 @@ export default class ExplainCommand {
|
|
|
59
54
|
}
|
|
60
55
|
}
|
|
61
56
|
|
|
57
|
+
/** Kept as the name `tests/explain.test.ts` pins the key order through. */
|
|
62
58
|
export function renderExplainJson(result: ExplainResult): string {
|
|
63
|
-
return
|
|
59
|
+
return machineText(result);
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
export function renderExplain(result: ExplainResult): string {
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command's machine output — the one shape a pipe reads.
|
|
3
|
+
*
|
|
4
|
+
* There is no list of commands here: `json` is declared by an entity like any other
|
|
5
|
+
* field, so a command that declares it gets the door. The runner used to name `explain`
|
|
6
|
+
* in an `if`, and `graph --json` announced a flag it then ignored.
|
|
7
|
+
*/
|
|
8
|
+
export declare function machineWanted(raw: Record<string, unknown>): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* A `Map` serializes to `{}`, so the door converts it rather than each command flattening
|
|
11
|
+
* its own result: `GraphResult.nodes` is a Map, and `graph --json` would have printed a
|
|
12
|
+
* report with an empty graph in it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function machineText(value: unknown): string;
|
|
15
|
+
export declare function printMachine(value: unknown): void;
|
|
16
|
+
//# sourceMappingURL=machine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine.d.ts","sourceRoot":"","sources":["../src/machine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAEnE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAElD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAEjD"}
|
package/dist/machine.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command's machine output — the one shape a pipe reads.
|
|
3
|
+
*
|
|
4
|
+
* There is no list of commands here: `json` is declared by an entity like any other
|
|
5
|
+
* field, so a command that declares it gets the door. The runner used to name `explain`
|
|
6
|
+
* in an `if`, and `graph --json` announced a flag it then ignored.
|
|
7
|
+
*/
|
|
8
|
+
export function machineWanted(raw) {
|
|
9
|
+
return raw.json === true || typeof raw.names === 'string';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A `Map` serializes to `{}`, so the door converts it rather than each command flattening
|
|
13
|
+
* its own result: `GraphResult.nodes` is a Map, and `graph --json` would have printed a
|
|
14
|
+
* report with an empty graph in it.
|
|
15
|
+
*/
|
|
16
|
+
export function machineText(value) {
|
|
17
|
+
return JSON.stringify(value, (_key, held) => (held instanceof Map ? Object.fromEntries(held) : held), 2);
|
|
18
|
+
}
|
|
19
|
+
export function printMachine(value) {
|
|
20
|
+
process.stdout.write(machineText(value) + '\n');
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=machine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine.js","sourceRoot":"","sources":["../src/machine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,GAA4B;IACxD,OAAO,GAAG,CAAC,IAAI,KAAK,IAAI,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,QAAQ,CAAC;AAC5D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3G,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;AAClD,CAAC"}
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAwCzC,wBAAsB,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAgGjD"}
|
package/dist/runner.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createAppRunner } from '@fougere/core';
|
|
|
2
2
|
import { lowerFirst } from '@fougere/core/contract';
|
|
3
3
|
import { defineCommand, runMain } from 'citty';
|
|
4
4
|
import { ui } from './ui.js';
|
|
5
|
+
import { machineWanted } from './machine.js';
|
|
5
6
|
import { entityToArgs } from './bridge.js';
|
|
6
7
|
import { readdir } from 'node:fs/promises';
|
|
7
8
|
import { join } from 'node:path';
|
|
@@ -31,7 +32,6 @@ async function loadAppCommands(cliRoot, loader) {
|
|
|
31
32
|
export async function run(app) {
|
|
32
33
|
const terminal = ui();
|
|
33
34
|
const cliRoot = new URL('..', import.meta.url).pathname;
|
|
34
|
-
// Load app commands (presentation layer)
|
|
35
35
|
const { createJiti } = await import('jiti');
|
|
36
36
|
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
37
37
|
const loader = (path) => jiti.import(path);
|
|
@@ -76,10 +76,12 @@ export async function run(app) {
|
|
|
76
76
|
},
|
|
77
77
|
args,
|
|
78
78
|
run: async ({ args: parsed }) => {
|
|
79
|
-
// JSON is a protocol: a branded intro before `{` makes it unparsable.
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
// JSON is a protocol: a branded intro before `{` makes it unparsable. Read from
|
|
80
|
+
// the invocation's own args, so any command declaring `json` gets a clean stdout
|
|
81
|
+
// — naming `explain` here is what let `graph --json` print its decorated box.
|
|
82
|
+
const machineOutput = machineWanted(parsed);
|
|
83
|
+
// `completion` is exempt by nature, not by flag: its output IS a shell script,
|
|
84
|
+
// there is no invocation of it that wants decoration.
|
|
83
85
|
if (cmdName !== 'completion' && !machineOutput)
|
|
84
86
|
terminal.intro();
|
|
85
87
|
// citty adds `_` (raw positionals) and `--` (passthrough); strip them
|
|
@@ -98,7 +100,13 @@ export async function run(app) {
|
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
catch (err) {
|
|
101
|
-
|
|
103
|
+
const said = err instanceof Error ? err.message : String(err);
|
|
104
|
+
// A machine reader parses stdout: a refusal printed there is a refusal that
|
|
105
|
+
// breaks the parse instead of being read. stderr is where it belongs.
|
|
106
|
+
if (machineOutput)
|
|
107
|
+
process.stderr.write(said + '\n');
|
|
108
|
+
else
|
|
109
|
+
terminal.error(said);
|
|
102
110
|
process.exit(1);
|
|
103
111
|
}
|
|
104
112
|
},
|
package/dist/runner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,OAAO,CAAC,KAAa;IAC5B,OAAO,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,8CAA8C;AAC9C,KAAK,UAAU,eAAe,CAC5B,OAAe,EACf,MAA0D;IAE1D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAE1E,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAAE,SAAS;QACjF,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QAC/E,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5C,IAAI,GAAG,CAAC,OAAO,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,GAAQ;IAChC,MAAM,QAAQ,GAAG,EAAE,EAAE,CAAC;IACtB,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;IAExD,
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC;AAED,SAAS,OAAO,CAAC,KAAa;IAC5B,OAAO,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,8CAA8C;AAC9C,KAAK,UAAU,eAAe,CAC5B,OAAe,EACf,MAA0D;IAE1D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAE1E,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAAE,SAAS;QACjF,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QAC/E,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5C,IAAI,GAAG,CAAC,OAAO,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,GAAQ;IAChC,MAAM,QAAQ,GAAG,EAAE,EAAE,CAAC;IACtB,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;IAExD,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAqC,CAAC;IACvF,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAE3D,MAAM,WAAW,GAAqD,EAAE,CAAC;IAEzE,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAEtE,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACpC,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACjD,IAAI,CAAC,YAAY;gBAAE,SAAS;YAE5B,MAAM,WAAW,GAAG,GAAG,MAAM,CAAC,IAAI,SAAS,CAAC;YAC5C,IAAI,OAAiC,CAAC;YACtC,IAAI,CAAC;gBACH,OAAO,GAAG,GAAG,CAAC,OAAO,CAA2B,WAAW,CAAC,CAAC;YAC/D,CAAC;YAAC,MAAM,CAAC;gBAAC,SAAS;YAAC,CAAC;YAErB,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,UAAU;gBAAE,SAAS;YAEpD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACrC,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC;YAC9C,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;YAElC,gDAAgD;YAChD,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAE5C,gFAAgF;YAChF,IAAI,UAAU,EAAE,CAAC;gBACf,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;oBACtC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG;wBAAG,GAA+B,CAAC,QAAQ,GAAG,KAAK,CAAC;gBACxF,CAAC;YACH,CAAC;YAED,WAAW,CAAC,OAAO,CAAC,GAAG,aAAa,CAAC;gBACnC,IAAI,EAAE;oBACJ,IAAI,EAAE,OAAO;oBACb,0EAA0E;oBAC1E,yEAAyE;oBACzE,2EAA2E;oBAC3E,2EAA2E;oBAC3E,WAAW,EAAE,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,WAAW;iBACjE;gBACD,IAAI;gBACJ,GAAG,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE;oBAC9B,gFAAgF;oBAChF,iFAAiF;oBACjF,8EAA8E;oBAC9E,MAAM,aAAa,GAAG,aAAa,CAAC,MAAiC,CAAC,CAAC;oBAEvE,+EAA+E;oBAC/E,sDAAsD;oBACtD,IAAI,OAAO,KAAK,YAAY,IAAI,CAAC,aAAa;wBAAE,QAAQ,CAAC,KAAK,EAAE,CAAC;oBAEjE,sEAAsE;oBACtE,qDAAqD;oBACrD,MAAM,KAAK,GAAG,EAAE,GAAI,MAAkC,EAAE,CAAC;oBACzD,OAAO,KAAK,CAAC,CAAC,CAAC;oBACf,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC;oBAEnB,IAAI,CAAC;wBACH,IAAI,UAAU,EAAE,CAAC;4BACf,MAAM,GAAG,GAAG,IAAK,UAAgG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;4BACjI,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;wBACvB,CAAC;6BAAM,CAAC;4BACN,kEAAkE;4BAClE,MAAM,eAAe,CAAC,GAAG,CAAC,CACxB,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,EAClD,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAClD,CAAC;wBACJ,CAAC;oBACH,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,MAAM,IAAI,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;wBAC9D,4EAA4E;wBAC5E,sEAAsE;wBACtE,IAAI,aAAa;4BAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;;4BAChD,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;wBAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;oBAClB,CAAC;gBACH,CAAC;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC;QACzB,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE;QACrD,WAAW;KACZ,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { entity, text, optional } from "@fougere/schema";
|
|
1
|
+
import { entity, bool, text, optional } from "@fougere/schema";
|
|
2
2
|
|
|
3
3
|
/** `fougere build` — write down what a scan found, so a deployment reads no disk. */
|
|
4
4
|
export default class Build extends entity({
|
|
5
5
|
root: optional(text({ description: "Project to read. Default: the current directory" })),
|
|
6
6
|
out: optional(text({ description: "Where to write it. Default: .fougere/scan.generated.ts" })),
|
|
7
|
+
json: bool({ default: false, description: "Print the report as JSON, with nothing else on stdout" }),
|
|
7
8
|
}) {}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { entity, text } from '@fougere/schema';
|
|
1
|
+
import { entity, bool, text } from '@fougere/schema';
|
|
2
2
|
|
|
3
3
|
export default class Check extends entity({
|
|
4
4
|
root: text({ description: 'Project root directory (default: cwd)' }),
|
|
5
|
+
json: bool({ default: false, description: 'Print the report as JSON, with nothing else on stdout' }),
|
|
5
6
|
}) {}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { entity, bool, text, optional } from '@fougere/schema';
|
|
2
|
+
|
|
3
|
+
/** `fougere devtools` — what the running apps of this project are dispatching. */
|
|
4
|
+
export default class Devtools extends entity({
|
|
5
|
+
url: optional(text({ description: 'One app to read. Absent: every address this project declares' })),
|
|
6
|
+
root: optional(text({ description: 'Project to read the addresses from. Default: the current directory' })),
|
|
7
|
+
json: bool({ default: false, description: 'Print one page as JSON and stop, instead of following' }),
|
|
8
|
+
}) {}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { entity, text, optional } from "@fougere/schema";
|
|
1
|
+
import { entity, bool, text, optional } from "@fougere/schema";
|
|
2
2
|
|
|
3
3
|
/** `fougere freeze <version>` — record today's shapes under a name, for good. */
|
|
4
4
|
export default class Freeze extends entity({
|
|
5
5
|
version: text({ min: 1, description: "Name to record these shapes under (e.g. v2)" }),
|
|
6
6
|
root: optional(text({ description: "Project to read. Default: the current directory" })),
|
|
7
|
+
json: bool({ default: false, description: "Print the report as JSON, with nothing else on stdout" }),
|
|
7
8
|
}) {}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { entity, text, number } from '@fougere/schema';
|
|
1
|
+
import { entity, bool, text, number } from '@fougere/schema';
|
|
2
2
|
|
|
3
3
|
export default class Graph extends entity({
|
|
4
4
|
root: text({ description: 'Project root directory (default: cwd)' }),
|
|
5
5
|
minEntities: number({ default: 6, description: 'Minimum entities before suggesting split' }),
|
|
6
|
+
json: bool({ default: false, description: 'Print the report as JSON, with nothing else on stdout' }),
|
|
6
7
|
}) {}
|
|
@@ -4,4 +4,5 @@ import { entity, bool, optional, text } from "@fougere/schema";
|
|
|
4
4
|
export default class Migrate extends entity({
|
|
5
5
|
apply: bool({ default: false, description: "Run it. Without this the plan is printed and nothing moves" }),
|
|
6
6
|
root: optional(text({ description: "Project to read. Default: the current directory" })),
|
|
7
|
+
json: bool({ default: false, description: "Print the report as JSON, with nothing else on stdout" }),
|
|
7
8
|
}) {}
|
|
@@ -39,7 +39,8 @@ export default class BuildHandler {
|
|
|
39
39
|
constructor(private projectScan: ProjectScan) {}
|
|
40
40
|
|
|
41
41
|
/** Write down what a scan found, so a deployment reads no disk. */
|
|
42
|
-
|
|
42
|
+
/** `json` is the presentation's, so it is not part of what this operation receives. */
|
|
43
|
+
async execute(input: Omit<Build, 'json'>): Promise<BuildReport> {
|
|
43
44
|
const scan = await this.projectScan.at(input.root ?? undefined);
|
|
44
45
|
const out = resolve(scan.root, input.out || DEFAULT_OUT);
|
|
45
46
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHttpTransport } from '@fougere/transport-http/client';
|
|
2
|
+
import type { CallPage, CallRecord } from '@fougere/core';
|
|
3
|
+
import ProjectScan from '../services/ProjectScan.js';
|
|
4
|
+
|
|
5
|
+
/** The default address of an app in development — Nuxt, Next and the site all sit there. */
|
|
6
|
+
const LOCAL = 'http://127.0.0.1:3000';
|
|
7
|
+
|
|
8
|
+
/** One address that was asked, and what came back from it. */
|
|
9
|
+
export interface CallSource {
|
|
10
|
+
url: string;
|
|
11
|
+
/** The frond `remotes:` names at this address, when that is where it came from. */
|
|
12
|
+
frond?: string;
|
|
13
|
+
cursor: number;
|
|
14
|
+
inFlight: number;
|
|
15
|
+
dropped: number;
|
|
16
|
+
/** Present instead of the numbers when the address did not answer, or serves no log. */
|
|
17
|
+
refusal?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Every app of this project, read at once.
|
|
22
|
+
*
|
|
23
|
+
* A reader pulls: no app registers anywhere, and an app started alone depends on nothing.
|
|
24
|
+
* The price is knowing the addresses, which the project already states — `remotes:` says
|
|
25
|
+
* where a call goes, so it also says where the other half of a call can be watched.
|
|
26
|
+
*/
|
|
27
|
+
export interface CallsView {
|
|
28
|
+
sources: CallSource[];
|
|
29
|
+
/** Every call, oldest first, each carrying the address it was read from. */
|
|
30
|
+
calls: (CallRecord & { source: string })[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default class DevtoolsHandler {
|
|
34
|
+
constructor(private projectScan: ProjectScan) {}
|
|
35
|
+
|
|
36
|
+
/** Read what the running apps have dispatched since a cursor per address. */
|
|
37
|
+
async execute(input: { url?: string; root?: string; since?: Record<string, number> }): Promise<CallsView> {
|
|
38
|
+
const asked = input.url ? [{ url: trimmed(input.url) }] : await this.addresses(input.root);
|
|
39
|
+
const since = input.since ?? {};
|
|
40
|
+
|
|
41
|
+
const read = await Promise.all(asked.map(async (one): Promise<CallSource & { calls: CallRecord[] }> => {
|
|
42
|
+
try {
|
|
43
|
+
const page = await createHttpTransport(one.url)(
|
|
44
|
+
{ entity: 'rpc', op: 'calls' },
|
|
45
|
+
{ params: {}, query: {}, body: { since: since[one.url] ?? 0 }, state: {} },
|
|
46
|
+
) as CallPage;
|
|
47
|
+
|
|
48
|
+
return { ...one, cursor: page.cursor, inFlight: page.inFlight, dropped: page.dropped, calls: page.calls };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
// An address that does not answer is a source with a reason, not a missing source:
|
|
51
|
+
// an app not started and an app without the package are different facts, and both
|
|
52
|
+
// are worth seeing beside the ones that answered.
|
|
53
|
+
return { ...one, cursor: since[one.url] ?? 0, inFlight: 0, dropped: 0, calls: [], refusal: said(err) };
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
sources: read.map(({ calls: _calls, ...source }) => source),
|
|
59
|
+
calls: read
|
|
60
|
+
.flatMap(({ url, calls }) => calls.map((call) => ({ ...call, source: url })))
|
|
61
|
+
.sort((a, b) => a.startedAt - b.startedAt || a.seq - b.seq),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The addresses this project declares, plus the local one.
|
|
67
|
+
*
|
|
68
|
+
* `remotes:` is the operator's statement of where a call goes; reading it here is not a
|
|
69
|
+
* second source of truth, it is the same one read for the other direction.
|
|
70
|
+
*/
|
|
71
|
+
private async addresses(root?: string): Promise<{ url: string; frond?: string }[]> {
|
|
72
|
+
const { config } = await this.projectScan.at(root);
|
|
73
|
+
const remotes = Object.entries(config.remotes ?? {})
|
|
74
|
+
.map(([frond, url]) => ({ url: trimmed(url), frond }));
|
|
75
|
+
|
|
76
|
+
return [{ url: LOCAL }, ...remotes.filter((one) => one.url !== LOCAL)];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function trimmed(url: string): string {
|
|
81
|
+
return url.replace(/\/$/, '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function said(err: unknown): string {
|
|
85
|
+
return err instanceof Error ? err.message : String(err);
|
|
86
|
+
}
|
|
@@ -40,7 +40,8 @@ export default class FreezeHandler {
|
|
|
40
40
|
* ambiguities and calls again with `renamed`. Splitting it would let a caller write
|
|
41
41
|
* a version it never inspected.
|
|
42
42
|
*/
|
|
43
|
-
|
|
43
|
+
/** `json` is the presentation's, so it is not part of what this operation receives. */
|
|
44
|
+
async execute(input: Omit<Freeze, 'json'> & { renamed?: Record<string, Record<string, string>> }): Promise<FreezeInspection> {
|
|
44
45
|
const fronds = await this.read(input);
|
|
45
46
|
const version = input.version;
|
|
46
47
|
const entities = fronds.flatMap(({ bundle }) => Object.keys(bundle.$defs ?? {}));
|
|
@@ -97,7 +98,7 @@ export default class FreezeHandler {
|
|
|
97
98
|
* `Fronds.schemas()` is deliberately flat — a fact heard in one frond is declared in
|
|
98
99
|
* another — so the per-frond map is built here, where the question IS per frond.
|
|
99
100
|
*/
|
|
100
|
-
private async read(input: Freeze) {
|
|
101
|
+
private async read(input: Omit<Freeze, 'json'>) {
|
|
101
102
|
const scan = await this.projectScan.at(input.root ?? undefined);
|
|
102
103
|
return Promise.all(
|
|
103
104
|
scan.fronds
|
|
@@ -31,7 +31,8 @@ export default class MigrateHandler {
|
|
|
31
31
|
constructor(private projectScan: ProjectScan) {}
|
|
32
32
|
|
|
33
33
|
/** Realise the frozen steps this database has not caught up with. */
|
|
34
|
-
|
|
34
|
+
/** `json` is the presentation's, so it is not part of what this operation receives. */
|
|
35
|
+
async execute(input: Omit<Migrate, 'json'>): Promise<MigrationPlan> {
|
|
35
36
|
const scan = await this.projectScan.at(input.root ?? undefined);
|
|
36
37
|
const perFrond = await Promise.all(scan.fronds.map((frond) => stepsOf(frond.source.path)));
|
|
37
38
|
const steps = perFrond.flat();
|
|
@@ -251,10 +251,10 @@ export default class SyncHandler {
|
|
|
251
251
|
},
|
|
252
252
|
}, null, 2) + '\n');
|
|
253
253
|
|
|
254
|
-
//
|
|
254
|
+
// .fougere/remotes.json is the central registry of synced remotes.
|
|
255
255
|
this.updateRemotesRegistry(input.frond, baseUrl, frondDir);
|
|
256
256
|
|
|
257
|
-
//
|
|
257
|
+
// Non-Nuxt projects only: Nuxt writes its own paths.
|
|
258
258
|
this.updateTsconfigPaths(input.frond, frondDir, conventions);
|
|
259
259
|
|
|
260
260
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-alpha.1",
|
|
4
4
|
"description": "The Fougere CLI — compose a workspace, serve a frond, call an operation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"consola": "^3.4.2",
|
|
42
42
|
"jiti": "^2.4.2",
|
|
43
43
|
"picocolors": "^1.1.1",
|
|
44
|
-
"@fougere/
|
|
45
|
-
"@fougere/
|
|
46
|
-
"@fougere/transport-http": "0.
|
|
47
|
-
"@fougere/
|
|
48
|
-
"@fougere/
|
|
49
|
-
"@fougere/
|
|
44
|
+
"@fougere/core": "0.5.0-alpha.1",
|
|
45
|
+
"@fougere/adapter-sql": "0.5.0-alpha.1",
|
|
46
|
+
"@fougere/transport-http": "0.5.0-alpha.1",
|
|
47
|
+
"@fougere/schema": "0.5.0-alpha.1",
|
|
48
|
+
"@fougere/container": "0.5.0-alpha.1",
|
|
49
|
+
"@fougere/defaults": "0.5.0-alpha.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"vitest": "^4.1.0"
|
package/src/machine.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A command's machine output — the one shape a pipe reads.
|
|
3
|
+
*
|
|
4
|
+
* There is no list of commands here: `json` is declared by an entity like any other
|
|
5
|
+
* field, so a command that declares it gets the door. The runner used to name `explain`
|
|
6
|
+
* in an `if`, and `graph --json` announced a flag it then ignored.
|
|
7
|
+
*/
|
|
8
|
+
export function machineWanted(raw: Record<string, unknown>): boolean {
|
|
9
|
+
return raw.json === true || typeof raw.names === 'string';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A `Map` serializes to `{}`, so the door converts it rather than each command flattening
|
|
14
|
+
* its own result: `GraphResult.nodes` is a Map, and `graph --json` would have printed a
|
|
15
|
+
* report with an empty graph in it.
|
|
16
|
+
*/
|
|
17
|
+
export function machineText(value: unknown): string {
|
|
18
|
+
return JSON.stringify(value, (_key, held) => (held instanceof Map ? Object.fromEntries(held) : held), 2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function printMachine(value: unknown): void {
|
|
22
|
+
process.stdout.write(machineText(value) + '\n');
|
|
23
|
+
}
|
package/src/runner.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createAppRunner } from '@fougere/core';
|
|
|
12
12
|
import { lowerFirst } from '@fougere/core/contract';
|
|
13
13
|
import { defineCommand, runMain } from 'citty';
|
|
14
14
|
import { ui } from './ui.js';
|
|
15
|
+
import { machineWanted } from './machine.js';
|
|
15
16
|
import { entityToArgs } from './bridge.js';
|
|
16
17
|
import { readdir } from 'node:fs/promises';
|
|
17
18
|
import { join } from 'node:path';
|
|
@@ -50,7 +51,6 @@ export async function run(app: App): Promise<void> {
|
|
|
50
51
|
const terminal = ui();
|
|
51
52
|
const cliRoot = new URL('..', import.meta.url).pathname;
|
|
52
53
|
|
|
53
|
-
// Load app commands (presentation layer)
|
|
54
54
|
const { createJiti } = await import('jiti');
|
|
55
55
|
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
56
56
|
const loader = (path: string) => jiti.import(path) as Promise<Record<string, unknown>>;
|
|
@@ -98,10 +98,13 @@ export async function run(app: App): Promise<void> {
|
|
|
98
98
|
},
|
|
99
99
|
args,
|
|
100
100
|
run: async ({ args: parsed }) => {
|
|
101
|
-
// JSON is a protocol: a branded intro before `{` makes it unparsable.
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
// JSON is a protocol: a branded intro before `{` makes it unparsable. Read from
|
|
102
|
+
// the invocation's own args, so any command declaring `json` gets a clean stdout
|
|
103
|
+
// — naming `explain` here is what let `graph --json` print its decorated box.
|
|
104
|
+
const machineOutput = machineWanted(parsed as Record<string, unknown>);
|
|
105
|
+
|
|
106
|
+
// `completion` is exempt by nature, not by flag: its output IS a shell script,
|
|
107
|
+
// there is no invocation of it that wants decoration.
|
|
105
108
|
if (cmdName !== 'completion' && !machineOutput) terminal.intro();
|
|
106
109
|
|
|
107
110
|
// citty adds `_` (raw positionals) and `--` (passthrough); strip them
|
|
@@ -122,7 +125,11 @@ export async function run(app: App): Promise<void> {
|
|
|
122
125
|
);
|
|
123
126
|
}
|
|
124
127
|
} catch (err) {
|
|
125
|
-
|
|
128
|
+
const said = err instanceof Error ? err.message : String(err);
|
|
129
|
+
// A machine reader parses stdout: a refusal printed there is a refusal that
|
|
130
|
+
// breaks the parse instead of being read. stderr is where it belongs.
|
|
131
|
+
if (machineOutput) process.stderr.write(said + '\n');
|
|
132
|
+
else terminal.error(said);
|
|
126
133
|
process.exit(1);
|
|
127
134
|
}
|
|
128
135
|
},
|
|
@@ -26,7 +26,7 @@ export default class PostHandler extends Crud(Post) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Only published posts exist for the outside world, projected to the card. */
|
|
29
|
-
async
|
|
29
|
+
async listPublished(): Promise<PostCard[]> {
|
|
30
30
|
const posts = await this.orm.list({ where: { status: 'published' } });
|
|
31
31
|
return posts.map(({ id, title, status }) => ({ id, title, status }));
|
|
32
32
|
}
|
|
@@ -17,7 +17,7 @@ export default class PostHandler extends Crud(Post) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Only published posts, projected to the card. */
|
|
20
|
-
async
|
|
20
|
+
async listPublished(): Promise<PostCard[]> {
|
|
21
21
|
const posts = await this.orm.list({ where: { status: 'published' } });
|
|
22
22
|
return posts.map(({ id, title, status }) => ({ id, title, status }));
|
|
23
23
|
}
|
|
@@ -29,7 +29,7 @@ export default class PostHandler extends Crud(Post) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Only published posts, projected to the card. */
|
|
32
|
-
async
|
|
32
|
+
async listPublished(): Promise<PostCard[]> {
|
|
33
33
|
const posts = await this.orm.list({ where: { status: 'published' } });
|
|
34
34
|
return posts.map(({ id, title, status }) => ({ id, title, status }));
|
|
35
35
|
}
|