@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/dist/completion.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completion.js","sourceRoot":"","sources":["../src/completion.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"completion.js","sourceRoot":"","sources":["../src/completion.ts"],"names":[],"mappings":"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"}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Conventions } from '@fougere/core/node';
|
|
2
|
+
/**
|
|
3
|
+
* The loader every command needs: `alias` is what makes `@fronds/user/entities/User.js`
|
|
4
|
+
* resolve when one frond names its neighbour, in any command that loads user code.
|
|
5
|
+
*
|
|
6
|
+
* Two jitis, because the config is read BEFORE the aliases — it names the scope they are
|
|
7
|
+
* built from. `reread` drops the module cache: every loader caches by specifier, so a
|
|
8
|
+
* second boot in one process would be handed what the first one read.
|
|
9
|
+
*/
|
|
10
|
+
export declare function installLoader(root: string, reread?: boolean): Promise<Conventions>;
|
|
11
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,UAAQ,GAAG,OAAO,CAAC,WAAW,CAAC,CActF"}
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { setModuleLoader, loadConfig, resolveConventions, frondAliases, } from '@fougere/core/node';
|
|
2
|
+
/**
|
|
3
|
+
* The loader every command needs: `alias` is what makes `@fronds/user/entities/User.js`
|
|
4
|
+
* resolve when one frond names its neighbour, in any command that loads user code.
|
|
5
|
+
*
|
|
6
|
+
* Two jitis, because the config is read BEFORE the aliases — it names the scope they are
|
|
7
|
+
* built from. `reread` drops the module cache: every loader caches by specifier, so a
|
|
8
|
+
* second boot in one process would be handed what the first one read.
|
|
9
|
+
*/
|
|
10
|
+
export async function installLoader(root, reread = false) {
|
|
11
|
+
const { createJiti } = await import('jiti');
|
|
12
|
+
const bare = createJiti(import.meta.url, { interopDefault: true });
|
|
13
|
+
setModuleLoader((filePath) => bare.import(filePath));
|
|
14
|
+
const conventions = resolveConventions((await loadConfig(root)).conventions);
|
|
15
|
+
const jiti = createJiti(import.meta.url, {
|
|
16
|
+
interopDefault: true,
|
|
17
|
+
alias: await frondAliases(root, conventions),
|
|
18
|
+
...(reread ? { moduleCache: false } : {}),
|
|
19
|
+
});
|
|
20
|
+
setModuleLoader((filePath) => jiti.import(filePath));
|
|
21
|
+
return conventions;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EAAE,UAAU,EAAE,kBAAkB,EAAE,YAAY,GAC9D,MAAM,oBAAoB,CAAC;AAG5B;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,MAAM,GAAG,KAAK;IAC9D,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,eAAe,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAqC,CAAC,CAAC;IAEzF,MAAM,WAAW,GAAG,kBAAkB,CAAC,CAAC,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE;QACvC,cAAc,EAAE,IAAI;QACpB,KAAK,EAAE,MAAM,YAAY,CAAC,IAAI,EAAE,WAAW,CAAC;QAC5C,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1C,CAAC,CAAC;IACH,eAAe,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAqC,CAAC,CAAC;IAEzF,OAAO,WAAW,CAAC;AACrB,CAAC"}
|
|
@@ -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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createAppRunner } from '@fougere/core';
|
|
2
|
-
import {
|
|
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,9 +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
|
-
|
|
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.
|
|
82
85
|
if (cmdName !== 'completion' && !machineOutput)
|
|
83
86
|
terminal.intro();
|
|
84
87
|
// citty adds `_` (raw positionals) and `--` (passthrough); strip them
|
|
@@ -93,11 +96,17 @@ export async function run(app) {
|
|
|
93
96
|
}
|
|
94
97
|
else {
|
|
95
98
|
// Ride the call contract — the same envelope every consumer uses.
|
|
96
|
-
await createAppRunner(app)({ entity:
|
|
99
|
+
await createAppRunner(app)({ entity: lowerFirst(entity.name), op: 'execute' }, { params: {}, query: {}, body: input, state: {} });
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
catch (err) {
|
|
100
|
-
|
|
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);
|
|
101
110
|
process.exit(1);
|
|
102
111
|
}
|
|
103
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,
|
|
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,8 +1,9 @@
|
|
|
1
|
-
import { bool, entity, optional, text } from '@fougere/schema';
|
|
1
|
+
import { bool, entity, oneOf, optional, text } from '@fougere/schema';
|
|
2
2
|
|
|
3
3
|
/** `fougere explain <Operation>` — print the operation contract Fougere will serve. */
|
|
4
4
|
export default class Explain extends entity({
|
|
5
5
|
operation: text({ min: 1, description: 'Operation to inspect (e.g. Post.publish)' }),
|
|
6
6
|
root: optional(text({ description: 'Project to read. Default: the current directory' })),
|
|
7
7
|
json: bool({ default: false, description: 'Print stable machine-readable JSON' }),
|
|
8
|
+
names: optional(oneOf('operations', 'fronds', { description: 'Print one name per line — what shell completion reads' })),
|
|
8
9
|
}) {}
|
|
@@ -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
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '@fougere/core';
|
|
6
6
|
import { relative } from 'node:path';
|
|
7
7
|
import ProjectScan from '../services/ProjectScan.js';
|
|
8
|
+
import { ANONYMOUS_SCHEMA_NAME, lowerFirst, type SchemaView } from '@fougere/schema';
|
|
8
9
|
|
|
9
10
|
type Cardinality = NonNullable<OperationContract['cardinality']>;
|
|
10
11
|
type Binding = EffectiveOperation['binding'][number];
|
|
@@ -60,6 +61,17 @@ export interface ExplainResult {
|
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/** What this project serves, when no single operation was named. */
|
|
65
|
+
export interface ExplainListing {
|
|
66
|
+
fronds: {
|
|
67
|
+
name: string;
|
|
68
|
+
runtime: 'local' | 'remote';
|
|
69
|
+
remote: string | null;
|
|
70
|
+
operations: number;
|
|
71
|
+
}[];
|
|
72
|
+
operations: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
interface Selector {
|
|
64
76
|
frond?: string;
|
|
65
77
|
surface?: string;
|
|
@@ -71,17 +83,46 @@ interface Selector {
|
|
|
71
83
|
export default class ExplainHandler {
|
|
72
84
|
constructor(private projectScan: ProjectScan) {}
|
|
73
85
|
|
|
86
|
+
/** The names this project serves — the list `explain` used to spell in a refusal only. */
|
|
87
|
+
async list(input: { root?: string }): Promise<ExplainListing> {
|
|
88
|
+
const { scan, model } = await this.modelOf(input.root);
|
|
89
|
+
const remotes = scan.config.remotes ?? {};
|
|
90
|
+
const counted = new Map<string, number>();
|
|
91
|
+
for (const operation of model.operations) {
|
|
92
|
+
const frond = operation.placement.frond;
|
|
93
|
+
counted.set(frond, (counted.get(frond) ?? 0) + 1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
fronds: scan.fronds.map((frond) => ({
|
|
98
|
+
name: frond.name,
|
|
99
|
+
runtime: remotes[frond.name] ? 'remote' as const : 'local' as const,
|
|
100
|
+
remote: remotes[frond.name] ?? null,
|
|
101
|
+
operations: counted.get(frond.name) ?? 0,
|
|
102
|
+
})).sort((a, b) => a.name.localeCompare(b.name)),
|
|
103
|
+
operations: model.operations.map((operation) => operation.id).sort(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async modelOf(root?: string) {
|
|
108
|
+
const scan = await this.projectScan.at(root);
|
|
109
|
+
return {
|
|
110
|
+
scan,
|
|
111
|
+
model: resolveEffectiveOperations(scan.fronds, {
|
|
112
|
+
diagnostics: scan.diagnostics,
|
|
113
|
+
remotes: scan.config.remotes,
|
|
114
|
+
adapters: scan.config.adapters,
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Print the contract one operation will be served under. */
|
|
74
120
|
async execute(input: { operation?: string; root?: string; json?: boolean }): Promise<ExplainResult> {
|
|
75
121
|
const requested = input.operation?.trim();
|
|
76
122
|
if (!requested) throw new Error('Usage: fougere explain <Operation> [--json] [--root <directory>]');
|
|
77
123
|
|
|
78
124
|
const selector = parseSelector(requested);
|
|
79
|
-
const scan = await this.
|
|
80
|
-
const model = resolveEffectiveOperations(scan.fronds, {
|
|
81
|
-
diagnostics: scan.diagnostics,
|
|
82
|
-
remotes: scan.config.remotes,
|
|
83
|
-
adapters: scan.config.adapters,
|
|
84
|
-
});
|
|
125
|
+
const { scan, model } = await this.modelOf(input.root);
|
|
85
126
|
const candidates = model.operations.filter((operation) => matches(operation, selector));
|
|
86
127
|
|
|
87
128
|
if (candidates.length === 0) {
|
|
@@ -184,7 +225,7 @@ function matches(operation: EffectiveOperation, selector: Selector): boolean {
|
|
|
184
225
|
|
|
185
226
|
function addressOf(value: string): string {
|
|
186
227
|
const base = value.endsWith('Handler') ? value.slice(0, -'Handler'.length) : value;
|
|
187
|
-
return
|
|
228
|
+
return lowerFirst(base);
|
|
188
229
|
}
|
|
189
230
|
|
|
190
231
|
function inputTypeOf(operation: EffectiveOperation): string | null {
|
|
@@ -197,11 +238,10 @@ function outputOf(operation: EffectiveOperation): ExplainResult['output'] {
|
|
|
197
238
|
return type ? { type, cardinality: operation.cardinality ?? null } : null;
|
|
198
239
|
}
|
|
199
240
|
|
|
200
|
-
function schemaName(schema:
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
return view.source?.name;
|
|
241
|
+
function schemaName(schema: SchemaView | undefined): string | undefined {
|
|
242
|
+
if (!schema) return undefined;
|
|
243
|
+
if (schema.name && schema.name !== ANONYMOUS_SCHEMA_NAME) return schema.name;
|
|
244
|
+
return schema.derivation?.sourceName;
|
|
205
245
|
}
|
|
206
246
|
|
|
207
247
|
function parsedOutput(raw: string | undefined): string | undefined {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { Bundle, lowerFirst, type SchemaBundle, type SchemaView, type SetDiff } from '@fougere/schema';
|
|
4
4
|
import ProjectScan from '../services/ProjectScan.js';
|
|
5
5
|
import { VERSIONS, chainOf } from '../versions.js';
|
|
6
6
|
import type Freeze from '../entities/Freeze.js';
|
|
@@ -13,7 +13,7 @@ export interface FreezeInspection {
|
|
|
13
13
|
/** What the step contains — absent when there is nothing before to step from. */
|
|
14
14
|
step?: SetDiff;
|
|
15
15
|
/** Per entity, the pairs the calculation refuses to decide. Empty means it was written. */
|
|
16
|
-
ambiguous: Record<string,
|
|
16
|
+
ambiguous: Record<string, { removed: string; added: string }[]>;
|
|
17
17
|
/** Whether anything reached the disk — false while a question stands. */
|
|
18
18
|
written: boolean;
|
|
19
19
|
}
|
|
@@ -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 ?? {}));
|
|
@@ -54,7 +55,9 @@ export default class FreezeHandler {
|
|
|
54
55
|
path,
|
|
55
56
|
bundle,
|
|
56
57
|
previous,
|
|
57
|
-
step: previous
|
|
58
|
+
step: previous
|
|
59
|
+
? Bundle.fromDescriptor(previous.bundle).diff(Bundle.fromDescriptor(bundle), { renamed })
|
|
60
|
+
: undefined,
|
|
58
61
|
}));
|
|
59
62
|
|
|
60
63
|
const ambiguous: FreezeInspection['ambiguous'] = {};
|
|
@@ -95,14 +98,16 @@ export default class FreezeHandler {
|
|
|
95
98
|
* `Fronds.schemas()` is deliberately flat — a fact heard in one frond is declared in
|
|
96
99
|
* another — so the per-frond map is built here, where the question IS per frond.
|
|
97
100
|
*/
|
|
98
|
-
private async read(input: Freeze) {
|
|
101
|
+
private async read(input: Omit<Freeze, 'json'>) {
|
|
99
102
|
const scan = await this.projectScan.at(input.root ?? undefined);
|
|
100
103
|
return Promise.all(
|
|
101
104
|
scan.fronds
|
|
102
105
|
.filter((frond) => frond.entities.length > 0)
|
|
103
106
|
.map(async (frond) => ({
|
|
104
107
|
path: frond.source.path,
|
|
105
|
-
bundle:
|
|
108
|
+
bundle: Bundle.fromSchemas(
|
|
109
|
+
Object.fromEntries(frond.entities.map((e) => [e.name, e.entityClass])),
|
|
110
|
+
).descriptor,
|
|
106
111
|
declared: declaredRenames(frond.entities),
|
|
107
112
|
previous: await previousOf(frond.source.path, input.version),
|
|
108
113
|
})),
|
|
@@ -114,17 +119,17 @@ type Inspected = { previous?: { name: string }; step?: SetDiff };
|
|
|
114
119
|
|
|
115
120
|
/**
|
|
116
121
|
* What the entities state about themselves — `previous` says what a field WAS, while
|
|
117
|
-
* `diff` reads old
|
|
122
|
+
* `Bundle.diff` reads old to new, so the pair is turned around here and nowhere else.
|
|
118
123
|
*/
|
|
119
124
|
function declaredRenames(
|
|
120
|
-
entities:
|
|
125
|
+
entities: readonly { name: string; entityClass: unknown }[],
|
|
121
126
|
): Record<string, Record<string, string>> {
|
|
122
127
|
const out: Record<string, Record<string, string>> = {};
|
|
123
128
|
for (const { name, entityClass } of entities) {
|
|
124
|
-
const previous = (entityClass as
|
|
125
|
-
// Keyed as `
|
|
129
|
+
const previous = (entityClass as SchemaView).previous;
|
|
130
|
+
// Keyed as `Bundle.fromSchemas` keys `$defs`, which is what `Bundle.diff` reads. Spelling the
|
|
126
131
|
// convention a second way here is the defect this repo has already recorded twice.
|
|
127
|
-
const key =
|
|
132
|
+
const key = lowerFirst(name);
|
|
128
133
|
if (previous) out[key] = Object.fromEntries(Object.entries(previous).map(([now, was]) => [was, now]));
|
|
129
134
|
}
|
|
130
135
|
return out;
|
|
@@ -132,12 +137,12 @@ function declaredRenames(
|
|
|
132
137
|
|
|
133
138
|
/** Every source of an answer, folded per entity — later sources win field by field. */
|
|
134
139
|
function settled(
|
|
135
|
-
sources:
|
|
140
|
+
sources: readonly Record<string, Record<string, string>>[],
|
|
136
141
|
answers: Record<string, Record<string, string>>,
|
|
137
142
|
): Record<string, Record<string, string>> {
|
|
138
143
|
const out: Record<string, Record<string, string>> = {};
|
|
139
144
|
for (const source of [...sources, answers]) {
|
|
140
|
-
for (const [entity, pairs] of Object.entries(source)) out[entity] = { ...
|
|
145
|
+
for (const [entity, pairs] of Object.entries(source)) out[entity] = { ...out[entity], ...pairs };
|
|
141
146
|
}
|
|
142
147
|
return out;
|
|
143
148
|
}
|
|
@@ -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();
|
|
@@ -77,7 +78,7 @@ export default class MigrateHandler {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/** The versions read, oldest first and each named once however many fronds cut it. */
|
|
80
|
-
function versionsOf(steps:
|
|
81
|
+
function versionsOf(steps: readonly { version: string }[]): string[] {
|
|
81
82
|
return [...new Set(steps.map(({ version }) => version))].sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -90,7 +91,7 @@ function onSource(step: SetDiff, source: string, sourceOf: (entity: string) => s
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
/** Every recorded step, oldest first — the chain composes, so it is replayed whole. */
|
|
93
|
-
async function stepsOf(frondPath: string): Promise<
|
|
94
|
+
async function stepsOf(frondPath: string): Promise<{ version: string; step: SetDiff }[]> {
|
|
94
95
|
const chain = await chainOf(frondPath);
|
|
95
96
|
// The first version has a shape and no step — there was nothing before it to move from.
|
|
96
97
|
return chain.flatMap(({ name, step }) => (step ? [{ version: name, step }] : []));
|
|
@@ -2,5 +2,5 @@ import { entity, text } from "@fougere/schema";
|
|
|
2
2
|
|
|
3
3
|
/** `fougere call <entity>.<op> [--field value …]` — invoke one operation, print the result. */
|
|
4
4
|
export default class Call extends entity({
|
|
5
|
-
|
|
5
|
+
operation: text({ min: 1, description: "entity.op to invoke (e.g. post.create)" }),
|
|
6
6
|
}) {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { entity, text } from '@fougere/schema';
|
|
2
2
|
|
|
3
3
|
export default class Sync extends entity({
|
|
4
|
-
|
|
4
|
+
frond: text({ description: 'Frond name to sync (e.g. blog)' }),
|
|
5
5
|
from: text({ description: 'Remote URL (e.g. http://blog-service:3000)' }),
|
|
6
6
|
}) {}
|