@beignet/cli 0.0.49 → 0.0.50
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/CHANGELOG.md +20 -0
- package/README.md +41 -1
- package/dist/analysis/workspace.d.ts +1 -0
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +20 -10
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts +103 -0
- package/dist/app-map-changes.d.ts.map +1 -0
- package/dist/app-map-changes.js +949 -0
- package/dist/app-map-changes.js.map +1 -0
- package/dist/git-changes.d.ts +30 -0
- package/dist/git-changes.d.ts.map +1 -0
- package/dist/git-changes.js +367 -0
- package/dist/git-changes.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.js +291 -18
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +3 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/lib.js.map +1 -1
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +31 -27
- package/dist/make/shared.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +24 -2
- package/dist/mcp.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +21 -1
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +4 -1
- package/dist/templates/base.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +25 -2
- package/src/analysis/workspace.ts +25 -9
- package/src/app-map-changes.ts +1462 -0
- package/src/git-changes.ts +511 -0
- package/src/index.ts +39 -1
- package/src/inspect.ts +422 -23
- package/src/lib.ts +21 -0
- package/src/make/shared.ts +31 -27
- package/src/mcp.ts +32 -2
- package/src/templates/agents.ts +21 -1
- package/src/templates/base.ts +4 -1
package/src/inspect.ts
CHANGED
|
@@ -4404,6 +4404,7 @@ type WorkflowRegistrationDrift = {
|
|
|
4404
4404
|
unregistered: UnregisteredWorkflowRegistry[];
|
|
4405
4405
|
wiringFile?: string;
|
|
4406
4406
|
eventBusFile?: string;
|
|
4407
|
+
unsafeLifecycleFiles: string[];
|
|
4407
4408
|
};
|
|
4408
4409
|
};
|
|
4409
4410
|
|
|
@@ -4482,6 +4483,15 @@ async function inspectWorkflowRegistrationDrift(
|
|
|
4482
4483
|
}
|
|
4483
4484
|
|
|
4484
4485
|
const infraDir = directoryPath(path.dirname(config.paths.portWiring));
|
|
4486
|
+
for (const file of drift.listeners.unsafeLifecycleFiles) {
|
|
4487
|
+
diagnostics.push({
|
|
4488
|
+
severity: "warning",
|
|
4489
|
+
code: "BEIGNET_LISTENER_LIFECYCLE_UNSAFE",
|
|
4490
|
+
file,
|
|
4491
|
+
message: `${file} calls registerListeners(...) without the complete provider lifecycle, so server startup can resolve before listeners are ready or shutdown can leak subscriptions. Register listeners in start(), return or await registration.ready, and return or await registration.unsubscribe() in stop().`,
|
|
4492
|
+
});
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4485
4495
|
const listenerTarget = drift.listeners.wiringFile
|
|
4486
4496
|
? `${drift.listeners.wiringFile}, which already calls registerListeners(...)`
|
|
4487
4497
|
: drift.listeners.eventBusFile
|
|
@@ -4652,7 +4662,7 @@ async function workflowRegistrationDrift(
|
|
|
4652
4662
|
events: [],
|
|
4653
4663
|
jobs: [],
|
|
4654
4664
|
},
|
|
4655
|
-
listeners: { unregistered: [] },
|
|
4665
|
+
listeners: { unregistered: [], unsafeLifecycleFiles: [] },
|
|
4656
4666
|
};
|
|
4657
4667
|
if (registries.length === 0) return drift;
|
|
4658
4668
|
|
|
@@ -4714,7 +4724,12 @@ async function workflowRegistrationDrift(
|
|
|
4714
4724
|
|
|
4715
4725
|
const listenerRegistries = byKind("listeners");
|
|
4716
4726
|
if (listenerRegistries.length > 0) {
|
|
4717
|
-
const wiring = await listenerWiringReferences(
|
|
4727
|
+
const wiring = await listenerWiringReferences(
|
|
4728
|
+
targetDir,
|
|
4729
|
+
files,
|
|
4730
|
+
config,
|
|
4731
|
+
listenerRegistries,
|
|
4732
|
+
);
|
|
4718
4733
|
drift.listeners = {
|
|
4719
4734
|
unregistered: unregisteredWorkflowRegistries(
|
|
4720
4735
|
listenerRegistries,
|
|
@@ -4722,6 +4737,7 @@ async function workflowRegistrationDrift(
|
|
|
4722
4737
|
),
|
|
4723
4738
|
wiringFile: wiring.wiringFile,
|
|
4724
4739
|
eventBusFile: wiring.eventBusFile,
|
|
4740
|
+
unsafeLifecycleFiles: wiring.unsafeLifecycleFiles,
|
|
4725
4741
|
};
|
|
4726
4742
|
}
|
|
4727
4743
|
|
|
@@ -5056,12 +5072,15 @@ async function listenerWiringReferences(
|
|
|
5056
5072
|
targetDir: string,
|
|
5057
5073
|
files: string[],
|
|
5058
5074
|
config: ResolvedBeignetConfig,
|
|
5075
|
+
listenerRegistries: FeatureWorkflowRegistry[],
|
|
5059
5076
|
): Promise<{
|
|
5060
5077
|
identifiers: Set<string>;
|
|
5061
5078
|
wiringFile?: string;
|
|
5062
5079
|
eventBusFile?: string;
|
|
5080
|
+
unsafeLifecycleFiles: string[];
|
|
5063
5081
|
}> {
|
|
5064
5082
|
const identifiers = new Set<string>();
|
|
5083
|
+
const unsafeLifecycleFiles = new Set<string>();
|
|
5065
5084
|
let wiringFile: string | undefined;
|
|
5066
5085
|
let eventBusFile: string | undefined;
|
|
5067
5086
|
let centralListenerRegistryReferenced = false;
|
|
@@ -5073,35 +5092,54 @@ async function listenerWiringReferences(
|
|
|
5073
5092
|
|
|
5074
5093
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
5075
5094
|
const namedImports = parseNamedImportSources(source);
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5095
|
+
const sourceFile = ts.createSourceFile(
|
|
5096
|
+
file,
|
|
5097
|
+
source,
|
|
5098
|
+
ts.ScriptTarget.Latest,
|
|
5099
|
+
true,
|
|
5100
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
5101
|
+
);
|
|
5102
|
+
const registerListenersBindings = importedRegisterListenersBindings(
|
|
5103
|
+
source,
|
|
5104
|
+
namedImports,
|
|
5105
|
+
);
|
|
5106
|
+
const calls = registerListenersCalls(sourceFile, registerListenersBindings);
|
|
5107
|
+
let foundRelevantCall = false;
|
|
5085
5108
|
|
|
5086
|
-
|
|
5109
|
+
for (const call of calls) {
|
|
5110
|
+
const argsText = call.arguments
|
|
5111
|
+
.map((argument) => argument.getText(sourceFile))
|
|
5112
|
+
.join(",");
|
|
5087
5113
|
const callIdentifiers = identifiersFromArrayExpression(argsText);
|
|
5114
|
+
const referencesCentral = callReferencesCentralListenerRegistry({
|
|
5115
|
+
callIdentifiers,
|
|
5116
|
+
namedImports,
|
|
5117
|
+
importerFile: file,
|
|
5118
|
+
listenerRegistryFile,
|
|
5119
|
+
files,
|
|
5120
|
+
});
|
|
5121
|
+
const referencesFeature = callReferencesFeatureListenerRegistry({
|
|
5122
|
+
callIdentifiers,
|
|
5123
|
+
namedImports,
|
|
5124
|
+
importerFile: file,
|
|
5125
|
+
files,
|
|
5126
|
+
listenerRegistries,
|
|
5127
|
+
});
|
|
5128
|
+
if (!referencesCentral && !referencesFeature) continue;
|
|
5129
|
+
|
|
5130
|
+
foundRelevantCall = true;
|
|
5088
5131
|
for (const identifier of callIdentifiers) {
|
|
5089
5132
|
identifiers.add(identifier);
|
|
5090
5133
|
}
|
|
5091
|
-
if (
|
|
5092
|
-
callReferencesCentralListenerRegistry({
|
|
5093
|
-
callIdentifiers,
|
|
5094
|
-
namedImports,
|
|
5095
|
-
importerFile: file,
|
|
5096
|
-
listenerRegistryFile,
|
|
5097
|
-
files,
|
|
5098
|
-
})
|
|
5099
|
-
) {
|
|
5134
|
+
if (referencesCentral) {
|
|
5100
5135
|
centralListenerRegistryReferenced = true;
|
|
5101
5136
|
}
|
|
5137
|
+
if (!hasSafeListenerRegistrationLifecycle(call, sourceFile)) {
|
|
5138
|
+
unsafeLifecycleFiles.add(file);
|
|
5139
|
+
}
|
|
5102
5140
|
}
|
|
5103
5141
|
|
|
5104
|
-
if (
|
|
5142
|
+
if (foundRelevantCall) {
|
|
5105
5143
|
wiringFile ??= file;
|
|
5106
5144
|
} else if (!eventBusFile && /\bcreate\w*EventBus\s*\(/.test(source)) {
|
|
5107
5145
|
eventBusFile = file;
|
|
@@ -5124,7 +5162,368 @@ async function listenerWiringReferences(
|
|
|
5124
5162
|
}
|
|
5125
5163
|
}
|
|
5126
5164
|
|
|
5127
|
-
return {
|
|
5165
|
+
return {
|
|
5166
|
+
identifiers,
|
|
5167
|
+
wiringFile,
|
|
5168
|
+
eventBusFile,
|
|
5169
|
+
unsafeLifecycleFiles: [...unsafeLifecycleFiles].sort(),
|
|
5170
|
+
};
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
type ListenerLifecycleHook =
|
|
5174
|
+
| ts.MethodDeclaration
|
|
5175
|
+
| ts.FunctionExpression
|
|
5176
|
+
| ts.ArrowFunction;
|
|
5177
|
+
|
|
5178
|
+
interface ListenerLifecycleTarget {
|
|
5179
|
+
text: string;
|
|
5180
|
+
binding?: ts.Node;
|
|
5181
|
+
}
|
|
5182
|
+
|
|
5183
|
+
function registerListenersCalls(
|
|
5184
|
+
sourceFile: ts.SourceFile,
|
|
5185
|
+
bindings: ReadonlySet<string>,
|
|
5186
|
+
): ts.CallExpression[] {
|
|
5187
|
+
const calls: ts.CallExpression[] = [];
|
|
5188
|
+
const visit = (node: ts.Node): void => {
|
|
5189
|
+
if (
|
|
5190
|
+
ts.isCallExpression(node) &&
|
|
5191
|
+
((ts.isIdentifier(node.expression) &&
|
|
5192
|
+
bindings.has(node.expression.text)) ||
|
|
5193
|
+
(ts.isPropertyAccessExpression(node.expression) &&
|
|
5194
|
+
node.expression.name.text === "registerListeners" &&
|
|
5195
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
5196
|
+
bindings.has(`${node.expression.expression.text}.*`)))
|
|
5197
|
+
) {
|
|
5198
|
+
calls.push(node);
|
|
5199
|
+
}
|
|
5200
|
+
ts.forEachChild(node, visit);
|
|
5201
|
+
};
|
|
5202
|
+
visit(sourceFile);
|
|
5203
|
+
return calls;
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
function importedRegisterListenersBindings(
|
|
5207
|
+
source: string,
|
|
5208
|
+
namedImports: Map<string, { importedName: string; sourcePath: string }>,
|
|
5209
|
+
): Set<string> {
|
|
5210
|
+
const bindings = new Set<string>();
|
|
5211
|
+
for (const [localName, imported] of namedImports) {
|
|
5212
|
+
if (
|
|
5213
|
+
imported.importedName === "registerListeners" &&
|
|
5214
|
+
imported.sourcePath === "@beignet/core/events"
|
|
5215
|
+
) {
|
|
5216
|
+
bindings.add(localName);
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
const namespaceImport =
|
|
5220
|
+
/import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']@beignet\/core\/events["']/g;
|
|
5221
|
+
for (const match of source.matchAll(namespaceImport)) {
|
|
5222
|
+
bindings.add(`${match[1]}.*`);
|
|
5223
|
+
}
|
|
5224
|
+
return bindings;
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5227
|
+
function hasSafeListenerRegistrationLifecycle(
|
|
5228
|
+
call: ts.CallExpression,
|
|
5229
|
+
sourceFile: ts.SourceFile,
|
|
5230
|
+
): boolean {
|
|
5231
|
+
const startHook = enclosingListenerLifecycleHook(call, "start");
|
|
5232
|
+
const target = listenerRegistrationTarget(call, sourceFile);
|
|
5233
|
+
if (!startHook || !target) return false;
|
|
5234
|
+
const lifecycleOwner = listenerLifecycleOwner(startHook);
|
|
5235
|
+
if (!lifecycleOwner) return false;
|
|
5236
|
+
|
|
5237
|
+
let observesReadiness = false;
|
|
5238
|
+
let observesCleanup = false;
|
|
5239
|
+
const visit = (node: ts.Node): void => {
|
|
5240
|
+
if (
|
|
5241
|
+
!observesReadiness &&
|
|
5242
|
+
ts.isPropertyAccessExpression(node) &&
|
|
5243
|
+
node.name.text === "ready" &&
|
|
5244
|
+
listenerLifecycleTargetsMatch(
|
|
5245
|
+
listenerLifecycleTarget(node.expression, sourceFile),
|
|
5246
|
+
target,
|
|
5247
|
+
) &&
|
|
5248
|
+
enclosingListenerLifecycleHook(node, "start") === startHook &&
|
|
5249
|
+
listenerLifecyclePromiseIsObserved(node, startHook)
|
|
5250
|
+
) {
|
|
5251
|
+
observesReadiness = true;
|
|
5252
|
+
}
|
|
5253
|
+
|
|
5254
|
+
if (
|
|
5255
|
+
!observesCleanup &&
|
|
5256
|
+
ts.isCallExpression(node) &&
|
|
5257
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
5258
|
+
node.expression.name.text === "unsubscribe" &&
|
|
5259
|
+
listenerLifecycleTargetsMatch(
|
|
5260
|
+
listenerLifecycleTarget(node.expression.expression, sourceFile),
|
|
5261
|
+
target,
|
|
5262
|
+
)
|
|
5263
|
+
) {
|
|
5264
|
+
const stopHook = enclosingListenerLifecycleHook(node, "stop");
|
|
5265
|
+
if (
|
|
5266
|
+
stopHook &&
|
|
5267
|
+
listenerLifecycleOwner(stopHook) === lifecycleOwner &&
|
|
5268
|
+
listenerLifecyclePromiseIsObserved(node, stopHook)
|
|
5269
|
+
) {
|
|
5270
|
+
observesCleanup = true;
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
|
|
5274
|
+
if (!observesReadiness || !observesCleanup) {
|
|
5275
|
+
ts.forEachChild(node, visit);
|
|
5276
|
+
}
|
|
5277
|
+
};
|
|
5278
|
+
visit(lifecycleOwner);
|
|
5279
|
+
return observesReadiness && observesCleanup;
|
|
5280
|
+
}
|
|
5281
|
+
|
|
5282
|
+
function listenerRegistrationTarget(
|
|
5283
|
+
call: ts.CallExpression,
|
|
5284
|
+
sourceFile: ts.SourceFile,
|
|
5285
|
+
): ListenerLifecycleTarget | undefined {
|
|
5286
|
+
let expression: ts.Expression = call;
|
|
5287
|
+
while (
|
|
5288
|
+
ts.isParenthesizedExpression(expression.parent) ||
|
|
5289
|
+
ts.isAsExpression(expression.parent) ||
|
|
5290
|
+
ts.isSatisfiesExpression(expression.parent) ||
|
|
5291
|
+
ts.isNonNullExpression(expression.parent)
|
|
5292
|
+
) {
|
|
5293
|
+
expression = expression.parent;
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
const parent = expression.parent;
|
|
5297
|
+
if (
|
|
5298
|
+
ts.isVariableDeclaration(parent) &&
|
|
5299
|
+
parent.initializer === expression &&
|
|
5300
|
+
ts.isIdentifier(parent.name)
|
|
5301
|
+
) {
|
|
5302
|
+
return { text: parent.name.text, binding: parent };
|
|
5303
|
+
}
|
|
5304
|
+
if (
|
|
5305
|
+
ts.isBinaryExpression(parent) &&
|
|
5306
|
+
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
5307
|
+
parent.right === expression
|
|
5308
|
+
) {
|
|
5309
|
+
return listenerLifecycleTarget(parent.left, sourceFile);
|
|
5310
|
+
}
|
|
5311
|
+
return undefined;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
function listenerLifecycleTarget(
|
|
5315
|
+
expression: ts.Expression,
|
|
5316
|
+
sourceFile: ts.SourceFile,
|
|
5317
|
+
): ListenerLifecycleTarget | undefined {
|
|
5318
|
+
let current = expression;
|
|
5319
|
+
while (
|
|
5320
|
+
ts.isParenthesizedExpression(current) ||
|
|
5321
|
+
ts.isAsExpression(current) ||
|
|
5322
|
+
ts.isSatisfiesExpression(current) ||
|
|
5323
|
+
ts.isNonNullExpression(current)
|
|
5324
|
+
) {
|
|
5325
|
+
current = current.expression;
|
|
5326
|
+
}
|
|
5327
|
+
if (!ts.isIdentifier(current) && !ts.isPropertyAccessExpression(current)) {
|
|
5328
|
+
return undefined;
|
|
5329
|
+
}
|
|
5330
|
+
|
|
5331
|
+
return {
|
|
5332
|
+
text: current.getText(sourceFile),
|
|
5333
|
+
binding: listenerLifecycleRootIdentifier(current)
|
|
5334
|
+
? resolveListenerLifecycleBinding(
|
|
5335
|
+
listenerLifecycleRootIdentifier(current) as ts.Identifier,
|
|
5336
|
+
)
|
|
5337
|
+
: undefined,
|
|
5338
|
+
};
|
|
5339
|
+
}
|
|
5340
|
+
|
|
5341
|
+
function listenerLifecycleRootIdentifier(
|
|
5342
|
+
expression: ts.Identifier | ts.PropertyAccessExpression,
|
|
5343
|
+
): ts.Identifier | undefined {
|
|
5344
|
+
let current: ts.Expression = expression;
|
|
5345
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
5346
|
+
current = current.expression;
|
|
5347
|
+
}
|
|
5348
|
+
return ts.isIdentifier(current) ? current : undefined;
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
function resolveListenerLifecycleBinding(
|
|
5352
|
+
identifier: ts.Identifier,
|
|
5353
|
+
): ts.Node | undefined {
|
|
5354
|
+
let current: ts.Node | undefined = identifier;
|
|
5355
|
+
while (current) {
|
|
5356
|
+
if (ts.isBlock(current) || ts.isSourceFile(current)) {
|
|
5357
|
+
for (const statement of current.statements) {
|
|
5358
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
5359
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
5360
|
+
if (
|
|
5361
|
+
ts.isIdentifier(declaration.name) &&
|
|
5362
|
+
declaration.name.text === identifier.text
|
|
5363
|
+
) {
|
|
5364
|
+
return declaration;
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
if (ts.isFunctionLike(current)) {
|
|
5371
|
+
for (const parameter of current.parameters) {
|
|
5372
|
+
if (
|
|
5373
|
+
ts.isIdentifier(parameter.name) &&
|
|
5374
|
+
parameter.name.text === identifier.text
|
|
5375
|
+
) {
|
|
5376
|
+
return parameter;
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
if (
|
|
5382
|
+
ts.isCatchClause(current) &&
|
|
5383
|
+
current.variableDeclaration &&
|
|
5384
|
+
ts.isIdentifier(current.variableDeclaration.name) &&
|
|
5385
|
+
current.variableDeclaration.name.text === identifier.text
|
|
5386
|
+
) {
|
|
5387
|
+
return current.variableDeclaration;
|
|
5388
|
+
}
|
|
5389
|
+
current = current.parent;
|
|
5390
|
+
}
|
|
5391
|
+
return undefined;
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
function listenerLifecycleTargetsMatch(
|
|
5395
|
+
left: ListenerLifecycleTarget | undefined,
|
|
5396
|
+
right: ListenerLifecycleTarget,
|
|
5397
|
+
): boolean {
|
|
5398
|
+
return (
|
|
5399
|
+
left?.text === right.text &&
|
|
5400
|
+
(left.binding !== undefined || right.binding !== undefined
|
|
5401
|
+
? left.binding === right.binding
|
|
5402
|
+
: true)
|
|
5403
|
+
);
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
function enclosingListenerLifecycleHook(
|
|
5407
|
+
node: ts.Node,
|
|
5408
|
+
hookName: "start" | "stop",
|
|
5409
|
+
): ListenerLifecycleHook | undefined {
|
|
5410
|
+
let current: ts.Node | undefined = node.parent;
|
|
5411
|
+
while (current) {
|
|
5412
|
+
if (ts.isMethodDeclaration(current)) {
|
|
5413
|
+
return staticPropertyName(current.name) === hookName
|
|
5414
|
+
? current
|
|
5415
|
+
: undefined;
|
|
5416
|
+
}
|
|
5417
|
+
if (ts.isFunctionExpression(current) || ts.isArrowFunction(current)) {
|
|
5418
|
+
const parent = current.parent;
|
|
5419
|
+
return ts.isPropertyAssignment(parent) &&
|
|
5420
|
+
staticPropertyName(parent.name) === hookName
|
|
5421
|
+
? current
|
|
5422
|
+
: undefined;
|
|
5423
|
+
}
|
|
5424
|
+
if (ts.isFunctionDeclaration(current)) return undefined;
|
|
5425
|
+
current = current.parent;
|
|
5426
|
+
}
|
|
5427
|
+
return undefined;
|
|
5428
|
+
}
|
|
5429
|
+
|
|
5430
|
+
function listenerLifecycleOwner(
|
|
5431
|
+
hook: ListenerLifecycleHook,
|
|
5432
|
+
): ts.ObjectLiteralExpression | undefined {
|
|
5433
|
+
if (ts.isMethodDeclaration(hook)) {
|
|
5434
|
+
return ts.isObjectLiteralExpression(hook.parent) ? hook.parent : undefined;
|
|
5435
|
+
}
|
|
5436
|
+
const property = hook.parent;
|
|
5437
|
+
return ts.isPropertyAssignment(property) &&
|
|
5438
|
+
ts.isObjectLiteralExpression(property.parent)
|
|
5439
|
+
? property.parent
|
|
5440
|
+
: undefined;
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
function listenerLifecyclePromiseIsObserved(
|
|
5444
|
+
node: ts.Node,
|
|
5445
|
+
hook: ListenerLifecycleHook,
|
|
5446
|
+
): boolean {
|
|
5447
|
+
let current: ts.Node | undefined = node;
|
|
5448
|
+
while (current && current !== hook) {
|
|
5449
|
+
const parent: ts.Node = current.parent;
|
|
5450
|
+
if (
|
|
5451
|
+
(ts.isAwaitExpression(parent) && parent.expression === current) ||
|
|
5452
|
+
(ts.isReturnStatement(parent) && parent.expression === current)
|
|
5453
|
+
) {
|
|
5454
|
+
return true;
|
|
5455
|
+
}
|
|
5456
|
+
if (
|
|
5457
|
+
ts.isParenthesizedExpression(parent) ||
|
|
5458
|
+
ts.isAsExpression(parent) ||
|
|
5459
|
+
ts.isSatisfiesExpression(parent) ||
|
|
5460
|
+
ts.isNonNullExpression(parent)
|
|
5461
|
+
) {
|
|
5462
|
+
current = parent;
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
if (
|
|
5466
|
+
ts.isPropertyAccessExpression(parent) &&
|
|
5467
|
+
parent.expression === current
|
|
5468
|
+
) {
|
|
5469
|
+
const method = parent.name.text;
|
|
5470
|
+
const invocation = parent.parent;
|
|
5471
|
+
if (
|
|
5472
|
+
!["then", "catch", "finally"].includes(method) ||
|
|
5473
|
+
!ts.isCallExpression(invocation) ||
|
|
5474
|
+
invocation.expression !== parent ||
|
|
5475
|
+
method === "catch" ||
|
|
5476
|
+
(method === "then" && invocation.arguments.length > 1)
|
|
5477
|
+
) {
|
|
5478
|
+
return false;
|
|
5479
|
+
}
|
|
5480
|
+
current = invocation;
|
|
5481
|
+
continue;
|
|
5482
|
+
}
|
|
5483
|
+
return false;
|
|
5484
|
+
}
|
|
5485
|
+
return false;
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
function callReferencesFeatureListenerRegistry(args: {
|
|
5489
|
+
callIdentifiers: Set<string>;
|
|
5490
|
+
namedImports: Map<string, { importedName: string; sourcePath: string }>;
|
|
5491
|
+
importerFile: string;
|
|
5492
|
+
files: string[];
|
|
5493
|
+
listenerRegistries: FeatureWorkflowRegistry[];
|
|
5494
|
+
}): boolean {
|
|
5495
|
+
for (const identifier of args.callIdentifiers) {
|
|
5496
|
+
for (const registry of args.listenerRegistries) {
|
|
5497
|
+
if (
|
|
5498
|
+
identifier === registry.registryName ||
|
|
5499
|
+
registry.members.includes(identifier)
|
|
5500
|
+
) {
|
|
5501
|
+
return true;
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
const imported = args.namedImports.get(identifier);
|
|
5506
|
+
if (!imported) continue;
|
|
5507
|
+
const importedFile = sourceFileFromImport(
|
|
5508
|
+
imported.sourcePath,
|
|
5509
|
+
args.importerFile,
|
|
5510
|
+
args.files,
|
|
5511
|
+
);
|
|
5512
|
+
if (!importedFile) continue;
|
|
5513
|
+
|
|
5514
|
+
for (const registry of args.listenerRegistries) {
|
|
5515
|
+
if (
|
|
5516
|
+
(imported.importedName === registry.registryName &&
|
|
5517
|
+
importedFile === registry.indexFile) ||
|
|
5518
|
+
(registry.members.includes(imported.importedName) &&
|
|
5519
|
+
(importedFile === registry.indexFile ||
|
|
5520
|
+
registry.memberFiles.get(imported.importedName) === importedFile))
|
|
5521
|
+
) {
|
|
5522
|
+
return true;
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5526
|
+
return false;
|
|
5128
5527
|
}
|
|
5129
5528
|
|
|
5130
5529
|
function callReferencesCentralListenerRegistry(args: {
|
package/src/lib.ts
CHANGED
|
@@ -25,6 +25,23 @@ export {
|
|
|
25
25
|
mapApp,
|
|
26
26
|
projectAppMap,
|
|
27
27
|
} from "./app-map.js";
|
|
28
|
+
export type {
|
|
29
|
+
AppChangedFile,
|
|
30
|
+
AppChangeFileScope,
|
|
31
|
+
AppChangeGap,
|
|
32
|
+
AppChangeImpact,
|
|
33
|
+
AppChangeImpactedNode,
|
|
34
|
+
AppChangeImpactResult,
|
|
35
|
+
AppChangeReason,
|
|
36
|
+
AppChangeRelationship,
|
|
37
|
+
AppMapChangesOptions,
|
|
38
|
+
BoundedAppChangeGapSection,
|
|
39
|
+
BoundedAppChangeSection,
|
|
40
|
+
} from "./app-map-changes.js";
|
|
41
|
+
export {
|
|
42
|
+
formatAppChangeImpact,
|
|
43
|
+
mapAppChanges,
|
|
44
|
+
} from "./app-map-changes.js";
|
|
28
45
|
export type { CreateOptions } from "./create.js";
|
|
29
46
|
export { createProject } from "./create.js";
|
|
30
47
|
export type {
|
|
@@ -71,6 +88,10 @@ export {
|
|
|
71
88
|
explainTargetKinds,
|
|
72
89
|
formatExplain,
|
|
73
90
|
} from "./explain.js";
|
|
91
|
+
export type {
|
|
92
|
+
GitChangeComparison,
|
|
93
|
+
GitChangedFileStatus,
|
|
94
|
+
} from "./git-changes.js";
|
|
74
95
|
export { main } from "./index.js";
|
|
75
96
|
export type {
|
|
76
97
|
ApplyDoctorFixPlanOptions,
|
package/src/make/shared.ts
CHANGED
|
@@ -453,36 +453,40 @@ export function wireListenersProviderSource(
|
|
|
453
453
|
>()({
|
|
454
454
|
\tname: "app-listeners",
|
|
455
455
|
\tsetup({ ports, createServiceContext }) {
|
|
456
|
-
\t\
|
|
457
|
-
|
|
458
|
-
\t\
|
|
459
|
-
\t\t\
|
|
460
|
-
\t\t\t\
|
|
461
|
-
\t\t\
|
|
462
|
-
\t\t\t\
|
|
463
|
-
\t\t\t\t\
|
|
464
|
-
\t\t\t\t\
|
|
465
|
-
\t\t\t\t
|
|
466
|
-
\t\t\t\
|
|
467
|
-
\t\t\t\t\
|
|
468
|
-
\t\t\t\t\
|
|
469
|
-
\t\t\t\t\
|
|
470
|
-
\t\t\t\t\t\
|
|
471
|
-
\t\t\t\t\t\
|
|
472
|
-
\t\t\t\t\t\
|
|
473
|
-
\t\t\t\t\t\
|
|
474
|
-
\t\t\t\t\t\t\t
|
|
475
|
-
\t\t\t\t\t\t\t"beignet.listener"
|
|
476
|
-
\t\t\t\t\t\t
|
|
477
|
-
\t\t\t\t\t\
|
|
456
|
+
\t\tlet registration: ReturnType<typeof registerListeners> | undefined;
|
|
457
|
+
|
|
458
|
+
\t\treturn {
|
|
459
|
+
\t\t\tasync start() {
|
|
460
|
+
\t\t\t\tregistration = registerListeners(ports.eventBus, listeners, {
|
|
461
|
+
\t\t\t\t\tctx: () =>
|
|
462
|
+
\t\t\t\t\t\tcreateServiceContext({
|
|
463
|
+
\t\t\t\t\t\t\tactor: createServiceActor("beignet-listener"),
|
|
464
|
+
\t\t\t\t\t\t}),
|
|
465
|
+
\t\t\t\t\tonError(error, listener) {
|
|
466
|
+
\t\t\t\t\t\tports.logger.error("Event listener failed", {
|
|
467
|
+
\t\t\t\t\t\t\terror,
|
|
468
|
+
\t\t\t\t\t\t\tlistenerName: listener.name,
|
|
469
|
+
\t\t\t\t\t\t});
|
|
470
|
+
\t\t\t\t\t\tvoid tryReportException({
|
|
471
|
+
\t\t\t\t\t\t\treporter: ports.errorReporter,
|
|
472
|
+
\t\t\t\t\t\t\terror,
|
|
473
|
+
\t\t\t\t\t\t\treportOptions: {
|
|
474
|
+
\t\t\t\t\t\t\t\tlevel: "error",
|
|
475
|
+
\t\t\t\t\t\t\t\tmechanism: "beignet.listener",
|
|
476
|
+
\t\t\t\t\t\t\t\thandled: false,
|
|
477
|
+
\t\t\t\t\t\t\t\ttags: {
|
|
478
|
+
\t\t\t\t\t\t\t\t\t"beignet.kind": "listener",
|
|
479
|
+
\t\t\t\t\t\t\t\t\t"beignet.listener": listener.name,
|
|
480
|
+
\t\t\t\t\t\t\t\t},
|
|
481
|
+
\t\t\t\t\t\t\t\tcontexts: { listener: { name: listener.name } },
|
|
482
|
+
\t\t\t\t\t\t\t},
|
|
483
|
+
\t\t\t\t\t\t});
|
|
478
484
|
\t\t\t\t\t},
|
|
479
485
|
\t\t\t\t});
|
|
486
|
+
\t\t\t\tawait registration.ready;
|
|
480
487
|
\t\t\t},
|
|
481
|
-
\t\t
|
|
482
|
-
|
|
483
|
-
\t\treturn {
|
|
484
|
-
\t\t\tstop() {
|
|
485
|
-
\t\t\t\tunregister();
|
|
488
|
+
\t\t\tasync stop() {
|
|
489
|
+
\t\t\t\tawait registration?.unsubscribe();
|
|
486
490
|
\t\t\t},
|
|
487
491
|
\t\t};
|
|
488
492
|
\t},
|
package/src/mcp.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
import { z } from "zod/v4";
|
|
6
6
|
import { mapApp, projectAppMap } from "./app-map.js";
|
|
7
|
+
import { mapAppChanges } from "./app-map-changes.js";
|
|
7
8
|
import { appMapNodeKinds } from "./app-map-schema.js";
|
|
8
9
|
import { checkApp } from "./check.js";
|
|
9
10
|
import {
|
|
@@ -579,8 +580,21 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
579
580
|
"app_map",
|
|
580
581
|
{
|
|
581
582
|
description:
|
|
582
|
-
"Return a deterministic graph of this Beignet app
|
|
583
|
+
"Return a deterministic graph of this Beignet app, or map the current Git change set to bounded, source-backed concepts with changed=true. Changed mode is report-only and exposes unresolved evidence without claiming verification.",
|
|
583
584
|
inputSchema: {
|
|
585
|
+
changed: z
|
|
586
|
+
.boolean()
|
|
587
|
+
.optional()
|
|
588
|
+
.describe(
|
|
589
|
+
"Map the current Git change set instead of returning the complete app graph.",
|
|
590
|
+
),
|
|
591
|
+
base: z
|
|
592
|
+
.string()
|
|
593
|
+
.min(1)
|
|
594
|
+
.optional()
|
|
595
|
+
.describe(
|
|
596
|
+
"Include committed changes since the merge base of this already-local Git ref. Requires changed=true; Beignet never fetches it.",
|
|
597
|
+
),
|
|
584
598
|
feature: z
|
|
585
599
|
.string()
|
|
586
600
|
.optional()
|
|
@@ -598,6 +612,22 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
598
612
|
},
|
|
599
613
|
async (input) =>
|
|
600
614
|
safeToolResult(async () => {
|
|
615
|
+
if (input.base !== undefined && !input.changed) {
|
|
616
|
+
throw new Error("app_map base requires changed=true.");
|
|
617
|
+
}
|
|
618
|
+
if (
|
|
619
|
+
input.changed &&
|
|
620
|
+
(input.feature !== undefined ||
|
|
621
|
+
input.kinds !== undefined ||
|
|
622
|
+
input.includeDiagnostics !== undefined)
|
|
623
|
+
) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
"app_map changed=true cannot be combined with feature, kinds, or includeDiagnostics.",
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
if (input.changed) {
|
|
629
|
+
return jsonResult(await mapAppChanges({ cwd, base: input.base }));
|
|
630
|
+
}
|
|
601
631
|
const result = await mapApp({ cwd, strict: true });
|
|
602
632
|
return jsonResult(
|
|
603
633
|
projectAppMap(result, {
|
|
@@ -1166,7 +1196,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
|
|
|
1166
1196
|
async function readAppGuidance(cwd: string): Promise<string> {
|
|
1167
1197
|
const guidancePath = await resolveAppGuidancePath(cwd);
|
|
1168
1198
|
if (!guidancePath) {
|
|
1169
|
-
return "# Beignet app guidance\n\nNo app-local AGENTS.md was found. Use
|
|
1199
|
+
return "# Beignet app guidance\n\nNo app-local AGENTS.md was found. Use app_map and explain before editing, prefer Beignet generators, use app_map with changed=true to inspect potential consumers after editing in Git, and then run check.\n";
|
|
1170
1200
|
}
|
|
1171
1201
|
|
|
1172
1202
|
let file: Awaited<ReturnType<typeof open>>;
|