@fougere/cli 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/app/commands/CallCommand.ts +68 -0
- package/app/commands/CheckCommand.ts +61 -0
- package/app/commands/CompletionCommand.ts +23 -0
- package/app/commands/GraphCommand.ts +61 -0
- package/app/commands/NewCommand.ts +122 -0
- package/app/commands/ServeCommand.ts +34 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +28 -0
- package/dist/bin.js.map +1 -0
- package/dist/bridge.d.ts +11 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +54 -0
- package/dist/bridge.js.map +1 -0
- package/dist/completion.d.ts +9 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +93 -0
- package/dist/completion.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/runner.d.ts +12 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +113 -0
- package/dist/runner.js.map +1 -0
- package/fronds/analysis/entities/Check.ts +5 -0
- package/fronds/analysis/entities/Graph.ts +6 -0
- package/fronds/analysis/handlers/CheckHandler.ts +90 -0
- package/fronds/analysis/handlers/GraphHandler.ts +32 -0
- package/fronds/analysis/services/ProjectScan.ts +35 -0
- package/fronds/scaffold/entities/BuildFrond.ts +5 -0
- package/fronds/scaffold/entities/Call.ts +6 -0
- package/fronds/scaffold/entities/Completion.ts +5 -0
- package/fronds/scaffold/entities/New.ts +19 -0
- package/fronds/scaffold/entities/Serve.ts +7 -0
- package/fronds/scaffold/entities/Sync.ts +6 -0
- package/fronds/scaffold/handlers/BuildFrondHandler.ts +97 -0
- package/fronds/scaffold/handlers/CallHandler.ts +9 -0
- package/fronds/scaffold/handlers/CompletionHandler.ts +8 -0
- package/fronds/scaffold/handlers/NewHandler.ts +8 -0
- package/fronds/scaffold/handlers/ServeHandler.ts +9 -0
- package/fronds/scaffold/handlers/SyncHandler.ts +329 -0
- package/fronds/scaffold/services/ProjectWriter.ts +176 -0
- package/package.json +52 -0
- package/templates/admin/fronds/admin/entities/User.ts +10 -0
- package/templates/admin/fronds/admin/handlers/UserHandler.ts +29 -0
- package/templates/admin/fronds/admin/package.json +10 -0
- package/templates/api/fronds/api/entities/Task.ts +9 -0
- package/templates/api/fronds/api/handlers/TaskHandler.ts +29 -0
- package/templates/api/fronds/api/package.json +10 -0
- package/templates/apps/nuxt/app/app.vue +25 -0
- package/templates/apps/nuxt/app/pages/index.vue +33 -0
- package/templates/apps/nuxt/nuxt.config.ts +6 -0
- package/templates/apps/nuxt/package.json +19 -0
- package/templates/apps/nuxt/tsconfig.json +3 -0
- package/templates/blog/app/pages/index.vue +12 -0
- package/templates/blog/app/pages/posts/index.vue +21 -0
- package/templates/blog/app/pages/posts/manage.vue +47 -0
- package/templates/blog/app/pages/posts/new.vue +34 -0
- package/templates/blog/fronds/blog/entities/Post.ts +12 -0
- package/templates/blog/fronds/blog/handlers/PostHandler.ts +35 -0
- package/templates/blog/fronds/blog/package.json +10 -0
- package/templates/flat/CLAUDE.md +65 -0
- package/templates/flat/_gitignore +6 -0
- package/templates/flat/app/app.vue +25 -0
- package/templates/flat/app/pages/index.vue +7 -0
- package/templates/flat/fougere.config.ts +7 -0
- package/templates/flat/nuxt.config.ts +4 -0
- package/templates/flat/package.json +24 -0
- package/templates/flat/pnpm-workspace.yaml +6 -0
- package/templates/flat/tsconfig.frond.json +25 -0
- package/templates/flat/tsconfig.json +3 -0
- package/templates/frond/CLAUDE.md +65 -0
- package/templates/frond/_gitignore +4 -0
- package/templates/frond/fougere.config.ts +5 -0
- package/templates/frond/fronds/__name__/entities/Post.ts +10 -0
- package/templates/frond/fronds/__name__/handlers/PostHandler.ts +26 -0
- package/templates/frond/fronds/__name__/package.json +10 -0
- package/templates/frond/package.json +18 -0
- package/templates/frond/pnpm-workspace.yaml +2 -0
- package/templates/frond/serve.mjs +25 -0
- package/templates/fronds/blank/entities/Item.ts +8 -0
- package/templates/fronds/blank/handlers/ItemHandler.ts +5 -0
- package/templates/fronds/blank/package.json +14 -0
- package/templates/fronds/blog/entities/Post.ts +10 -0
- package/templates/fronds/blog/handlers/PostHandler.ts +38 -0
- package/templates/fronds/blog/package.json +14 -0
- package/templates/workspace/CLAUDE.md +65 -0
- package/templates/workspace/_gitignore +6 -0
- package/templates/workspace/fougere.config.ts +5 -0
- package/templates/workspace/fronds/tsconfig.json +18 -0
- package/templates/workspace/package.json +19 -0
- package/templates/workspace/pnpm-workspace.yaml +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fougere contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @fougere/cli
|
|
2
|
+
> The Fougere CLI
|
|
3
|
+
Compose a workspace (`new`), serve a frond on its own (`serve`), call an operation
|
|
4
|
+
(`call`), read the application graph (`graph`).
|
|
5
|
+
|
|
6
|
+
The CLI is itself a Fougere app: its commands ride on the call contract.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @fougere/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Part of [Fougere](https://github.com/chok/fougere) — one schema, a gradient from
|
|
16
|
+
monolith to distributed, the same user code.
|
|
17
|
+
Reference documentation: [the site](https://chok.github.io/fougere/) (en/fr).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createAppRunner } from '@fougere/core';
|
|
2
|
+
import { toRegistrationName } from '@fougere/core/contract';
|
|
3
|
+
import { bootAppFromConfig } from '@fougere/runtime';
|
|
4
|
+
import type { App } from '@fougere/core';
|
|
5
|
+
import type { ui as createUi } from '@fougere/cli-ui';
|
|
6
|
+
|
|
7
|
+
type Ui = ReturnType<typeof createUi>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse `--field value` / `--field=value` / `--flag` from the raw argv.
|
|
11
|
+
* `call`'s payload is free-form (any entity's fields), so it can't be declared
|
|
12
|
+
* on the Call entity — citty would boolean-ify undeclared flags. We read the
|
|
13
|
+
* tail ourselves; positionals (the target) are ignored (not `--`-prefixed).
|
|
14
|
+
*/
|
|
15
|
+
function parseFlags(tokens: string[]): Record<string, unknown> {
|
|
16
|
+
const out: Record<string, unknown> = {};
|
|
17
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
18
|
+
const t = tokens[i];
|
|
19
|
+
if (!t.startsWith('--')) continue;
|
|
20
|
+
const eq = t.indexOf('=');
|
|
21
|
+
if (eq !== -1) { out[t.slice(2, eq)] = t.slice(eq + 1); continue; }
|
|
22
|
+
const key = t.slice(2);
|
|
23
|
+
const next = tokens[i + 1];
|
|
24
|
+
if (next === undefined || next.startsWith('--')) out[key] = true;
|
|
25
|
+
else { out[key] = next; i++; }
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The client end of the gradient: drive one operation on the project's app and
|
|
32
|
+
* print the result. It follows the topology — if `remotes:` sends the frond
|
|
33
|
+
* elsewhere, the call travels there. Same envelope every consumer uses.
|
|
34
|
+
*/
|
|
35
|
+
export default class CallCommand {
|
|
36
|
+
constructor(private app: App, private ui: Ui) {}
|
|
37
|
+
|
|
38
|
+
async run(raw: Record<string, unknown>) {
|
|
39
|
+
const target = raw.target as string | undefined;
|
|
40
|
+
if (!target || !target.includes('.')) {
|
|
41
|
+
this.ui.error('Usage: fougere call <entity>.<op> [--field value …]');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const [entityName, op] = target.split('.');
|
|
45
|
+
|
|
46
|
+
// Flags → invocation, by the same rule the framework's binding uses:
|
|
47
|
+
// a primitive param (like `id`) resolves from `params`, an object from
|
|
48
|
+
// `body`. So `--id` is a route param, every other flag is the body.
|
|
49
|
+
const flags = parseFlags(process.argv.slice(2));
|
|
50
|
+
const params: Record<string, string> = {};
|
|
51
|
+
const body: Record<string, unknown> = {};
|
|
52
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
53
|
+
if (k === 'id') params.id = String(v);
|
|
54
|
+
else body[k] = v;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const app = await bootAppFromConfig(process.cwd(), {});
|
|
58
|
+
try {
|
|
59
|
+
const result = await createAppRunner(app)(
|
|
60
|
+
{ entity: toRegistrationName(entityName), op },
|
|
61
|
+
{ params, query: {}, body, state: {} },
|
|
62
|
+
);
|
|
63
|
+
this.ui.note(JSON.stringify(result, null, 2), target);
|
|
64
|
+
} finally {
|
|
65
|
+
await app.dispose();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { CheckResult, Finding } from '../../fronds/analysis/handlers/CheckHandler.js';
|
|
2
|
+
import type { App } from '@fougere/core';
|
|
3
|
+
import { createAppRunner } from '@fougere/core';
|
|
4
|
+
import type { ui as createUi } from '@fougere/cli-ui';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { relative } from 'node:path';
|
|
7
|
+
|
|
8
|
+
type Ui = ReturnType<typeof createUi>;
|
|
9
|
+
|
|
10
|
+
export default class CheckCommand {
|
|
11
|
+
constructor(private app: App, private ui: Ui) {}
|
|
12
|
+
|
|
13
|
+
async run(raw: Record<string, unknown>) {
|
|
14
|
+
// Ride the call contract — the same envelope every consumer uses.
|
|
15
|
+
const result = await createAppRunner(this.app)(
|
|
16
|
+
{ entity: 'check', op: 'execute' },
|
|
17
|
+
{ params: {}, query: {}, body: raw, state: {} },
|
|
18
|
+
) as CheckResult;
|
|
19
|
+
|
|
20
|
+
if (result.fronds === 0) {
|
|
21
|
+
this.ui.warn('No fronds found. Run this from a Fougere project root.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.ui.step(`${pc.bold(String(result.fronds))} frond(s), ${pc.bold(String(result.handlers))} handler(s)`);
|
|
26
|
+
|
|
27
|
+
if (result.findings.length === 0) {
|
|
28
|
+
this.ui.success('Nothing to report.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.ui.note(result.findings.map(render).join('\n\n'), 'Findings');
|
|
33
|
+
|
|
34
|
+
const blocking = result.findings.filter((f) => f.severity === 'blocking').length;
|
|
35
|
+
const warnings = result.findings.length - blocking;
|
|
36
|
+
this.ui.info(`${blocking} blocking, ${warnings} warning(s)`);
|
|
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
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function render(f: Finding): string {
|
|
46
|
+
const mark = f.severity === 'blocking' ? pc.red('✗') : pc.yellow('⚠');
|
|
47
|
+
const where = relative(process.cwd(), f.filePath) || f.filePath;
|
|
48
|
+
return ` ${mark} ${pc.bold(`[${f.code}]`)}\n${wrap(f.message, 76, ' ')}\n ${pc.dim(where)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A box grows to its longest line, so a one-line sentence makes an unreadable box. */
|
|
52
|
+
function wrap(text: string, width: number, indent: string): string {
|
|
53
|
+
const lines: string[] = [];
|
|
54
|
+
let line = '';
|
|
55
|
+
for (const word of text.split(' ')) {
|
|
56
|
+
if (line && line.length + 1 + word.length > width) { lines.push(line); line = word; }
|
|
57
|
+
else line = line ? `${line} ${word}` : word;
|
|
58
|
+
}
|
|
59
|
+
if (line) lines.push(line);
|
|
60
|
+
return lines.map((l) => indent + l).join('\n');
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { App } from '@fougere/core';
|
|
2
|
+
// `dist/`, not `src/` — app/ ships raw and is loaded by jiti at runtime, but the
|
|
3
|
+
// published package carries only dist/, app/, fronds/ and templates/.
|
|
4
|
+
import { generateZshCompletion, generateBashCompletion } from '../../dist/completion.js';
|
|
5
|
+
|
|
6
|
+
export default class CompletionCommand {
|
|
7
|
+
constructor(private app: App) {}
|
|
8
|
+
|
|
9
|
+
async run(raw: Record<string, unknown>) {
|
|
10
|
+
const shell = (raw.shell as string) || detectShell();
|
|
11
|
+
const script = shell === 'bash'
|
|
12
|
+
? generateBashCompletion(this.app)
|
|
13
|
+
: generateZshCompletion(this.app);
|
|
14
|
+
console.log(script);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function detectShell(): string {
|
|
19
|
+
const shell = process.env.SHELL ?? '';
|
|
20
|
+
if (shell.includes('zsh')) return 'zsh';
|
|
21
|
+
if (shell.includes('bash')) return 'bash';
|
|
22
|
+
return 'zsh';
|
|
23
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { GraphResult } from '../../fronds/analysis/handlers/GraphHandler.js';
|
|
2
|
+
import type { EntityNode, DomainCluster, App } from '@fougere/core';
|
|
3
|
+
import { createAppRunner } from '@fougere/core';
|
|
4
|
+
import type { ui as createUi } from '@fougere/cli-ui';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
|
|
7
|
+
type Ui = ReturnType<typeof createUi>;
|
|
8
|
+
|
|
9
|
+
export default class GraphCommand {
|
|
10
|
+
constructor(private app: App, private ui: Ui) {}
|
|
11
|
+
|
|
12
|
+
async run(raw: Record<string, unknown>) {
|
|
13
|
+
// Ride the call contract — the same envelope every consumer uses.
|
|
14
|
+
const result = await createAppRunner(this.app)(
|
|
15
|
+
{ entity: 'graph', op: 'execute' },
|
|
16
|
+
{ params: {}, query: {}, body: raw, state: {} },
|
|
17
|
+
) as GraphResult;
|
|
18
|
+
|
|
19
|
+
if (result.fronds.length === 0) {
|
|
20
|
+
this.ui.warn('No fronds found. Run this from a Fougere project root.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.ui.step(`${pc.bold(String(result.totalEntities))} entities, ${pc.bold(String(result.totalRefs))} refs, ${pc.bold(String(result.fronds.length))} frond(s)`);
|
|
25
|
+
this.ui.note(renderGraph(result.nodes), 'Entity Graph');
|
|
26
|
+
|
|
27
|
+
const min = (raw.minEntities as number) ?? 6;
|
|
28
|
+
if (result.clusters.length > 1 && result.totalEntities >= min) {
|
|
29
|
+
this.ui.note(renderClusters(result.clusters), 'Suggested Domains');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (result.fronds.length === 1 && result.clusters.length > 1 && result.totalEntities >= min) {
|
|
33
|
+
this.ui.info(`${pc.dim('Tip:')} ${result.clusters.length} natural domains detected. Consider splitting into separate fronds.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderGraph(nodes: Map<string, EntityNode>): string {
|
|
40
|
+
return [...nodes.entries()].map(([name, node]) => {
|
|
41
|
+
const refs = node.refs.length > 0
|
|
42
|
+
? ` ${pc.dim('→')} ${node.refs.map((r) => pc.cyan(r)).join(', ')}`
|
|
43
|
+
: '';
|
|
44
|
+
const badge = node.referencedBy.length > 0
|
|
45
|
+
? pc.dim(` (${node.referencedBy.length} incoming)`)
|
|
46
|
+
: '';
|
|
47
|
+
return ` ${pc.bold(name)}${refs}${badge}`;
|
|
48
|
+
}).join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderClusters(clusters: DomainCluster[]): string {
|
|
52
|
+
return clusters.map((cluster) => {
|
|
53
|
+
const header = ` ${pc.green(pc.bold(cluster.name + '/'))} ${pc.dim(`(${cluster.entities.length} entities)`)}`;
|
|
54
|
+
const entities = cluster.entities.map((e) => ` ${e}`);
|
|
55
|
+
const crossRefs = cluster.crossRefs.length > 0
|
|
56
|
+
? [` ${pc.dim('cross-refs:')}`, ...cluster.crossRefs.map((r) =>
|
|
57
|
+
` ${r.from} ${pc.dim('→')} ${pc.yellow(r.to)} ${pc.dim(`(${r.targetCluster})`)}`)]
|
|
58
|
+
: [];
|
|
59
|
+
return [header, ...entities, ...crossRefs, ''].join('\n');
|
|
60
|
+
}).join('\n');
|
|
61
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import ProjectWriter from '../../fronds/scaffold/services/ProjectWriter.js';
|
|
4
|
+
import type { ui as createUi } from '@fougere/cli-ui';
|
|
5
|
+
import type { App } from '@fougere/core';
|
|
6
|
+
|
|
7
|
+
type Ui = ReturnType<typeof createUi>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The guided composer: a workspace, then its fronds (domains), then the apps
|
|
11
|
+
* that consume them — in that dependency order. ProjectWriter is a plain,
|
|
12
|
+
* dependency-free service, so the presentation command drives it directly.
|
|
13
|
+
*/
|
|
14
|
+
export default class NewCommand {
|
|
15
|
+
constructor(private app: App, private ui: Ui) {}
|
|
16
|
+
|
|
17
|
+
async run(raw: Record<string, unknown>) {
|
|
18
|
+
const pw = new ProjectWriter();
|
|
19
|
+
|
|
20
|
+
let name = raw.name as string | undefined;
|
|
21
|
+
if (!name) name = (await this.ui.text({ message: 'Workspace', placeholder: 'shop' })) as string;
|
|
22
|
+
|
|
23
|
+
const dir = join(process.cwd(), name);
|
|
24
|
+
if (existsSync(dir) && !raw.force) {
|
|
25
|
+
const ok = await this.ui.confirm({ message: `${name}/ existe déjà. Écraser ?` });
|
|
26
|
+
if (!ok) { this.ui.cancel(); return; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A shape, decided before anything is written — the flat form has no workspace shell
|
|
30
|
+
// to put the domain beside, because the root IS the domain.
|
|
31
|
+
if (raw.flat) {
|
|
32
|
+
if (raw.app) throw new Error('--flat is one app and it is the root — drop --app.');
|
|
33
|
+
const template = ((raw.frond as string) || 'blank').split(':')[0].trim();
|
|
34
|
+
const available = pw.listTemplates('fronds');
|
|
35
|
+
if (!available.includes(template)) {
|
|
36
|
+
throw new Error(`Unknown fronds template '${template}' — available: ${available.join(', ') || '(none)'}`);
|
|
37
|
+
}
|
|
38
|
+
pw.createFlat(dir, name);
|
|
39
|
+
pw.addRootFrond(dir, template);
|
|
40
|
+
if (raw.local) pw.linkLocal(dir);
|
|
41
|
+
this.ui.info(`${template} at the root`);
|
|
42
|
+
const flatInstall = raw.local ? 'pnpm install' : 'pnpm install # needs @fougere/* published (or re-run with --local)';
|
|
43
|
+
this.ui.note([`cd ${name}`, flatInstall, `pnpm dev`].join('\n'), `${name} — one frond, at the root`);
|
|
44
|
+
this.ui.outro('Prêt.');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pw.createWorkspace(dir, name);
|
|
49
|
+
|
|
50
|
+
if (raw.bare) {
|
|
51
|
+
if (raw.local) pw.linkLocal(dir);
|
|
52
|
+
this.ui.note([`cd ${name}`, `fougere new # compose it (guided)`].join('\n'), `${name} — empty workspace`);
|
|
53
|
+
this.ui.outro('Prêt.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Stated composition wins over the prompts: it is the only form a script, a CI
|
|
58
|
+
// job or an agent can drive — the guided flow needs a TTY and hangs without one.
|
|
59
|
+
const stated = (raw.frond as string) || (raw.app as string);
|
|
60
|
+
const fronds = stated
|
|
61
|
+
? this.state(dir, pw, 'fronds', raw.frond as string)
|
|
62
|
+
: await this.compose(dir, pw, 'fronds', 'Fronds — tes domaines', pw.listTemplates('fronds'));
|
|
63
|
+
const apps = stated
|
|
64
|
+
? this.state(dir, pw, 'apps', raw.app as string)
|
|
65
|
+
: await this.compose(dir, pw, 'apps', 'Apps — ce qui les consomme', pw.listTemplates('apps'));
|
|
66
|
+
|
|
67
|
+
// Every app depends on every frond — stated here, where both names are known.
|
|
68
|
+
pw.linkFronds(dir);
|
|
69
|
+
if (raw.local) pw.linkLocal(dir);
|
|
70
|
+
const install = raw.local ? 'pnpm install' : 'pnpm install # needs @fougere/* published (or re-run with --local)';
|
|
71
|
+
this.ui.note([`cd ${name}`, install, `pnpm dev`].join('\n'), `${name} — ${fronds} frond(s), ${apps} app(s)`);
|
|
72
|
+
this.ui.outro('Prêt.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* One phase, stated rather than prompted — `blog,api:catalog` is two pieces, the
|
|
77
|
+
* second renamed. The template name is the default name: a piece you don't rename
|
|
78
|
+
* is called what it is.
|
|
79
|
+
*/
|
|
80
|
+
private state(dir: string, pw: ProjectWriter, kind: 'fronds' | 'apps', spec: string): number {
|
|
81
|
+
const available = pw.listTemplates(kind);
|
|
82
|
+
let count = 0;
|
|
83
|
+
for (const piece of spec.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
84
|
+
const [template, itemName = template] = piece.split(':');
|
|
85
|
+
if (!available.includes(template)) {
|
|
86
|
+
throw new Error(`Unknown ${kind} template '${template}' — available: ${available.join(', ') || '(none)'}`);
|
|
87
|
+
}
|
|
88
|
+
if (kind === 'fronds') pw.addFrond(dir, template, itemName);
|
|
89
|
+
else pw.addApp(dir, template, itemName);
|
|
90
|
+
this.ui.info(`${kind}/${itemName}`);
|
|
91
|
+
count++;
|
|
92
|
+
}
|
|
93
|
+
return count;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One phase — loop "template → name" until the user is done. Returns the count added. */
|
|
97
|
+
private async compose(
|
|
98
|
+
dir: string,
|
|
99
|
+
pw: ProjectWriter,
|
|
100
|
+
kind: 'fronds' | 'apps',
|
|
101
|
+
header: string,
|
|
102
|
+
templates: string[],
|
|
103
|
+
): Promise<number> {
|
|
104
|
+
this.ui.step(header);
|
|
105
|
+
let count = 0;
|
|
106
|
+
for (;;) {
|
|
107
|
+
const doneLabel = count === 0 ? (kind === 'fronds' ? 'passer' : 'aucune') : 'terminé';
|
|
108
|
+
const template = (await this.ui.select({
|
|
109
|
+
message: count === 0 ? 'Template' : 'Encore un ?',
|
|
110
|
+
options: [...templates.map((v) => ({ value: v, label: v })), { value: '__done__', label: doneLabel }],
|
|
111
|
+
})) as string;
|
|
112
|
+
if (template === '__done__') break;
|
|
113
|
+
|
|
114
|
+
const itemName = (await this.ui.text({ message: 'Nom' })) as string;
|
|
115
|
+
if (kind === 'fronds') pw.addFrond(dir, template, itemName);
|
|
116
|
+
else pw.addApp(dir, template, itemName);
|
|
117
|
+
this.ui.info(`${kind}/${itemName}`);
|
|
118
|
+
count++;
|
|
119
|
+
}
|
|
120
|
+
return count;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createLocalRunner } from '@fougere/core';
|
|
2
|
+
import { bootAppFromConfig } from '@fougere/runtime';
|
|
3
|
+
import { serve } from '@fougere/transport-http';
|
|
4
|
+
import type { App } from '@fougere/core';
|
|
5
|
+
import type { ui as createUi } from '@fougere/cli-ui';
|
|
6
|
+
|
|
7
|
+
type Ui = ReturnType<typeof createUi>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The host end of the gradient: one frond, alone in this process, reachable
|
|
11
|
+
* over HTTP. `topology: false` — a served frond IS the host, it never routes
|
|
12
|
+
* back out through `remotes:`. The same fronds/** an app runs in-process;
|
|
13
|
+
* only the runtime moves.
|
|
14
|
+
*/
|
|
15
|
+
export default class ServeCommand {
|
|
16
|
+
constructor(private app: App, private ui: Ui) {}
|
|
17
|
+
|
|
18
|
+
async run(raw: Record<string, unknown>) {
|
|
19
|
+
const frond = raw.frond as string | undefined;
|
|
20
|
+
if (!frond) { this.ui.error('Usage: fougere serve <frond>'); return; }
|
|
21
|
+
|
|
22
|
+
const app = await bootAppFromConfig(process.cwd(), { fronds: [frond], topology: false });
|
|
23
|
+
if (!app.fronds.some((f) => f.name === frond)) {
|
|
24
|
+
this.ui.error(`Frond '${frond}' introuvable dans ce projet.`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const port = raw.port != null ? Number(raw.port) : 4100;
|
|
29
|
+
const { port: bound } = await serve(createLocalRunner(app), { port });
|
|
30
|
+
this.ui.step(`frond ${frond} servie — POST http://127.0.0.1:${bound}/_fougere/call`);
|
|
31
|
+
this.ui.info('Ctrl-C pour arrêter.');
|
|
32
|
+
// The listening server keeps the event loop alive; the command returns and stays up.
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
*/
|
|
9
|
+
import { createApp, setModuleLoader } from '@fougere/core';
|
|
10
|
+
import { createContainer } from '@fougere/container-fougere';
|
|
11
|
+
import { ui } from '@fougere/cli-ui';
|
|
12
|
+
import { run } from './runner.js';
|
|
13
|
+
const { createJiti } = await import('jiti');
|
|
14
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
15
|
+
setModuleLoader((filePath) => jiti.import(filePath));
|
|
16
|
+
const cliRoot = new URL('..', import.meta.url).pathname;
|
|
17
|
+
const container = createContainer();
|
|
18
|
+
container.registerValue('ui', ui());
|
|
19
|
+
container.registerValue('cwd', process.cwd());
|
|
20
|
+
// The CLI is a Fougere app — silence its boot chatter unless explicitly asked.
|
|
21
|
+
process.env.FOUGERE_LOG_LEVEL ??= 'warn';
|
|
22
|
+
const app = await createApp({
|
|
23
|
+
root: cliRoot,
|
|
24
|
+
createContainer: () => container,
|
|
25
|
+
});
|
|
26
|
+
container.registerValue('app', app);
|
|
27
|
+
await run(app);
|
|
28
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AACrC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;AAC5C,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,CAAC;AACnE,eAAe,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAqC,CAAC,CAAC;AAEzF,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;AACxD,MAAM,SAAS,GAAG,eAAe,EAAE,CAAC;AACpC,SAAS,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;AACpC,SAAS,CAAC,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AAE9C,+EAA+E;AAC/E,OAAO,CAAC,GAAG,CAAC,iBAAiB,KAAK,MAAM,CAAC;AAEzC,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC;IAC1B,IAAI,EAAE,OAAO;IACb,eAAe,EAAE,GAAG,EAAE,CAAC,SAAS;CACjC,CAAC,CAAC;AAEH,SAAS,CAAC,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAEpC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC"}
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity → citty bridge.
|
|
3
|
+
*
|
|
4
|
+
* Converts Entity fields into citty ArgsDef.
|
|
5
|
+
* The Entity IS the CLI definition — no duplicate schema.
|
|
6
|
+
*/
|
|
7
|
+
import type { Fields } from '@fougere/schema';
|
|
8
|
+
import type { ArgsDef } from 'citty';
|
|
9
|
+
/** Convert an Entity's fields into citty args definition. */
|
|
10
|
+
export declare function entityToArgs(fields: Fields): ArgsDef;
|
|
11
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,KAAK,EAAE,OAAO,EAAU,MAAM,OAAO,CAAC;AAM7C,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAgDpD"}
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { anatomy, inputFields } from '@fougere/schema';
|
|
2
|
+
function toKebab(name) {
|
|
3
|
+
return name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
4
|
+
}
|
|
5
|
+
/** Convert an Entity's fields into citty args definition. */
|
|
6
|
+
export function entityToArgs(fields) {
|
|
7
|
+
const args = {};
|
|
8
|
+
let positionalIndex = 0;
|
|
9
|
+
// Axes-derived ingress membership; the CLI additionally skips ALL relations
|
|
10
|
+
// (a ref is not a flag — supplying related rows is not a CLI gesture).
|
|
11
|
+
for (const [key, field] of Object.entries(inputFields(fields))) {
|
|
12
|
+
if (field.role?.relation)
|
|
13
|
+
continue;
|
|
14
|
+
// A `default(v)` travels as the create rule `{ value }` — citty shows it.
|
|
15
|
+
const create = field.lifecycle?.create;
|
|
16
|
+
const defaultValue = typeof create === 'object' && 'value' in create ? create.value : undefined;
|
|
17
|
+
const { base: shape, nullable } = anatomy(field.shape);
|
|
18
|
+
const kebab = toKebab(key);
|
|
19
|
+
const def = {
|
|
20
|
+
description: field.meta?.description,
|
|
21
|
+
required: !nullable && field.lifecycle?.create === undefined,
|
|
22
|
+
};
|
|
23
|
+
switch (shape?.type) {
|
|
24
|
+
case 'boolean':
|
|
25
|
+
def.type = 'boolean';
|
|
26
|
+
if (defaultValue !== undefined)
|
|
27
|
+
def.default = defaultValue;
|
|
28
|
+
break;
|
|
29
|
+
case 'number':
|
|
30
|
+
case 'integer':
|
|
31
|
+
case 'string':
|
|
32
|
+
// A date-time string stays a named string — never a positional arg.
|
|
33
|
+
if (shape.type === 'string' && shape.format === 'date-time') {
|
|
34
|
+
def.type = 'string';
|
|
35
|
+
}
|
|
36
|
+
else if (positionalIndex === 0 && def.required && key !== 'force') {
|
|
37
|
+
// First non-bool required field becomes positional
|
|
38
|
+
def.type = 'positional';
|
|
39
|
+
positionalIndex++;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
def.type = 'string';
|
|
43
|
+
}
|
|
44
|
+
if (defaultValue !== undefined)
|
|
45
|
+
def.default = String(defaultValue);
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
def.type = 'string';
|
|
49
|
+
}
|
|
50
|
+
args[kebab === key ? key : kebab] = def;
|
|
51
|
+
}
|
|
52
|
+
return args;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGvD,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;AAC9D,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,IAAI,GAAY,EAAE,CAAC;IACzB,IAAI,eAAe,GAAG,CAAC,CAAC;IAExB,4EAA4E;IAC5E,uEAAuE;IACvE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QAC/D,IAAI,KAAK,CAAC,IAAI,EAAE,QAAQ;YAAE,SAAS;QAEnC,0EAA0E;QAC1E,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC;QACvC,MAAM,YAAY,GAAG,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;QAChG,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAEvD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,GAAG,GAAW;YAClB,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,WAAW;YACpC,QAAQ,EAAE,CAAC,QAAQ,IAAI,KAAK,CAAC,SAAS,EAAE,MAAM,KAAK,SAAS;SAC7D,CAAC;QAEF,QAAQ,KAAK,EAAE,IAAI,EAAE,CAAC;YACpB,KAAK,SAAS;gBACX,GAA+B,CAAC,IAAI,GAAG,SAAS,CAAC;gBAClD,IAAI,YAAY,KAAK,SAAS;oBAAE,GAAG,CAAC,OAAO,GAAG,YAAuB,CAAC;gBACtE,MAAM;YACR,KAAK,QAAQ,CAAC;YACd,KAAK,SAAS,CAAC;YACf,KAAK,QAAQ;gBACX,oEAAoE;gBACpE,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;oBAC3D,GAA+B,CAAC,IAAI,GAAG,QAAQ,CAAC;gBACnD,CAAC;qBAAM,IAAI,eAAe,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;oBACpE,mDAAmD;oBAClD,GAA+B,CAAC,IAAI,GAAG,YAAY,CAAC;oBACrD,eAAe,EAAE,CAAC;gBACpB,CAAC;qBAAM,CAAC;oBACL,GAA+B,CAAC,IAAI,GAAG,QAAQ,CAAC;gBACnD,CAAC;gBACD,IAAI,YAAY,KAAK,SAAS;oBAAE,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;gBACnE,MAAM;YACR;gBACG,GAA+B,CAAC,IAAI,GAAG,QAAQ,CAAC;QACrD,CAAC;QAED,IAAI,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC;IAC1C,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion generator — outputs a script for bash/zsh.
|
|
3
|
+
*
|
|
4
|
+
* Sources: scanned frond handlers (commands) + entity fields (flags).
|
|
5
|
+
*/
|
|
6
|
+
import type { App } from '@fougere/core';
|
|
7
|
+
export declare function generateZshCompletion(app: App, binName?: string): string;
|
|
8
|
+
export declare function generateBashCompletion(app: App, binName?: string): string;
|
|
9
|
+
//# sourceMappingURL=completion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../src/completion.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAsCzC,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,SAAY,GAAG,MAAM,CAmC3E;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,SAAY,GAAG,MAAM,CA8B5E"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { inputFields } from '@fougere/schema';
|
|
2
|
+
function toKebab(name) {
|
|
3
|
+
return name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()).replace(/^-/, '');
|
|
4
|
+
}
|
|
5
|
+
function extractCommandMeta(app) {
|
|
6
|
+
const commands = [];
|
|
7
|
+
for (const frond of app.fronds) {
|
|
8
|
+
const handlerMap = new Map(frond.handlers.map((h) => [h.address, h]));
|
|
9
|
+
for (const entity of frond.entities) {
|
|
10
|
+
const handler = handlerMap.get(entity.name);
|
|
11
|
+
if (!handler)
|
|
12
|
+
continue;
|
|
13
|
+
let facade;
|
|
14
|
+
try {
|
|
15
|
+
facade = app.resolve(handler.name);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (typeof facade.execute !== 'function')
|
|
21
|
+
continue;
|
|
22
|
+
const fields = entity.entityClass.getFields();
|
|
23
|
+
const flags = Object.keys(inputFields(fields))
|
|
24
|
+
.map((key) => `--${toKebab(key)}`);
|
|
25
|
+
commands.push({ name: toKebab(entity.name), flags });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return commands;
|
|
29
|
+
}
|
|
30
|
+
export function generateZshCompletion(app, binName = 'fougere') {
|
|
31
|
+
const commands = extractCommandMeta(app);
|
|
32
|
+
const cmdNames = commands.map((c) => c.name).join(' ');
|
|
33
|
+
const flagCases = commands
|
|
34
|
+
.map((cmd) => {
|
|
35
|
+
const flags = cmd.flags.join(' ');
|
|
36
|
+
return ` ${cmd.name}) flags="${flags}" ;;`;
|
|
37
|
+
})
|
|
38
|
+
.join('\n');
|
|
39
|
+
return `# Auto-generated by @fougere/cli
|
|
40
|
+
|
|
41
|
+
_${binName}() {
|
|
42
|
+
local -a commands flags
|
|
43
|
+
commands=(${commands.map((c) => `'${c.name}'`).join(' ')})
|
|
44
|
+
|
|
45
|
+
if (( CURRENT == 2 )); then
|
|
46
|
+
_describe 'command' commands
|
|
47
|
+
return
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
local cmd=\${words[2]}
|
|
51
|
+
case "$cmd" in
|
|
52
|
+
${flagCases}
|
|
53
|
+
*) flags="" ;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
if [[ "\${words[CURRENT]}" == --* ]]; then
|
|
57
|
+
_values 'flags' \${(s: :)flags}
|
|
58
|
+
fi
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
compdef _${binName} ${binName}
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
export function generateBashCompletion(app, binName = 'fougere') {
|
|
65
|
+
const commands = extractCommandMeta(app);
|
|
66
|
+
const cmdNames = commands.map((c) => c.name).join(' ');
|
|
67
|
+
const flagCases = commands
|
|
68
|
+
.map((cmd) => {
|
|
69
|
+
const flags = cmd.flags.join(' ');
|
|
70
|
+
return ` ${cmd.name}) COMPREPLY=( $(compgen -W "${flags}" -- "\${cur}") ) ;;`;
|
|
71
|
+
})
|
|
72
|
+
.join('\n');
|
|
73
|
+
return `# Auto-generated by @fougere/cli
|
|
74
|
+
_${binName}_completions() {
|
|
75
|
+
local cur prev cmd
|
|
76
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
77
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
78
|
+
cmd="\${COMP_WORDS[1]}"
|
|
79
|
+
|
|
80
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
81
|
+
COMPREPLY=( $(compgen -W "${cmdNames}" -- "\${cur}") )
|
|
82
|
+
return
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
case "$cmd" in
|
|
86
|
+
${flagCases}
|
|
87
|
+
esac
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
complete -F _${binName}_completions ${binName}
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=completion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"completion.js","sourceRoot":"","sources":["../src/completion.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC"}
|