@fougere/cli 0.6.0-alpha.0 → 0.8.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.
Files changed (72) hide show
  1. package/app/commands/BuildCommand.ts +1 -1
  2. package/app/commands/CallCommand.ts +3 -3
  3. package/app/commands/CheckCommand.ts +1 -1
  4. package/app/commands/DevtoolsCommand.ts +1 -1
  5. package/app/commands/ExplainCommand.ts +1 -1
  6. package/app/commands/FreezeCommand.ts +2 -2
  7. package/app/commands/GraphCommand.ts +1 -1
  8. package/app/commands/MigrateCommand.ts +1 -1
  9. package/app/commands/NewCommand.ts +5 -3
  10. package/dist/bin.js +3 -14
  11. package/dist/bin.js.map +1 -1
  12. package/dist/bridge.d.ts +1 -6
  13. package/dist/bridge.d.ts.map +1 -1
  14. package/dist/bridge.js +38 -39
  15. package/dist/bridge.js.map +1 -1
  16. package/dist/completion.d.ts +1 -8
  17. package/dist/completion.d.ts.map +1 -1
  18. package/dist/completion.js.map +1 -1
  19. package/dist/loader.d.ts +1 -8
  20. package/dist/loader.d.ts.map +1 -1
  21. package/dist/loader.js +1 -8
  22. package/dist/loader.js.map +1 -1
  23. package/dist/machine.d.ts +4 -10
  24. package/dist/machine.d.ts.map +1 -1
  25. package/dist/machine.js +4 -10
  26. package/dist/machine.js.map +1 -1
  27. package/dist/runner.d.ts +2 -7
  28. package/dist/runner.d.ts.map +1 -1
  29. package/dist/runner.js +4 -4
  30. package/dist/runner.js.map +1 -1
  31. package/dist/typescript/EntityTypes.d.ts +12 -0
  32. package/dist/typescript/EntityTypes.d.ts.map +1 -0
  33. package/dist/typescript/EntityTypes.js +76 -0
  34. package/dist/typescript/EntityTypes.js.map +1 -0
  35. package/dist/typescript/FacadeTypes.d.ts +19 -0
  36. package/dist/typescript/FacadeTypes.d.ts.map +1 -0
  37. package/dist/typescript/FacadeTypes.js +34 -0
  38. package/dist/typescript/FacadeTypes.js.map +1 -0
  39. package/dist/typescript/syntax.d.ts +5 -0
  40. package/dist/typescript/syntax.d.ts.map +1 -0
  41. package/dist/typescript/syntax.js +11 -0
  42. package/dist/typescript/syntax.js.map +1 -0
  43. package/dist/ui.d.ts.map +1 -1
  44. package/dist/ui.js +1 -11
  45. package/dist/ui.js.map +1 -1
  46. package/fronds/analysis/handlers/CheckHandler.ts +61 -2
  47. package/fronds/analysis/handlers/DevtoolsHandler.ts +1 -1
  48. package/fronds/analysis/handlers/ExplainHandler.ts +2 -2
  49. package/fronds/analysis/handlers/MigrateHandler.ts +3 -1
  50. package/fronds/scaffold/handlers/SyncHandler.ts +9 -7
  51. package/fronds/scaffold/services/ProjectWriter.ts +43 -0
  52. package/fronds/shims.d.ts +9 -0
  53. package/package.json +8 -7
  54. package/src/bin.ts +3 -14
  55. package/src/bridge.ts +52 -41
  56. package/src/completion.ts +1 -8
  57. package/src/loader.ts +1 -8
  58. package/src/machine.ts +4 -10
  59. package/src/runner.ts +7 -12
  60. package/src/typescript/EntityTypes.ts +86 -0
  61. package/src/typescript/FacadeTypes.ts +48 -0
  62. package/src/typescript/syntax.ts +11 -0
  63. package/src/ui.ts +1 -11
  64. package/templates/admin/fronds/admin/handlers/UserHandler.ts +1 -1
  65. package/templates/api/fronds/api/handlers/TaskHandler.ts +1 -1
  66. package/templates/blog/app/pages/index.vue +1 -1
  67. package/templates/blog/app/pages/posts/manage.vue +2 -2
  68. package/templates/blog/fronds/blog/handlers/PostHandler.ts +2 -2
  69. package/templates/flat/CLAUDE.md +4 -4
  70. package/templates/frond/CLAUDE.md +4 -4
  71. package/templates/fronds/blank/handlers/ItemHandler.ts +1 -1
  72. package/templates/workspace/CLAUDE.md +4 -4
