@grafema/cli 0.3.24 → 0.3.27
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 +59 -45
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyzeAction.d.ts.map +1 -1
- package/dist/commands/analyzeAction.js +134 -3
- package/dist/commands/analyzeAction.js.map +1 -1
- package/dist/commands/doctor/checks.d.ts.map +1 -1
- package/dist/commands/doctor/checks.js +7 -3
- package/dist/commands/doctor/checks.js.map +1 -1
- package/dist/commands/export.d.ts +15 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +88 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/exportAction.d.ts +35 -0
- package/dist/commands/exportAction.d.ts.map +1 -0
- package/dist/commands/exportAction.js +58 -0
- package/dist/commands/exportAction.js.map +1 -0
- package/dist/commands/features.d.ts +13 -0
- package/dist/commands/features.d.ts.map +1 -0
- package/dist/commands/features.js +69 -0
- package/dist/commands/features.js.map +1 -0
- package/dist/commands/featuresAction.d.ts +82 -0
- package/dist/commands/featuresAction.d.ts.map +1 -0
- package/dist/commands/featuresAction.js +139 -0
- package/dist/commands/featuresAction.js.map +1 -0
- package/dist/commands/start.d.ts +12 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +294 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +50 -30
- package/dist/commands/trace.js.map +1 -1
- package/dist/commands/upgrade.d.ts +3 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +279 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/package.json +4 -4
- package/src/cli.ts +11 -0
- package/src/commands/analyzeAction.ts +135 -2
- package/src/commands/doctor/checks.ts +4 -3
- package/src/commands/explore.tsx +29 -2
- package/src/commands/export.ts +102 -0
- package/src/commands/exportAction.ts +107 -0
- package/src/commands/features.ts +88 -0
- package/src/commands/featuresAction.ts +218 -0
- package/src/commands/start.ts +303 -0
- package/src/commands/trace.ts +49 -29
- package/src/commands/upgrade.ts +310 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `grafema export` — emit a multi-format spec from the feature graph
|
|
3
|
+
* (REG-1116, REG-1118).
|
|
4
|
+
*
|
|
5
|
+
* Modes shipped:
|
|
6
|
+
* --as openapi-3.1 → http:route features only
|
|
7
|
+
* --as docs-md → all FEATURE categories (single + bulk)
|
|
8
|
+
*
|
|
9
|
+
* Phase 2 formats (mcp-schema, asyncapi, json-schema, ts-declarations,
|
|
10
|
+
* mermaid) plug in by adding renderers to packages/util/src/exporters; the
|
|
11
|
+
* CLI surface here doesn't change.
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import { resolve, join, dirname } from 'path';
|
|
15
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
16
|
+
import { RFDBServerBackend, RFDBClient, RENDERERS } from '@grafema/util';
|
|
17
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
18
|
+
import { runExport } from './exportAction.js';
|
|
19
|
+
const KNOWN_FORMATS = Object.keys(RENDERERS).sort().join(', ');
|
|
20
|
+
export const exportCommand = new Command('export')
|
|
21
|
+
.description('Export FEATURE entries from the graph in a chosen format')
|
|
22
|
+
.requiredOption('--feature <pattern>', "Feature glob (e.g. 'cli:*', 'http:*', \"cli:command:'analyze'\")")
|
|
23
|
+
.requiredOption('--as <format>', `Output format (${KNOWN_FORMATS})`)
|
|
24
|
+
.option('-o, --output <path>', 'Write output to <path> instead of stdout')
|
|
25
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
26
|
+
.addHelpText('after', `
|
|
27
|
+
Examples:
|
|
28
|
+
grafema export --feature 'http:*' --as openapi-3.1 --output api.yaml
|
|
29
|
+
grafema export --feature 'cli:*' --as docs-md
|
|
30
|
+
grafema export --feature "cli:command:'analyze'" --as docs-md
|
|
31
|
+
|
|
32
|
+
Phase 1 supports two formats: openapi-3.1 (http:route only) and
|
|
33
|
+
docs-md (all categories). Other formats — mcp-schema, asyncapi,
|
|
34
|
+
json-schema, ts-declarations, mermaid — are tracked separately.
|
|
35
|
+
`)
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
if (!options.feature || !options.as) {
|
|
38
|
+
exitWithError('Both --feature and --as are required', [
|
|
39
|
+
'See: grafema export --help',
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
const projectPath = resolve(options.project);
|
|
43
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
44
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
45
|
+
if (!existsSync(dbPath)) {
|
|
46
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
47
|
+
}
|
|
48
|
+
// RFDBServerBackend negotiates protocol v3, which returns semantic edge
|
|
49
|
+
// dst values that don't resolve via getNode(). Connect with a plain
|
|
50
|
+
// RFDBClient (protocol v2) so edges preserve raw numeric ids end-to-end.
|
|
51
|
+
// Still use RFDBServerBackend for its auto-start side effect.
|
|
52
|
+
const server = new RFDBServerBackend({ dbPath, clientName: 'cli-bootstrap' });
|
|
53
|
+
await server.connect();
|
|
54
|
+
const socketPath = server.socketPath;
|
|
55
|
+
const rawClient = new RFDBClient(socketPath, 'export');
|
|
56
|
+
await rawClient.connect();
|
|
57
|
+
try {
|
|
58
|
+
await runExport({
|
|
59
|
+
feature: options.feature,
|
|
60
|
+
as: options.as,
|
|
61
|
+
output: options.output,
|
|
62
|
+
}, {
|
|
63
|
+
backend: rawClient,
|
|
64
|
+
writeText: writeOutput,
|
|
65
|
+
warn: (msg) => console.error(msg),
|
|
66
|
+
fail: (msg, code) => {
|
|
67
|
+
console.error(`✗ ${msg}`);
|
|
68
|
+
process.exit(code);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
rawClient.close();
|
|
74
|
+
await server.close();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
/** Default writer: stdout when path == null, otherwise file. */
|
|
78
|
+
function writeOutput(path, text) {
|
|
79
|
+
if (path === null) {
|
|
80
|
+
process.stdout.write(text);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const dir = dirname(path);
|
|
84
|
+
if (dir && !existsSync(dir))
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
writeFileSync(path, text, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export.js","sourceRoot":"","sources":["../../src/commands/export.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `grafema export` action (REG-1116, REG-1118).
|
|
3
|
+
*
|
|
4
|
+
* Pure orchestration logic — connects feature glob → renderer, with all I/O
|
|
5
|
+
* surfaced as inputs (backend) and effects (writeText / log / exit). The
|
|
6
|
+
* Commander shell in `export.ts` plugs in concrete implementations; tests
|
|
7
|
+
* supply mocks.
|
|
8
|
+
*/
|
|
9
|
+
import { type ExportBackendLike, type Renderer } from '@grafema/util';
|
|
10
|
+
export interface ExportActionOptions {
|
|
11
|
+
/** Feature glob — required. */
|
|
12
|
+
feature: string;
|
|
13
|
+
/** Output format — required. Must be a key of RENDERERS. */
|
|
14
|
+
as: string;
|
|
15
|
+
/** Optional output file path. When unset, stdout. */
|
|
16
|
+
output?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ExportActionDeps {
|
|
19
|
+
backend: ExportBackendLike;
|
|
20
|
+
/** Write file/stdout content. Receives `null` for path → stdout. */
|
|
21
|
+
writeText: (path: string | null, text: string) => Promise<void> | void;
|
|
22
|
+
/** Print to stderr (for diagnostic notes that should not contaminate stdout). */
|
|
23
|
+
warn: (msg: string) => void;
|
|
24
|
+
/** Reported when an unrecoverable error happens — caller decides exit code. */
|
|
25
|
+
fail: (msg: string, code: number) => never;
|
|
26
|
+
/** Renderers map — defaults to the package-shipped RENDERERS. */
|
|
27
|
+
renderers?: Record<string, Renderer>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run the export action. Returns the rendered text on success — convenient
|
|
31
|
+
* for tests that don't want to mock writeText. The deps' writeText is also
|
|
32
|
+
* invoked so production callers don't need to handle the return value.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runExport(options: ExportActionOptions, deps: ExportActionDeps): Promise<string>;
|
|
35
|
+
//# sourceMappingURL=exportAction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exportAction.d.ts","sourceRoot":"","sources":["../../src/commands/exportAction.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAKL,KAAK,iBAAiB,EAEtB,KAAK,QAAQ,EACd,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,mBAAmB;IAClC,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,EAAE,EAAE,MAAM,CAAC;IACX,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,oEAAoE;IACpE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,iFAAiF;IACjF,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,+EAA+E;IAC/E,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC;IAC3C,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,MAAM,CAAC,CA2DjB"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `grafema export` action (REG-1116, REG-1118).
|
|
3
|
+
*
|
|
4
|
+
* Pure orchestration logic — connects feature glob → renderer, with all I/O
|
|
5
|
+
* surfaced as inputs (backend) and effects (writeText / log / exit). The
|
|
6
|
+
* Commander shell in `export.ts` plugs in concrete implementations; tests
|
|
7
|
+
* supply mocks.
|
|
8
|
+
*/
|
|
9
|
+
import { collectFeatureSnapshots, parseFeaturePattern, resolveCategories, RENDERERS, } from '@grafema/util';
|
|
10
|
+
/**
|
|
11
|
+
* Run the export action. Returns the rendered text on success — convenient
|
|
12
|
+
* for tests that don't want to mock writeText. The deps' writeText is also
|
|
13
|
+
* invoked so production callers don't need to handle the return value.
|
|
14
|
+
*/
|
|
15
|
+
export async function runExport(options, deps) {
|
|
16
|
+
const renderers = deps.renderers ?? RENDERERS;
|
|
17
|
+
const renderer = renderers[options.as];
|
|
18
|
+
if (!renderer) {
|
|
19
|
+
const known = Object.keys(renderers).sort().join(', ');
|
|
20
|
+
deps.fail(`Unknown format '${options.as}'. Known formats: ${known}.`, 2);
|
|
21
|
+
}
|
|
22
|
+
const parsed = parseFeaturePattern(options.feature);
|
|
23
|
+
if (!parsed) {
|
|
24
|
+
deps.fail(`Invalid --feature pattern: '${options.feature}'.`, 2);
|
|
25
|
+
}
|
|
26
|
+
// Validate format-category compatibility before doing any graph work.
|
|
27
|
+
const categories = resolveCategories(parsed.categoryGlob);
|
|
28
|
+
if (categories.length === 0) {
|
|
29
|
+
deps.fail(`Feature pattern '${options.feature}' matches no known category.`, 2);
|
|
30
|
+
}
|
|
31
|
+
const unsupported = categories.filter(c => !renderer.supports(c));
|
|
32
|
+
if (unsupported.length > 0 && unsupported.length === categories.length) {
|
|
33
|
+
deps.fail(`Format '${options.as}' does not support category '${unsupported[0]}'. ` +
|
|
34
|
+
`Try '--as docs-md' or narrow the pattern.`, 2);
|
|
35
|
+
}
|
|
36
|
+
const snapshots = await collectFeatureSnapshots(deps.backend, options.feature);
|
|
37
|
+
// If the renderer rejects some categories, drop those snapshots before
|
|
38
|
+
// rendering. Warn so the user sees the gap.
|
|
39
|
+
const accepted = [];
|
|
40
|
+
let droppedForCategory = 0;
|
|
41
|
+
for (const s of snapshots) {
|
|
42
|
+
if (renderer.supports(s.category))
|
|
43
|
+
accepted.push(s);
|
|
44
|
+
else
|
|
45
|
+
droppedForCategory++;
|
|
46
|
+
}
|
|
47
|
+
if (droppedForCategory > 0) {
|
|
48
|
+
deps.warn(`Skipping ${droppedForCategory} feature${droppedForCategory === 1 ? '' : 's'} ` +
|
|
49
|
+
`unsupported by --as ${options.as}.`);
|
|
50
|
+
}
|
|
51
|
+
if (accepted.length === 0) {
|
|
52
|
+
deps.warn(`No features matched '${options.feature}' for format '${options.as}'.`);
|
|
53
|
+
}
|
|
54
|
+
const text = renderer.render(accepted);
|
|
55
|
+
await deps.writeText(options.output ?? null, text);
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=exportAction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exportAction.js","sourceRoot":"","sources":["../../src/commands/exportAction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `grafema features` — surface FEATURE-level cross-modality insights.
|
|
3
|
+
*
|
|
4
|
+
* Currently exposes one mode:
|
|
5
|
+
* --duplicates List clusters of FEATUREs whose entry-points share an
|
|
6
|
+
* identical BEHAVIOR (same forward-slice hash).
|
|
7
|
+
*
|
|
8
|
+
* Other modes (--by-effect, --by-domain, …) are anticipated but not in scope
|
|
9
|
+
* for REG-1119.
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
export declare const featuresCommand: Command;
|
|
13
|
+
//# sourceMappingURL=features.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/commands/features.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBpC,eAAO,MAAM,eAAe,SAkDxB,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `grafema features` — surface FEATURE-level cross-modality insights.
|
|
3
|
+
*
|
|
4
|
+
* Currently exposes one mode:
|
|
5
|
+
* --duplicates List clusters of FEATUREs whose entry-points share an
|
|
6
|
+
* identical BEHAVIOR (same forward-slice hash).
|
|
7
|
+
*
|
|
8
|
+
* Other modes (--by-effect, --by-domain, …) are anticipated but not in scope
|
|
9
|
+
* for REG-1119.
|
|
10
|
+
*/
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { resolve, join } from 'path';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
14
|
+
import { RFDBServerBackend } from '@grafema/util';
|
|
15
|
+
import { exitWithError } from '../utils/errorFormatter.js';
|
|
16
|
+
import { findSharedBehaviorClusters, formatSharedBehaviorClusters, } from './featuresAction.js';
|
|
17
|
+
export const featuresCommand = new Command('features')
|
|
18
|
+
.description('List FEATURE-level cross-modality insights (e.g. duplicate behaviors)')
|
|
19
|
+
.option('-p, --project <path>', 'Project path', '.')
|
|
20
|
+
.option('-j, --json', 'Output as JSON')
|
|
21
|
+
.option('-d, --duplicates', 'List clusters of FEATUREs that share a BEHAVIOR')
|
|
22
|
+
.option('--min-cluster-size <n>', 'Minimum features per cluster (default: 2)', '2')
|
|
23
|
+
.option('--limit <n>', 'Maximum clusters to return (default: 100)', '100')
|
|
24
|
+
.addHelpText('after', `
|
|
25
|
+
Examples:
|
|
26
|
+
grafema features --duplicates List FEATUREs with shared behaviors
|
|
27
|
+
grafema features --duplicates --json Same, machine-readable JSON
|
|
28
|
+
grafema features -d --min-cluster-size 3 Only clusters with >=3 features
|
|
29
|
+
`)
|
|
30
|
+
.action(async (options) => {
|
|
31
|
+
if (!options.duplicates) {
|
|
32
|
+
exitWithError('No subcommand selected', [
|
|
33
|
+
'Try: grafema features --duplicates',
|
|
34
|
+
'See: grafema features --help',
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
const projectPath = resolve(options.project);
|
|
38
|
+
const grafemaDir = join(projectPath, '.grafema');
|
|
39
|
+
const dbPath = join(grafemaDir, 'graph.rfdb');
|
|
40
|
+
if (!existsSync(dbPath)) {
|
|
41
|
+
exitWithError('No graph database found', ['Run: grafema analyze']);
|
|
42
|
+
}
|
|
43
|
+
const minClusterSize = parsePositiveInt(options.minClusterSize, 2);
|
|
44
|
+
const limit = parsePositiveInt(options.limit, 100);
|
|
45
|
+
const backend = new RFDBServerBackend({ dbPath, clientName: 'cli' });
|
|
46
|
+
await backend.connect();
|
|
47
|
+
try {
|
|
48
|
+
// RFDBServerBackend matches FeaturesBackendLike at runtime.
|
|
49
|
+
const clusters = await findSharedBehaviorClusters(backend, { minClusterSize, limit });
|
|
50
|
+
if (options.json) {
|
|
51
|
+
console.log(JSON.stringify(clusters, null, 2));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log(formatSharedBehaviorClusters(clusters));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await backend.close();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
function parsePositiveInt(raw, fallback) {
|
|
62
|
+
if (!raw)
|
|
63
|
+
return fallback;
|
|
64
|
+
const n = Number.parseInt(raw, 10);
|
|
65
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
66
|
+
return fallback;
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=features.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features.js","sourceRoot":"","sources":["../../src/commands/features.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Features command action — surface FEATURE-level cross-modality insights.
|
|
3
|
+
*
|
|
4
|
+
* Currently implements --duplicates: groups BEHAVIOR nodes by their
|
|
5
|
+
* `metadata.hash` and lists every cluster of size >= 2. Each cluster shows the
|
|
6
|
+
* feature ENTRY_POINTs (incoming `IMPLEMENTED_BY` edges) that share the
|
|
7
|
+
* implementation.
|
|
8
|
+
*
|
|
9
|
+
* Surfaces the data emitted by `behaviorEnricher` Pass 2: SHARES_BEHAVIOR_WITH
|
|
10
|
+
* edges already exist, but they're not user-facing. This subcommand makes the
|
|
11
|
+
* insight visible from the CLI ("this CLI command and that MCP tool are thin
|
|
12
|
+
* wrappers around the same library function").
|
|
13
|
+
*
|
|
14
|
+
* REG-1119.
|
|
15
|
+
*/
|
|
16
|
+
import type { EdgeType } from '@grafema/types';
|
|
17
|
+
/**
|
|
18
|
+
* Minimal backend interface — matches the subset of RFDBServerBackend
|
|
19
|
+
* (and the graph-handlers' GraphBackendLike) that we need. Lets us test with
|
|
20
|
+
* a simple in-memory mock without requiring a live RFDB server.
|
|
21
|
+
*/
|
|
22
|
+
export interface FeaturesBackendLike {
|
|
23
|
+
queryNodes(query: {
|
|
24
|
+
type?: string;
|
|
25
|
+
nodeType?: string;
|
|
26
|
+
}): AsyncIterable<Record<string, unknown>>;
|
|
27
|
+
getNode(id: string): Promise<Record<string, unknown> | null>;
|
|
28
|
+
getIncomingEdges(nodeId: string, edgeTypes?: EdgeType[] | null): Promise<Array<{
|
|
29
|
+
src: string;
|
|
30
|
+
dst: string;
|
|
31
|
+
type: string;
|
|
32
|
+
metadata?: Record<string, unknown> | undefined;
|
|
33
|
+
}>>;
|
|
34
|
+
}
|
|
35
|
+
/** A FEATURE participating in a shared-behavior cluster. */
|
|
36
|
+
export interface SharedFeatureRef {
|
|
37
|
+
/** Feature semantic id. */
|
|
38
|
+
id: string;
|
|
39
|
+
/** Feature node type — e.g. `cli:command`, `mcp:tool`, `vscode:command`. */
|
|
40
|
+
type: string;
|
|
41
|
+
/** Feature name (e.g. `analyze`). */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Source file (may be empty for synthetic features). */
|
|
44
|
+
file: string;
|
|
45
|
+
}
|
|
46
|
+
/** A group of FEATUREs whose behavior hashes are identical. */
|
|
47
|
+
export interface SharedBehaviorCluster {
|
|
48
|
+
/** sha256 hash from BEHAVIOR.metadata.hash — the cluster key. */
|
|
49
|
+
hash: string;
|
|
50
|
+
/** Effects array carried on the shared BEHAVIOR. */
|
|
51
|
+
effects: string[];
|
|
52
|
+
/** Forward-slice size of the shared behavior. */
|
|
53
|
+
coreNodeCount: number;
|
|
54
|
+
/** Features that implement the same behavior. */
|
|
55
|
+
features: SharedFeatureRef[];
|
|
56
|
+
}
|
|
57
|
+
export interface FindSharedBehaviorOptions {
|
|
58
|
+
/** Minimum cluster size to include. Default 2. */
|
|
59
|
+
minClusterSize?: number;
|
|
60
|
+
/** Maximum number of clusters to return. Default 100. */
|
|
61
|
+
limit?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Core algorithm — testable, accepts any backend implementing
|
|
65
|
+
* `FeaturesBackendLike`.
|
|
66
|
+
*
|
|
67
|
+
* Steps:
|
|
68
|
+
* 1. Iterate every BEHAVIOR node, read its `hash` + `effects` + `coreNodeCount`.
|
|
69
|
+
* 2. Bucket behaviors by hash.
|
|
70
|
+
* 3. For each bucket of size >= minClusterSize, walk incoming `IMPLEMENTED_BY`
|
|
71
|
+
* edges to enumerate the FEATUREs.
|
|
72
|
+
*
|
|
73
|
+
* Returns clusters sorted by size (desc), then by hash (asc, deterministic).
|
|
74
|
+
*/
|
|
75
|
+
export declare function findSharedBehaviorClusters(backend: FeaturesBackendLike, options?: FindSharedBehaviorOptions): Promise<SharedBehaviorCluster[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Format clusters as a human-readable text report. Mirrors the symmetric MCP
|
|
78
|
+
* handler output: same field ordering, same labels. JSON output is the same
|
|
79
|
+
* structured array.
|
|
80
|
+
*/
|
|
81
|
+
export declare function formatSharedBehaviorClusters(clusters: SharedBehaviorCluster[]): string;
|
|
82
|
+
//# sourceMappingURL=featuresAction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"featuresAction.d.ts","sourceRoot":"","sources":["../../src/commands/featuresAction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAC7D,gBAAgB,CACd,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,QAAQ,EAAE,GAAG,IAAI,GAC5B,OAAO,CAAC,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;KAAE,CAAC,CAAC,CAAC;CAC/G;AAED,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,2BAA2B;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,iDAAiD;IACjD,aAAa,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,yBAAyB;IACxC,kDAAkD;IAClD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD;;;;;;;;;;;GAWG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAiFlC;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,qBAAqB,EAAE,GAAG,MAAM,CAsBtF"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const DEFAULT_MIN_CLUSTER_SIZE = 2;
|
|
2
|
+
const DEFAULT_LIMIT = 100;
|
|
3
|
+
/**
|
|
4
|
+
* Core algorithm — testable, accepts any backend implementing
|
|
5
|
+
* `FeaturesBackendLike`.
|
|
6
|
+
*
|
|
7
|
+
* Steps:
|
|
8
|
+
* 1. Iterate every BEHAVIOR node, read its `hash` + `effects` + `coreNodeCount`.
|
|
9
|
+
* 2. Bucket behaviors by hash.
|
|
10
|
+
* 3. For each bucket of size >= minClusterSize, walk incoming `IMPLEMENTED_BY`
|
|
11
|
+
* edges to enumerate the FEATUREs.
|
|
12
|
+
*
|
|
13
|
+
* Returns clusters sorted by size (desc), then by hash (asc, deterministic).
|
|
14
|
+
*/
|
|
15
|
+
export async function findSharedBehaviorClusters(backend, options = {}) {
|
|
16
|
+
const minClusterSize = Math.max(2, options.minClusterSize ?? DEFAULT_MIN_CLUSTER_SIZE);
|
|
17
|
+
const limit = Math.max(1, options.limit ?? DEFAULT_LIMIT);
|
|
18
|
+
const behaviors = [];
|
|
19
|
+
for await (const node of backend.queryNodes({ type: 'BEHAVIOR' })) {
|
|
20
|
+
const id = String(node.id ?? '');
|
|
21
|
+
if (!id)
|
|
22
|
+
continue;
|
|
23
|
+
const hash = readString(node.hash);
|
|
24
|
+
if (!hash)
|
|
25
|
+
continue;
|
|
26
|
+
const effects = readStringArray(node.effects);
|
|
27
|
+
const coreNodeCount = readNumber(node.coreNodeCount) ?? 0;
|
|
28
|
+
behaviors.push({ id, hash, effects, coreNodeCount });
|
|
29
|
+
}
|
|
30
|
+
// 2. Group by hash.
|
|
31
|
+
const byHash = new Map();
|
|
32
|
+
for (const b of behaviors) {
|
|
33
|
+
let bucket = byHash.get(b.hash);
|
|
34
|
+
if (!bucket) {
|
|
35
|
+
bucket = [];
|
|
36
|
+
byHash.set(b.hash, bucket);
|
|
37
|
+
}
|
|
38
|
+
bucket.push(b);
|
|
39
|
+
}
|
|
40
|
+
// 3. For each multi-behavior bucket, expand incoming IMPLEMENTED_BY edges
|
|
41
|
+
// into FEATURE refs.
|
|
42
|
+
const clusters = [];
|
|
43
|
+
for (const [hash, bucket] of byHash) {
|
|
44
|
+
if (bucket.length < minClusterSize)
|
|
45
|
+
continue;
|
|
46
|
+
const features = [];
|
|
47
|
+
const seenFeatureIds = new Set();
|
|
48
|
+
for (const beh of bucket) {
|
|
49
|
+
const edges = await backend.getIncomingEdges(beh.id, ['IMPLEMENTED_BY']);
|
|
50
|
+
for (const edge of edges) {
|
|
51
|
+
const featureId = String(edge.src);
|
|
52
|
+
if (!featureId || seenFeatureIds.has(featureId))
|
|
53
|
+
continue;
|
|
54
|
+
seenFeatureIds.add(featureId);
|
|
55
|
+
const node = await backend.getNode(featureId);
|
|
56
|
+
if (!node)
|
|
57
|
+
continue;
|
|
58
|
+
features.push({
|
|
59
|
+
id: featureId,
|
|
60
|
+
type: readString(node.type) ?? 'UNKNOWN',
|
|
61
|
+
name: readString(node.name) ?? '',
|
|
62
|
+
file: readString(node.file) ?? '',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// De-dup may have dropped behaviors back below threshold (e.g. multiple
|
|
67
|
+
// BEHAVIOR rows from the same FEATURE). Re-check on the resolved feature
|
|
68
|
+
// count.
|
|
69
|
+
if (features.length < minClusterSize)
|
|
70
|
+
continue;
|
|
71
|
+
// Stable order of features within a cluster: by type then name.
|
|
72
|
+
features.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type.localeCompare(b.type)));
|
|
73
|
+
// Take effects + coreNodeCount from the first behavior — they're identical
|
|
74
|
+
// for behaviors with the same hash by construction.
|
|
75
|
+
clusters.push({
|
|
76
|
+
hash,
|
|
77
|
+
effects: bucket[0].effects,
|
|
78
|
+
coreNodeCount: bucket[0].coreNodeCount,
|
|
79
|
+
features,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Sort clusters: largest first, hash ascending for tie-break (deterministic).
|
|
83
|
+
clusters.sort((a, b) => (b.features.length - a.features.length) || a.hash.localeCompare(b.hash));
|
|
84
|
+
return clusters.slice(0, limit);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Format clusters as a human-readable text report. Mirrors the symmetric MCP
|
|
88
|
+
* handler output: same field ordering, same labels. JSON output is the same
|
|
89
|
+
* structured array.
|
|
90
|
+
*/
|
|
91
|
+
export function formatSharedBehaviorClusters(clusters) {
|
|
92
|
+
if (clusters.length === 0) {
|
|
93
|
+
return 'No FEATUREs share a BEHAVIOR (each entry-point has a unique implementation).';
|
|
94
|
+
}
|
|
95
|
+
const lines = [];
|
|
96
|
+
lines.push(`Shared-behavior clusters: ${clusters.length}`);
|
|
97
|
+
lines.push('='.repeat(40));
|
|
98
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
99
|
+
const c = clusters[i];
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push(`Cluster ${i + 1} — ${c.features.length} feature(s) share behavior`);
|
|
102
|
+
lines.push(` hash: ${c.hash.slice(0, 16)}…`);
|
|
103
|
+
lines.push(` coreNodeCount: ${c.coreNodeCount}`);
|
|
104
|
+
lines.push(` effects: ${c.effects.length === 0 ? '(none)' : c.effects.join(', ')}`);
|
|
105
|
+
lines.push(' features:');
|
|
106
|
+
for (const f of c.features) {
|
|
107
|
+
const file = f.file ? ` [${f.file}]` : '';
|
|
108
|
+
lines.push(` - ${f.type} ${f.name}${file}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return lines.join('\n');
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Internal helpers — defensive parsers tolerating both string-encoded JSON
|
|
115
|
+
// metadata (from raw client) and already-parsed values (from RFDBServerBackend
|
|
116
|
+
// _parseNode).
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
function readString(v) {
|
|
119
|
+
return typeof v === 'string' ? v : undefined;
|
|
120
|
+
}
|
|
121
|
+
function readNumber(v) {
|
|
122
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
123
|
+
}
|
|
124
|
+
function readStringArray(v) {
|
|
125
|
+
if (Array.isArray(v))
|
|
126
|
+
return v.filter((x) => typeof x === 'string');
|
|
127
|
+
if (typeof v === 'string') {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(v);
|
|
130
|
+
if (Array.isArray(parsed))
|
|
131
|
+
return parsed.filter((x) => typeof x === 'string');
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// not JSON
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=featuresAction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"featuresAction.js","sourceRoot":"","sources":["../../src/commands/featuresAction.ts"],"names":[],"mappings":"AA8DA,MAAM,wBAAwB,GAAG,CAAC,CAAC;AACnC,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,OAA4B,EAC5B,UAAqC,EAAE;IAEvC,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,cAAc,IAAI,wBAAwB,CAAC,CAAC;IACvF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,IAAI,aAAa,CAAC,CAAC;IAS1D,MAAM,SAAS,GAAkB,EAAE,CAAC;IACpC,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;QAClE,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QACjC,IAAI,CAAC,EAAE;YAAE,SAAS;QAClB,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9C,MAAM,aAAa,GAAG,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAC1D,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,oBAAoB;IACpB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAyB,CAAC;IAChD,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,IAAI,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7B,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IAED,0EAA0E;IAC1E,wBAAwB;IACxB,MAAM,QAAQ,GAA4B,EAAE,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACpC,IAAI,MAAM,CAAC,MAAM,GAAG,cAAc;YAAE,SAAS;QAE7C,MAAM,QAAQ,GAAuB,EAAE,CAAC;QACxC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;QAEzC,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;YACzB,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAe,CAAC,CAAC;YACvF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACnC,IAAI,CAAC,SAAS,IAAI,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC;oBAAE,SAAS;gBAC1D,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC9B,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;gBAC9C,IAAI,CAAC,IAAI;oBAAE,SAAS;gBACpB,QAAQ,CAAC,IAAI,CAAC;oBACZ,EAAE,EAAE,SAAS;oBACb,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,SAAS;oBACxC,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;oBACjC,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;iBAClC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,wEAAwE;QACxE,yEAAyE;QACzE,SAAS;QACT,IAAI,QAAQ,CAAC,MAAM,GAAG,cAAc;YAAE,SAAS;QAE/C,gEAAgE;QAChE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAE3G,2EAA2E;QAC3E,oDAAoD;QACpD,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI;YACJ,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO;YAC1B,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa;YACtC,QAAQ;SACT,CAAC,CAAC;IACL,CAAC;IAED,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEjG,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAClC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAAC,QAAiC;IAC5E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,8EAA8E,CAAC;IACxF,CAAC;IAED,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,6BAA6B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3D,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,4BAA4B,CAAC,CAAC;QAChF,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;QACvD,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;QAClD,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3F,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC1B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,8EAA8E;AAC9E,2EAA2E;AAC3E,+EAA+E;AAC/E,eAAe;AACf,8EAA8E;AAE9E,SAAS,UAAU,CAAC,CAAU;IAC5B,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC;AAED,SAAS,UAAU,CAAC,CAAU;IAC5B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACrE,CAAC;AAED,SAAS,eAAe,CAAC,CAAU;IACjC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;IACjF,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;gBAAE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;QAC7F,CAAC;QAAC,MAAM,CAAC;YACP,WAAW;QACb,CAAC;IACH,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `grafema start` — unified command to bring up the entire Grafema stack.
|
|
3
|
+
*
|
|
4
|
+
* Starts RFDB server (with HTTP) and prints connection info.
|
|
5
|
+
* Foreground by default; `--background` detaches.
|
|
6
|
+
*
|
|
7
|
+
* `grafema stop` — graceful shutdown of everything.
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
export declare const startCommand: Command;
|
|
11
|
+
export declare const stopCommand: Command;
|
|
12
|
+
//# sourceMappingURL=start.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../../src/commands/start.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0FpC,eAAO,MAAM,YAAY,SAgKrB,CAAC;AAIL,eAAO,MAAM,WAAW,SAuCpB,CAAC"}
|