@@ -0,0 +1,19 @@
1
+ import type { SchemaDescriptor } from '@fougere/schema';
2
+ export interface FacadeTypesOptions {
3
+ name?: string;
4
+ exported?: boolean;
5
+ rowType?: string;
6
+ }
7
+ export interface OpDescriptor {
8
+ name: string;
9
+ description?: string;
10
+ output?: SchemaDescriptor;
11
+ cardinality?: 'one' | 'maybe' | 'many' | 'page' | 'none';
12
+ }
13
+ export declare class FacadeTypes {
14
+ private readonly operations;
15
+ private constructor();
16
+ static of(operations: readonly OpDescriptor[]): FacadeTypes;
17
+ render(options?: FacadeTypesOptions): string;
18
+ }
19
+ //# sourceMappingURL=FacadeTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FacadeTypes.d.ts","sourceRoot":"","sources":["../../src/typescript/FacadeTypes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGxD,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,WAAW,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;CAC1D;AAcD,qBAAa,WAAW;IACF,OAAO,CAAC,QAAQ,CAAC,UAAU;IAA/C,OAAO,eAAqE;IAE5E,MAAM,CAAC,EAAE,CAAC,UAAU,EAAE,SAAS,YAAY,EAAE,GAAG,WAAW,CAE1D;IAED,MAAM,CAAC,OAAO,GAAE,kBAAuB,GAAG,MAAM,CAW/C;CACF"}
@@ -0,0 +1,34 @@
1
+ import { docCommentOf, propertyKey } from './syntax.js';
2
+ /** So a consumer sees the cardinality in the type, not in a doc line. */
3
+ function returnTypeOf(op, rowType) {
4
+ switch (op.cardinality) {
5
+ case 'many': return `${rowType}[]`;
6
+ case 'page': return `${rowType}[] & { total?: number; endCursor?: string; hasMore?: boolean }`;
7
+ case 'maybe': return `${rowType} | undefined`;
8
+ case 'one': return rowType;
9
+ case 'none': return 'unknown';
10
+ default: return 'unknown';
11
+ }
12
+ }
13
+ export class FacadeTypes {
14
+ operations;
15
+ constructor(operations) {
16
+ this.operations = operations;
17
+ }
18
+ static of(operations) {
19
+ return new FacadeTypes(operations);
20
+ }
21
+ render(options = {}) {
22
+ const name = options.name ?? 'Facade';
23
+ const exported = options.exported === false ? '' : 'export ';
24
+ const rowType = options.rowType ?? 'unknown';
25
+ const members = this.operations.map((operation) => {
26
+ const doc = docCommentOf(operation.description, ' ');
27
+ return `${doc} ${propertyKey(operation.name)}(invocation?: Invocation): Promise<${returnTypeOf(operation, rowType)}>;`;
28
+ });
29
+ if (members.length === 0)
30
+ return `${exported}interface ${name} {}`;
31
+ return `${exported}interface ${name} {\n${members.join('\n')}\n}`;
32
+ }
33
+ }
34
+ //# sourceMappingURL=FacadeTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FacadeTypes.js","sourceRoot":"","sources":["../../src/typescript/FacadeTypes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAexD,yEAAyE;AACzE,SAAS,YAAY,CAAC,EAAgB,EAAE,OAAe;IACrD,QAAQ,EAAE,CAAC,WAAW,EAAE,CAAC;QACvB,KAAK,MAAM,EAAE,OAAO,GAAG,OAAO,IAAI,CAAC;QACnC,KAAK,MAAM,EAAE,OAAO,GAAG,OAAO,gEAAgE,CAAC;QAC/F,KAAK,OAAO,EAAE,OAAO,GAAG,OAAO,cAAc,CAAC;QAC9C,KAAK,KAAK,EAAE,OAAO,OAAO,CAAC;QAC3B,KAAK,MAAM,EAAE,OAAO,SAAS,CAAC;QAC9B,SAAS,OAAO,SAAS,CAAC;IAC5B,CAAC;AACH,CAAC;AAED,MAAM,OAAO,WAAW;IACe,UAAU;IAA/C,YAAqC,UAAmC;0BAAnC,UAAU;IAA4B,CAAC;IAE5E,MAAM,CAAC,EAAE,CAAC,UAAmC;QAC3C,OAAO,IAAI,WAAW,CAAC,UAAU,CAAC,CAAC;IACrC,CAAC;IAED,MAAM,CAAC,OAAO,GAAuB,EAAE;QACrC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,QAAQ,CAAC;QACtC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,SAAS,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;YAChD,MAAM,GAAG,GAAG,YAAY,CAAC,SAAS,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YACtD,OAAO,GAAG,GAAG,KAAK,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,sCAAsC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC;QAC1H,CAAC,CAAC,CAAC;QAEH,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,GAAG,QAAQ,aAAa,IAAI,KAAK,CAAC;QACnE,OAAO,GAAG,QAAQ,aAAa,IAAI,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;IACpE,CAAC;CACF"}
@@ -0,0 +1,5 @@
1
+ /** So a name that is not an identifier is still written as a key. */
2
+ export declare function propertyKey(name: string): string;
3
+ /** So a sentence shows on hover — and cannot close the comment it sits in. */
4
+ export declare function docCommentOf(text: string | undefined, indent: string): string;
5
+ //# sourceMappingURL=syntax.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syntax.d.ts","sourceRoot":"","sources":["../../src/typescript/syntax.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAI7E"}
@@ -0,0 +1,11 @@
1
+ /** So a name that is not an identifier is still written as a key. */
2
+ export function propertyKey(name) {
3
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
4
+ }
5
+ /** So a sentence shows on hover — and cannot close the comment it sits in. */
6
+ export function docCommentOf(text, indent) {
7
+ if (!text)
8
+ return '';
9
+ return `${indent}/** ${text.replace(/\*\//g, '*\\/')} */\n`;
10
+ }
11
+ //# sourceMappingURL=syntax.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syntax.js","sourceRoot":"","sources":["../../src/typescript/syntax.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AAC/E,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAC,IAAwB,EAAE,MAAc;IACnE,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAErB,OAAO,GAAG,MAAM,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC;AAC9D,CAAC"}
package/dist/ui.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAcA,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CAC/B;AAED,wBAAgB,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO;IAMhC,qDAAqD;;IAKrD,sCAAsC;mBACvB,MAAM;IAIrB,uBAAuB;;IAQvB,kBAAkB;eACD;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;KAAE;IAMnI,2BAA2B;kBACP;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE;IAM/D,8BAA8B;iBACX;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB;IAMD,gCAAgC;sBACR;QACtB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;IAQD,sDAAsD;sBACpC,MAAM;QAIpB,MAAM,QAAQ,MAAM;QACpB,IAAI,SAAS,MAAM;;IAMvB,6BAA6B;kBACf,MAAM;IAIpB,uBAAuB;qBACN,MAAM;IAIvB,uBAAuB;kBACT,MAAM;IAIpB,qBAAqB;mBACN,MAAM;IAIrB,sBAAsB;kBACR,MAAM;IAIpB,6CAA6C;kBAC/B,MAAM,UAAU,MAAM;;;;;;;;;;;;;EAUvC"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAIA,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CAC/B;AAED,wBAAgB,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO;IAMhC,qDAAqD;;IAKrD,sCAAsC;mBACvB,MAAM;IAIrB,uBAAuB;;IAQvB,kBAAkB;eACD;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;KAAE;IAMnI,2BAA2B;kBACP;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE;IAM/D,8BAA8B;iBACX;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB;IAMD,gCAAgC;sBACR;QACtB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;IAQD,sDAAsD;sBACpC,MAAM;QAIpB,MAAM,QAAQ,MAAM;QACpB,IAAI,SAAS,MAAM;;IAMvB,6BAA6B;kBACf,MAAM;IAIpB,uBAAuB;qBACN,MAAM;IAIvB,uBAAuB;kBACT,MAAM;IAIpB,qBAAqB;mBACN,MAAM;IAIrB,sBAAsB;kBACR,MAAM;IAIpB,6CAA6C;kBAC/B,MAAM,UAAU,MAAM;;;;;;;;;;;;;EAUvC"}
package/dist/ui.js CHANGED
@@ -1,14 +1,4 @@
1
- /**
2
- * Fougere CLI UI — beautiful terminal interface.
3
- *
4
- * Wraps @clack/prompts + picocolors + consola into a cohesive API.
5
- *
6
- * This was `@fougere/cli-ui`, a published package with exactly one consumer —
7
- * the CLI it is named after. A second name in the registry that nobody would
8
- * ever install on purpose is a name, not a boundary. Same dependency profile,
9
- * so it folds in as a module; a subpath export is one line the day something
10
- * outside the CLI wants it.
11
- */
1
+ /** Fougere CLI UI — beautiful terminal interface. */
12
2
  import * as clack from '@clack/prompts';
13
3
  import pc from 'picocolors';
14
4
  import { consola } from 'consola';
package/dist/ui.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,12 @@
1
+ import { Shapes } from '@fougere/schema';
2
+ import safeRegex from 'safe-regex';
1
3
  import {
2
4
  buildGraph,
3
5
  clusterEntities,
4
6
  resolveEffectiveOperations,
5
7
  type ScanDiagnostic,
6
8
  } from '@fougere/core';
7
- import { crossFrondImports } from '@fougere/core/node';
9
+ import { adaptersOf, crossFrondImports } from '@fougere/core/node';
8
10
  import ProjectScan from '../services/ProjectScan.js';
9
11
 
10
12
  /** One thing that does not hold, in the terms of whoever has to fix it. */
@@ -52,7 +54,7 @@ export default class CheckHandler {
52
54
 
53
55
  /** Report what does not hold in a Fougere app, without booting it. */
54
56
  async execute(input: { root?: string }): Promise<CheckResult> {
55
- const { fronds, diagnostics, config } = await this.projectScan.at(input.root);
57
+ const { fronds, diagnostics, config, root } = await this.projectScan.at(input.root);
56
58
  // This is the same pure resolution the boot consumes. No app lifecycle, database,
57
59
  // migration, seed or adapter mount is needed for a global semantic check.
58
60
  const model = resolveEffectiveOperations(fronds, {
@@ -87,6 +89,63 @@ export default class CheckHandler {
87
89
  }
88
90
  }
89
91
 
92
+ /**
93
+ * An entity addressing an adapter this project does not depend on. Judged HERE and
94
+ * not at boot: a process only knows the adapters it loaded, so an entity stating a
95
+ * Postgres column type in an app running on `adapter/memory` is indistinguishable
96
+ * from a typo. A project's dependencies name every adapter it could ever load.
97
+ *
98
+ * Silent when the project depends on none — there would be nothing to compare against,
99
+ * and reporting every key as unknown is worse than saying nothing.
100
+ */
101
+ const known = await adaptersOf(root);
102
+ if (known.length > 0) {
103
+ for (const frond of fronds) {
104
+ for (const entity of frond.entities) {
105
+ for (const name of Object.keys(entity.entityClass.getAdapters() ?? {})) {
106
+ if (known.includes(name)) continue;
107
+ findings.push({
108
+ severity: 'warning',
109
+ code: 'unknown-adapter',
110
+ filePath: entity.filePath,
111
+ subject: entity.name,
112
+ message: `states \`adapters: { ${name} }\`, and this project depends on no adapter `
113
+ + `that answers to it. It depends on ${known.join(', ')}. Nothing reads the entry, `
114
+ + 'so it is inert — a typo, or a dependency that was never added.',
115
+ });
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * A `pattern` that backtracks super-linearly. The pattern is fixed at declaration and
123
+ * the string tested against it is not: whoever calls the door chooses it, so the cost
124
+ * of one match is theirs to set. Judged HERE because a regex says the same thing on
125
+ * every run — checking it per row would pay for a verdict that never changes.
126
+ *
127
+ * `safe-regex` reads the star height, which is the common shape and not every one:
128
+ * `(a|a)*b` backtracks too and passes. Reported as a suspicion, never as a refusal.
129
+ */
130
+ for (const frond of fronds) {
131
+ for (const entity of frond.entities) {
132
+ for (const [key, field] of Object.entries(entity.entityClass.getFields())) {
133
+ for (const pattern of Shapes.patterns(field.shape)) {
134
+ if (safeRegex(pattern)) continue;
135
+ findings.push({
136
+ severity: 'warning',
137
+ code: 'super-linear-pattern',
138
+ filePath: entity.filePath,
139
+ subject: `${entity.name}.${key}`,
140
+ message: `states \`pattern: ${JSON.stringify(pattern)}\`, whose nested repetition `
141
+ + 'backtracks super-linearly. A value a caller chooses can hold the event loop '
142
+ + 'for seconds — rewrite the repeated group, or bound the field with `max`.',
143
+ });
144
+ }
145
+ }
146
+ }
147
+ }
148
+
90
149
  /**
91
150
  * A warning, not a refusal: it resolves today and the app runs. What it costs is
92
151
  * paid once, late — the day the frond it reaches into is deployed on its own.
@@ -42,7 +42,7 @@ export default class DevtoolsHandler {
42
42
  try {
43
43
  const page = await createHttpTransport(one.url)(
44
44
  { entity: 'rpc', op: 'calls' },
45
- { params: {}, query: {}, body: { since: since[one.url] ?? 0 }, state: {} },
45
+ { params: {}, query: {}, input: { since: since[one.url] ?? 0 }, state: {} },
46
46
  ) as CallPage;
47
47
 
48
48
  return { ...one, cursor: page.cursor, inFlight: page.inFlight, dropped: page.dropped, calls: page.calls };
@@ -14,7 +14,7 @@ export type ExplainedBinding =
14
14
  | { kind: 'collector'; typeName: string }
15
15
  | { kind: 'fact'; factName: string }
16
16
  | { kind: 'param'; name: string; coerce?: 'number' | 'boolean' }
17
- | { kind: 'body' }
17
+ | { kind: 'input' }
18
18
  | { kind: 'context' }
19
19
  | { kind: 'query' };
20
20
 
@@ -229,7 +229,7 @@ function addressOf(value: string): string {
229
229
  }
230
230
 
231
231
  function inputTypeOf(operation: EffectiveOperation): string | null {
232
- const body = operation.parameters.find((parameter) => parameter.binding.source.kind === 'body');
232
+ const body = operation.parameters.find((parameter) => parameter.binding.source.kind === 'input');
233
233
  return body?.type ?? schemaName(operation.input) ?? null;
234
234
  }
235
235
 
@@ -39,7 +39,9 @@ export default class MigrateHandler {
39
39
  if (steps.length === 0) return { chain: [], changes: [], refusals: [], ran: [] };
40
40
 
41
41
  const config = await loadConfig(scan.root);
42
- const storage = resolveStorage(config.db ?? {});
42
+ // `fougere migrate --root ../app` read that project's config and planned against a
43
+ // database beside the CLI's own directory — the loudest form of this bug.
44
+ const storage = resolveStorage(config.db ?? {}, undefined, scan.root);
43
45
  if (!storage.db) {
44
46
  return {
45
47
  chain: versionsOf(steps),
@@ -1,6 +1,8 @@
1
1
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { upperFirst, EntityTypeSource, FacadeTypeSource, type SchemaDescriptor } from '@fougere/schema';
3
+ import { upperFirst, type SchemaDescriptor } from '@fougere/schema';
4
+ import { EntityTypes } from '../../../src/typescript/EntityTypes.js';
5
+ import { FacadeTypes } from '../../../src/typescript/FacadeTypes.js';
4
6
  // The card's shape is declared once, in core, and imported here. A private copy of it
5
7
  // lived in this file and went stale the day an op stopped being a bare name: nothing
6
8
  // compared the copy to the original, so the drift cost nothing until someone read it.
@@ -35,7 +37,7 @@ export function entityClassName(name: string): string {
35
37
  }
36
38
 
37
39
  /**
38
- * One entry — a door or a fact — judged the same way, because sync consumes the same two
40
+ * One entry — a door or a fact — validated the same way, because sync consumes the same two
39
41
  * values from both: a name it can turn into a class, and a descriptor it can rebuild.
40
42
  *
41
43
  * A missing descriptor is legal on either side and means different things: a door that
@@ -52,7 +54,7 @@ function assertEntry(kind: string, frondName: string, entry: { name: string; sch
52
54
  // `ops` is not checked because it is not used: sync writes entities, and the name
53
55
  // and the descriptor below are the only two values that reach a file. The clause
54
56
  // that stood here demanded strings — the shape ops had before they carried their
55
- // kind and their views — and so refused every real host. Judge what you consume.
57
+ // kind and their views — and so refused every real host. Validate what you consume.
56
58
  const descriptor = entry.schema as unknown;
57
59
  if (
58
60
  !descriptor
@@ -70,7 +72,7 @@ function assertEntry(kind: string, frondName: string, entry: { name: string; sch
70
72
  }
71
73
 
72
74
  function identityCardOf(value: unknown): IdentityCard {
73
- // The card's own shape is judged by the package that declares it — `fronds`, and each
75
+ // The card's own shape is validated by the package that declares it — `fronds`, and each
74
76
  // frond's `doors`. What stays here is what only a writer of files needs: a name safe to
75
77
  // become one, and the descriptor a class is generated from.
76
78
  const card = assertIdentityCard(value, 'Remote rpc.discover');
@@ -170,7 +172,7 @@ export default class SyncHandler {
170
172
  `import { Card } from '@fougere/schema';`,
171
173
  ``,
172
174
  `// Generated by \`fougere sync\` from ${baseUrl} — do not edit.`,
173
- EntityTypeSource.of(descriptor).render({ name: className }),
175
+ EntityTypes.of(descriptor).render({ name: className }),
174
176
  ``,
175
177
  `export default ${className};`,
176
178
  ``,
@@ -203,7 +205,7 @@ export default class SyncHandler {
203
205
  `// contract, and a contract that drags a runtime dependency is not one.`,
204
206
  `type Invocation = { params?: Record<string, string>; query?: Record<string, unknown>; body?: unknown; state?: Record<string, unknown> };`,
205
207
  ``,
206
- FacadeTypeSource.of(ops ?? []).render({
208
+ FacadeTypes.of(ops ?? []).render({
207
209
  name: `${className}Handler`,
208
210
  ...(descriptor !== undefined ? { rowType: className } : {}),
209
211
  }),
@@ -263,7 +265,7 @@ export default class SyncHandler {
263
265
  * The barrel is rewritten every run, so a dropped entity loses its export on its own
264
266
  * — but the FILE stayed, and the generated `package.json` exports `'./entities/*'` as
265
267
  * a wildcard, so `@fronds/blog/entities/Ticket.js` kept resolving to a class nothing
266
- * behind it answers for. The consumer compiles, its local judge accepts, and the call
268
+ * behind it answers for. The consumer compiles, its local validator accepts, and the call
267
269
  * comes back NOT_FOUND at the door — or never leaves, because the page dropped the
268
270
  * call and kept the type.
269
271
  */
@@ -30,6 +30,11 @@ function monorepoPackages(): string | undefined {
30
30
  */
31
31
  const TEMPLATES = fileURLToPath(new URL('../../../templates/', import.meta.url));
32
32
 
33
+ /** The version that scaffolds is the version the templates were written for. */
34
+ const scaffoldVersion = (): string =>
35
+ (JSON.parse(readFileSync(fileURLToPath(new URL('../../../package.json', import.meta.url)), 'utf8')) as
36
+ { version: string }).version;
37
+
33
38
  // npm strips a literal .gitignore from published packages — it ships as
34
39
  // _gitignore and the name is restored on copy.
35
40
  function restoreGitignore(dir: string): void {
@@ -146,6 +151,44 @@ export default class ProjectWriter {
146
151
  }
147
152
  }
148
153
 
154
+ /**
155
+ * Every `@fougere/*` dependency, pinned to the version that scaffolded it.
156
+ *
157
+ * The templates say `latest`, which reads as "whatever is current" and is not: pnpm
158
+ * answers from a metadata cache, and a fresh project installed 0.6 while the registry
159
+ * said 0.7 — measured. A version is also what makes an install reproducible, which
160
+ * `latest` never was.
161
+ *
162
+ * Read off this CLI's own package, because the scaffold and the packages it names ship
163
+ * together: the version that wrote the file is the one it was written for.
164
+ */
165
+ pinVersions(wsDir: string, conventions: Conventions = DEFAULT_CONVENTIONS): void {
166
+ const version = scaffoldVersion();
167
+ const manifests = [
168
+ join(wsDir, 'package.json'),
169
+ ...['apps', conventions.fronds].flatMap((kind) => {
170
+ const dir = join(wsDir, kind);
171
+ if (!existsSync(dir)) return [];
172
+
173
+ return readdirSync(dir, { withFileTypes: true })
174
+ .filter((e) => e.isDirectory())
175
+ .map((e) => join(dir, e.name, 'package.json'));
176
+ }),
177
+ ];
178
+
179
+ for (const path of manifests) {
180
+ if (!existsSync(path)) continue;
181
+ const pkg = JSON.parse(readFileSync(path, 'utf8')) as { dependencies?: Record<string, string> };
182
+ let moved = false;
183
+ for (const [name, range] of Object.entries(pkg.dependencies ?? {})) {
184
+ if (!name.startsWith('@fougere/') || range !== 'latest') continue;
185
+ pkg.dependencies![name] = version;
186
+ moved = true;
187
+ }
188
+ if (moved) writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
189
+ }
190
+ }
191
+
149
192
  /**
150
193
  * Dev mode: rewrite every `@fougere/*` dependency in the workspace to a
151
194
  * `link:` into this monorepo, so `pnpm install` resolves offline (the
@@ -0,0 +1,9 @@
1
+ // `safe-regex` ships no types, and `@types/safe-regex` describes its version 1 while the
2
+ // package here is version 2. The exported signature did not move between the two, so the
3
+ // shim states it rather than pinning a types package a major behind.
4
+ declare module 'safe-regex' {
5
+ export default function safeRegex(
6
+ pattern: string | RegExp,
7
+ options?: { limit?: number },
8
+ ): boolean;
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/cli",
3
- "version": "0.6.0-alpha.0",
3
+ "version": "0.8.0-alpha.0",
4
4
  "description": "The Fougere CLI — compose a workspace, serve a frond, call an operation.",
5
5
  "keywords": [
6
6
  "fougere",
@@ -41,12 +41,13 @@
41
41
  "consola": "^3.4.2",
42
42
  "jiti": "^2.4.2",
43
43
  "picocolors": "^1.1.1",
44
- "@fougere/adapter-sql": "0.6.0-alpha.0",
45
- "@fougere/container": "0.6.0-alpha.0",
46
- "@fougere/defaults": "0.6.0-alpha.0",
47
- "@fougere/core": "0.6.0-alpha.0",
48
- "@fougere/transport-http": "0.6.0-alpha.0",
49
- "@fougere/schema": "0.6.0-alpha.0"
44
+ "safe-regex": "^2.1.1",
45
+ "@fougere/container": "0.8.0-alpha.0",
46
+ "@fougere/core": "0.8.0-alpha.0",
47
+ "@fougere/adapter-sql": "0.8.0-alpha.0",
48
+ "@fougere/schema": "0.8.0-alpha.0",
49
+ "@fougere/transport-http": "0.8.0-alpha.0",
50
+ "@fougere/defaults": "0.8.0-alpha.0"
50
51
  },
51
52
  "devDependencies": {
52
53
  "vitest": "^4.1.0"
package/src/bin.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * fougere CLI — a Fougere app powered by citty.
4
- *
5
- * src/ → compiled (tsc → dist/)
6
- * fronds/ → loaded at runtime by jiti (domain)
7
- * app/ → loaded at runtime by jiti (presentation)
8
- */
2
+ /** fougere CLI — a Fougere app powered by citty. */
9
3
  import { createApp, setLogLevel, envLevel, type ScanResult } from '@fougere/core';
10
4
  import { scanProject, getModuleLoader, frondDirsOf, DEFAULT_CONVENTIONS } from '@fougere/core/node';
11
5
  import { readdir, stat } from 'node:fs/promises';
@@ -50,13 +44,8 @@ async function newestDeclaration(root: string): Promise<number> {
50
44
  }
51
45
 
52
46
  /**
53
- * The CLI is a Fougere app, so it reads its own written-down scan like any deployment —
54
- * producing the description reads the project, consuming it does not.
55
- *
56
- * Reading its own 44 declarations through the compiler cost 617 ms on every invocation,
57
- * for a domain fixed at publish time. Staleness is decided by mtime rather than by a flag:
58
- * editing a frond must not need a command, and a published package has nothing newer than
59
- * its artefact.
47
+ * The CLI is a Fougere app, so it reads its own written-down scan like any deployment — producing
48
+ * the description reads the project, consuming it does not.
60
49
  */
61
50
  async function scanOf(root: string): Promise<ScanResult> {
62
51
  const written = join(root, '.fougere/scan.generated.ts');
package/src/bridge.ts CHANGED
@@ -1,12 +1,7 @@
1
1
  import { Lifecycle, Role } from '@fougere/schema';
2
- /**
3
- * Entity → citty bridge.
4
- *
5
- * Converts Entity fields into citty ArgsDef.
6
- * The Entity IS the CLI definition — no duplicate schema.
7
- */
2
+ /** Entity → citty bridge. */
8
3
  import type { Fields } from '@fougere/schema';
9
- import { Anatomy, Visibility } from '@fougere/schema';
4
+ import { Shapes, Visibility } from '@fougere/schema';
10
5
  import type { ArgsDef, ArgDef } from 'citty';
11
6
 
12
7
  function toKebab(name: string): string {
@@ -25,46 +20,62 @@ export function entityToArgs(fields: Fields): ArgsDef {
25
20
 
26
21
  // A `default(v)` travels as the create rule `{ value }` — citty shows it.
27
22
  const defaultValue = Lifecycle.of(field).literal?.value;
28
- const { base: shape, nullable } = Anatomy.of(field.shape);
29
-
30
- const kebab = toKebab(key);
31
- const def: ArgDef = {
23
+ const { base: shape, nullable } = Shapes.of(field.shape);
24
+ const common = {
32
25
  description: field.meta?.description,
33
26
  required: !nullable && Lifecycle.of(field).requiredAtCreate,
34
27
  };
35
28
 
36
- switch (shape?.type) {
37
- case 'boolean':
38
- (def as Record<string, unknown>).type = 'boolean';
39
- if (defaultValue !== undefined) def.default = defaultValue as boolean;
40
- break;
41
- case 'number':
42
- case 'integer':
43
- case 'string':
44
- // A closed set is citty's `enum`: the shape already names the legal values, so the
45
- // refusal and the `--help` listing come from the declaration rather than a check
46
- // written beside it.
47
- if (shape.type === 'string' && shape.enum?.length) {
48
- (def as Record<string, unknown>).type = 'enum';
49
- (def as Record<string, unknown>).options = shape.enum.filter((v) => v !== null);
50
- // A date-time string stays a named string — never a positional arg.
51
- } else if (shape.type === 'string' && shape.format === 'date-time') {
52
- (def as Record<string, unknown>).type = 'string';
53
- } else if (positionalIndex === 0 && def.required && key !== 'force') {
54
- // First non-bool required field becomes positional
55
- (def as Record<string, unknown>).type = 'positional';
56
- positionalIndex++;
57
- } else {
58
- (def as Record<string, unknown>).type = 'string';
59
- }
60
- if (defaultValue !== undefined) def.default = String(defaultValue);
61
- break;
62
- default:
63
- (def as Record<string, unknown>).type = 'string';
64
- }
29
+ // A closed set is citty's `enum`: the shape already names the legal values, so the
30
+ // refusal and the `--help` listing come from the declaration rather than a check
31
+ // written beside it.
32
+ const options = shape?.type === 'string' && shape.enum?.length
33
+ ? shape.enum.filter((value): value is string => value !== null)
34
+ : undefined;
35
+ // First required field becomes positional — but never a boolean, a closed set, or a
36
+ // date-time, which stay named flags.
37
+ const positional = positionalIndex === 0
38
+ && common.required
39
+ && key !== 'force'
40
+ && options === undefined
41
+ && shape?.type !== 'boolean'
42
+ && !(shape?.type === 'string' && shape.format === 'date-time');
43
+ if (positional) positionalIndex++;
65
44
 
66
- args[kebab === key ? key : kebab] = def;
45
+ args[toKebab(key) === key ? key : toKebab(key)] = argFor(
46
+ shape?.type,
47
+ common,
48
+ defaultValue,
49
+ options,
50
+ positional,
51
+ );
67
52
  }
68
53
 
69
54
  return args;
70
55
  }
56
+
57
+ /** One arg, built WITH its `type`. */
58
+ function argFor(
59
+ type: string | undefined,
60
+ common: { description?: string; required: boolean },
61
+ defaultValue: unknown,
62
+ options: string[] | undefined,
63
+ positional: boolean,
64
+ ): ArgDef {
65
+ if (type === 'boolean') {
66
+ return defaultValue === undefined
67
+ ? { ...common, type: 'boolean' }
68
+ : { ...common, type: 'boolean', default: defaultValue as boolean };
69
+ }
70
+
71
+ // Only these three shapes carry a default onto the CLI; anything else is named and bare.
72
+ const carriesDefault = type === 'number' || type === 'integer' || type === 'string';
73
+ const withDefault = carriesDefault && defaultValue !== undefined
74
+ ? { default: String(defaultValue) }
75
+ : {};
76
+
77
+ if (options) return { ...common, ...withDefault, type: 'enum', options };
78
+ if (positional) return { ...common, ...withDefault, type: 'positional' };
79
+
80
+ return { ...common, ...withDefault, type: 'string' };
81
+ }
package/src/completion.ts CHANGED
@@ -1,11 +1,4 @@
1
- /**
2
- * Shell completion generator — outputs a script for bash/zsh.
3
- *
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.
8
- */
1
+ /** Shell completion generator — outputs a script for bash/zsh. */
9
2
  import type { App } from '@fougere/core';
10
3
  import { entityToArgs } from './bridge.js';
11
4
 
package/src/loader.ts CHANGED
@@ -3,14 +3,7 @@ import {
3
3
  } from '@fougere/core/node';
4
4
  import type { Conventions } from '@fougere/core/node';
5
5
 
6
- /**
7
- * The loader every command needs: `alias` is what makes `@fronds/user/entities/User.js`
8
- * resolve when one frond names its neighbour, in any command that loads user code.
9
- *
10
- * Two jitis, because the config is read BEFORE the aliases — it names the scope they are
11
- * built from. `reread` drops the module cache: every loader caches by specifier, so a
12
- * second boot in one process would be handed what the first one read.
13
- */
6
+ /** The loader every command needs. */
14
7
  export async function installLoader(root: string, reread = false): Promise<Conventions> {
15
8
  const { createJiti } = await import('jiti');
16
9
  const bare = createJiti(import.meta.url, { interopDefault: true });
package/src/machine.ts CHANGED
@@ -1,18 +1,12 @@
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
- */
1
+ /** A command's machine output — the one shape a pipe reads. */
8
2
  export function machineWanted(raw: Record<string, unknown>): boolean {
9
3
  return raw.json === true || typeof raw.names === 'string';
10
4
  }
11
5
 
12
6
  /**
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.
7
+ * A `Map` serializes to `{}`, so the door converts it rather than each command flattening its own
8
+ * result: `GraphResult.nodes` is a Map, and `graph --json` would have printed a report with an
9
+ * empty graph in it.
16
10
  */
17
11
  export function machineText(value: unknown): string {
18
12
  return JSON.stringify(value, (_key, held) => (held instanceof Map ? Object.fromEntries(held) : held), 2);