@pikku/inspector 0.12.110 → 0.12.111

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.
@@ -0,0 +1,226 @@
1
+ import * as ts from 'typescript';
2
+ import { relative } from 'node:path';
3
+ import { ErrorCode } from '../error-codes.js';
4
+ /**
5
+ * A function's `disabled`, read from its config: `true`, or a non-empty string
6
+ * saying why. Anything else the build cannot read statically is refused rather
7
+ * than taken as enabled.
8
+ */
9
+ export const readFunctionDisabled = (obj, name, logger) => {
10
+ for (const prop of obj.properties) {
11
+ if (!ts.isPropertyAssignment(prop) ||
12
+ !ts.isIdentifier(prop.name) ||
13
+ prop.name.text !== 'disabled') {
14
+ continue;
15
+ }
16
+ const init = prop.initializer;
17
+ if (init.kind === ts.SyntaxKind.TrueKeyword)
18
+ return {};
19
+ if (init.kind === ts.SyntaxKind.FalseKeyword)
20
+ return null;
21
+ if ((ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) &&
22
+ init.text !== '') {
23
+ return { reason: init.text };
24
+ }
25
+ logger.critical(ErrorCode.INVALID_VALUE, `'${name}' has a \`disabled\` the build cannot read. Use \`true\` or a non-empty string literal.`);
26
+ return {};
27
+ }
28
+ return null;
29
+ };
30
+ const describe = (state, meta) => `'${meta.name}' is disabled${meta.reason ? ` (${meta.reason})` : ''} in ${relative(state.rootDir, meta.sourceFile)}`;
31
+ const position = (state, node) => {
32
+ const sourceFile = node.getSourceFile();
33
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
34
+ return `${relative(state.rootDir, sourceFile.fileName)}:${line + 1}`;
35
+ };
36
+ /** The call a reference sits in, by its callee: `wireHTTP`, `rpc.invoke`. */
37
+ const enclosingCall = (node) => {
38
+ for (let current = node.parent; current; current = current.parent) {
39
+ if (ts.isCallExpression(current) &&
40
+ (ts.isIdentifier(current.expression) ||
41
+ ts.isPropertyAccessExpression(current.expression))) {
42
+ return current.expression.getText();
43
+ }
44
+ }
45
+ return undefined;
46
+ };
47
+ /**
48
+ * Records a function declared with `disabled`, in place of everything
49
+ * addFunctions would otherwise record. A function defined inline in what
50
+ * uses it is referenced by that, so it is refused instead.
51
+ */
52
+ export const recordDisabledFunction = (logger, state, node, pikkuFuncId, meta) => {
53
+ const parent = node.parent;
54
+ if (ts.isVariableDeclaration(parent) && parent.initializer === node) {
55
+ state.functions.disabled[pikkuFuncId] = meta;
56
+ return;
57
+ }
58
+ const call = enclosingCall(node);
59
+ logger.critical(ErrorCode.DISABLED_FUNCTION_REFERENCED, `A function declared \`disabled\` is defined inline at ${position(state, node)}` +
60
+ (call ? ` inside ${call}(...)` : '') +
61
+ `, which still uses it. Remove what uses it, or remove \`disabled\`.`);
62
+ };
63
+ const byLocalName = new WeakMap();
64
+ /** Disabled functions by the name code reaches them by: the export, else the name. */
65
+ const disabledByLocalName = (state) => {
66
+ let index = byLocalName.get(state);
67
+ if (index)
68
+ return index;
69
+ index = new Map();
70
+ for (const meta of Object.values(state.functions.disabled)) {
71
+ const local = meta.exportedName ?? meta.name;
72
+ index.set(local, [...(index.get(local) ?? []), meta]);
73
+ }
74
+ byLocalName.set(state, index);
75
+ return index;
76
+ };
77
+ /** Whether a node is inside the definition of a disabled function, whose body is dead too. */
78
+ const isInsideDisabled = (state, node) => {
79
+ const fileName = node.getSourceFile().fileName;
80
+ const index = disabledByLocalName(state);
81
+ for (let current = node.parent; current; current = current.parent) {
82
+ if (ts.isVariableDeclaration(current) &&
83
+ ts.isIdentifier(current.name) &&
84
+ index.get(current.name.text)?.some((meta) => meta.sourceFile === fileName)) {
85
+ return true;
86
+ }
87
+ }
88
+ return false;
89
+ };
90
+ const isNotAReference = (node) => {
91
+ const parent = node.parent;
92
+ if (ts.isImportSpecifier(parent) ||
93
+ ts.isExportSpecifier(parent) ||
94
+ ts.isImportClause(parent) ||
95
+ ts.isNamespaceImport(parent) ||
96
+ ts.isTypeQueryNode(parent) ||
97
+ ts.isQualifiedName(parent)) {
98
+ return true;
99
+ }
100
+ if (ts.isShorthandPropertyAssignment(parent))
101
+ return false;
102
+ return 'name' in parent && parent.name === node;
103
+ };
104
+ const resolveDeclaration = (node, checker) => {
105
+ let symbol = ts.isShorthandPropertyAssignment(node.parent)
106
+ ? checker.getShorthandAssignmentValueSymbol(node.parent)
107
+ : checker.getSymbolAtLocation(node);
108
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
109
+ symbol = checker.getAliasedSymbol(symbol);
110
+ }
111
+ return symbol?.valueDeclaration ?? symbol?.declarations?.[0];
112
+ };
113
+ /** `rpc.invoke('x')` / `rpc.mesh('x')`, seen through `!` and parentheses. */
114
+ const invokedRpcName = (node) => {
115
+ const callee = node.expression;
116
+ if (!ts.isPropertyAccessExpression(callee) ||
117
+ (callee.name.text !== 'invoke' && callee.name.text !== 'mesh')) {
118
+ return undefined;
119
+ }
120
+ let receiver = callee.expression;
121
+ while (ts.isNonNullExpression(receiver) ||
122
+ ts.isParenthesizedExpression(receiver)) {
123
+ receiver = receiver.expression;
124
+ }
125
+ const isRpc = ts.isIdentifier(receiver)
126
+ ? receiver.text === 'rpc'
127
+ : ts.isPropertyAccessExpression(receiver) && receiver.name.text === 'rpc';
128
+ const [first] = node.arguments;
129
+ if (!isRpc ||
130
+ !first ||
131
+ !(ts.isStringLiteral(first) || ts.isNoSubstitutionTemplateLiteral(first))) {
132
+ return undefined;
133
+ }
134
+ return first.text;
135
+ };
136
+ /**
137
+ * Whether a node sits in a generated file. This run rewrites those from its own
138
+ * state, which leaves disabled functions out, so a reference there is the last
139
+ * run's and is about to go.
140
+ */
141
+ const isGenerated = (node) => {
142
+ const fileName = node.getSourceFile().fileName;
143
+ return fileName.endsWith('.gen.ts') || fileName.endsWith('.gen.js');
144
+ };
145
+ /**
146
+ * PKU562: anything that still references a disabled function — a wiring, an
147
+ * agent's tools, an `rpc.invoke` by name, any other code. Runs in the routes
148
+ * sweep, after every function is registered, so it sees the whole set.
149
+ */
150
+ export const checkDisabledReferences = (logger, node, checker, state) => {
151
+ if (Object.keys(state.functions.disabled).length === 0)
152
+ return;
153
+ if (isGenerated(node))
154
+ return;
155
+ if (ts.isCallExpression(node)) {
156
+ const invoked = invokedRpcName(node);
157
+ if (invoked === undefined)
158
+ return;
159
+ const meta = Object.values(state.functions.disabled).find((m) => m.name === invoked);
160
+ if (!meta || isInsideDisabled(state, node))
161
+ return;
162
+ logger.critical(ErrorCode.DISABLED_FUNCTION_REFERENCED, `${describe(state, meta)}, but ${node.expression.getText()}('${invoked}') calls it at ${position(state, node)}. ` +
163
+ `Remove the call, or remove \`disabled\`.`);
164
+ return;
165
+ }
166
+ if (!ts.isIdentifier(node))
167
+ return;
168
+ const candidates = disabledByLocalName(state).get(node.text);
169
+ if (!candidates || isNotAReference(node))
170
+ return;
171
+ const decl = resolveDeclaration(node, checker);
172
+ if (!decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) {
173
+ return;
174
+ }
175
+ const declFile = decl.getSourceFile().fileName;
176
+ const declName = decl.name.text;
177
+ const meta = candidates.find((m) => m.sourceFile === declFile && (m.exportedName ?? m.name) === declName);
178
+ if (!meta || isInsideDisabled(state, node))
179
+ return;
180
+ const call = enclosingCall(node);
181
+ logger.critical(ErrorCode.DISABLED_FUNCTION_REFERENCED, `${describe(state, meta)}, but ` +
182
+ (call ? `${call}(...) references it` : 'it is referenced') +
183
+ ` at ${position(state, node)}. Remove the reference, or remove \`disabled\`.`);
184
+ };
185
+ /** Every `rpcName` in a workflow's meta, wherever in its steps it sits. */
186
+ const collectRpcNames = (value, into) => {
187
+ if (Array.isArray(value)) {
188
+ for (const item of value)
189
+ collectRpcNames(item, into);
190
+ return;
191
+ }
192
+ if (!value || typeof value !== 'object')
193
+ return;
194
+ for (const [key, child] of Object.entries(value)) {
195
+ if (key === 'rpcName' && typeof child === 'string')
196
+ into.add(child);
197
+ else
198
+ collectRpcNames(child, into);
199
+ }
200
+ };
201
+ /**
202
+ * PKU562 for workflow steps, which name the function they call as a string
203
+ * the workflow meta already holds. Runs once, after the routes sweep.
204
+ */
205
+ export const checkDisabledWorkflowSteps = (logger, state) => {
206
+ const disabled = Object.values(state.functions.disabled);
207
+ if (disabled.length === 0)
208
+ return;
209
+ const sections = [
210
+ ['workflow', state.workflows.meta],
211
+ ['workflow graph', state.workflows.graphMeta],
212
+ ];
213
+ for (const [kind, section] of sections) {
214
+ for (const [workflowName, meta] of Object.entries(section)) {
215
+ const names = new Set();
216
+ collectRpcNames(meta, names);
217
+ for (const rpcName of [...names].sort()) {
218
+ const target = disabled.find((m) => m.name === rpcName);
219
+ if (!target)
220
+ continue;
221
+ logger.critical(ErrorCode.DISABLED_FUNCTION_REFERENCED, `${describe(state, target)}, but ${kind} '${workflowName}' calls it as a step. ` +
222
+ `Remove the step, or remove \`disabled\`.`);
223
+ }
224
+ }
225
+ }
226
+ };
@@ -46,6 +46,12 @@ export declare enum ErrorCode {
46
46
  REMOTE_ADDON_SDK_CORE_MISMATCH = "PKU351",
47
47
  REMOTE_ADDON_SDK_INTEGRITY_MISMATCH = "PKU352",
48
48
  REMOTE_ADDON_PACKAGE_INVALID = "PKU353",
49
+ ADDON_DEPENDENCY_VERSION_MISMATCH = "PKU354",
50
+ ADDON_USES_NOT_DECLARED = "PKU355",
51
+ ADDON_DEPENDENCY_INVALID = "PKU356",
52
+ STUDIO_SCREEN_PATH_DUPLICATE = "PKU357",
53
+ STUDIO_MESSAGE_KEY_MISSING = "PKU358",
54
+ STUDIO_BADGE_FUNCTION_NOT_FOUND = "PKU359",
49
55
  DB_COLUMN_NAME_TYPE_CONTRADICTION = "PKU480",
50
56
  DB_JSON_COLUMN_UNTYPED = "PKU481",
51
57
  DB_FORMAT_ON_NON_STRING = "PKU482",
@@ -56,6 +62,9 @@ export declare enum ErrorCode {
56
62
  SCHEMA_REFERENCE_UNRESOLVED = "PKU463",
57
63
  INLINE_SCHEMA = "PKU489",
58
64
  FUNCTION_METADATA_NOT_FOUND = "PKU559",
65
+ DISABLED_FUNCTION_REFERENCED = "PKU562",
66
+ UNUSED_FUNCTION = "PKU563",
67
+ FACTORY_NOT_A_DIRECT_CALL = "PKU564",
59
68
  DUPLICATE_AUTH_DEFINITION = "PKU581",
60
69
  AUTH_NOT_EXPORTED = "PKU582",
61
70
  DUPLICATE_VIRTUAL_USERS_DEFINITION = "PKU585",
@@ -49,6 +49,12 @@ export var ErrorCode;
49
49
  ErrorCode["REMOTE_ADDON_SDK_CORE_MISMATCH"] = "PKU351";
50
50
  ErrorCode["REMOTE_ADDON_SDK_INTEGRITY_MISMATCH"] = "PKU352";
51
51
  ErrorCode["REMOTE_ADDON_PACKAGE_INVALID"] = "PKU353";
52
+ ErrorCode["ADDON_DEPENDENCY_VERSION_MISMATCH"] = "PKU354";
53
+ ErrorCode["ADDON_USES_NOT_DECLARED"] = "PKU355";
54
+ ErrorCode["ADDON_DEPENDENCY_INVALID"] = "PKU356";
55
+ ErrorCode["STUDIO_SCREEN_PATH_DUPLICATE"] = "PKU357";
56
+ ErrorCode["STUDIO_MESSAGE_KEY_MISSING"] = "PKU358";
57
+ ErrorCode["STUDIO_BADGE_FUNCTION_NOT_FOUND"] = "PKU359";
52
58
  // Database schema codegen warnings
53
59
  ErrorCode["DB_COLUMN_NAME_TYPE_CONTRADICTION"] = "PKU480";
54
60
  ErrorCode["DB_JSON_COLUMN_UNTYPED"] = "PKU481";
@@ -62,6 +68,9 @@ export var ErrorCode;
62
68
  ErrorCode["INLINE_SCHEMA"] = "PKU489";
63
69
  // Function errors
64
70
  ErrorCode["FUNCTION_METADATA_NOT_FOUND"] = "PKU559";
71
+ ErrorCode["DISABLED_FUNCTION_REFERENCED"] = "PKU562";
72
+ ErrorCode["UNUSED_FUNCTION"] = "PKU563";
73
+ ErrorCode["FACTORY_NOT_A_DIRECT_CALL"] = "PKU564";
65
74
  // Auth errors
66
75
  ErrorCode["DUPLICATE_AUTH_DEFINITION"] = "PKU581";
67
76
  ErrorCode["AUTH_NOT_EXPORTED"] = "PKU582";
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { inspect, analyzeProgram } from './inspector.js';
2
2
  export { getInitialInspectorState } from './initial-state.js';
3
3
  export { finalizeInspectorState } from './finalize.js';
4
4
  export type { TypesMap } from './types-map.js';
5
- export type { AddonConfig, AuthDefinition, CredentialOverrideMeta, ScreensManifestMeta, AddonScreenMeta, AddonContributorRef, AddonContributorsMeta, InspectorDiagnostic, InspectorFilters, InspectorHTTPState, InspectorLogger, InspectorMiddlewareState, InspectorOptions, InspectorPermissionState, InspectorServiceTypes, InspectorState, MetaInputTypes, MiddlewareGroupMeta, MiddlewareRegistration, SchemaRef, } from './types.js';
5
+ export type { AddonConfig, AuthDefinition, CredentialOverrideMeta, StudioMeta, StudioScreenMeta, StudioNavMeta, AddonContributorRef, AddonContributorsMeta, InspectorDiagnostic, InspectorFilters, InspectorHTTPState, InspectorLogger, InspectorMiddlewareState, InspectorOptions, InspectorPermissionState, InspectorServiceTypes, InspectorState, MetaInputTypes, MiddlewareGroupMeta, MiddlewareRegistration, SchemaRef, } from './types.js';
6
6
  export type { FilesAndMethodsErrors } from './utils/get-files-and-methods.js';
7
7
  export { ErrorCode } from './error-codes.js';
8
8
  export type { DiagnosticSeverity, CodedDiagnostic } from './error-codes.js';
@@ -16,6 +16,7 @@ export { incompatibleServicesFor, resolveDeployTarget, } from './utils/resolve-d
16
16
  export { generateCustomTypes, customTypeNames, sanitizeTypeName, } from './utils/custom-types-generator.js';
17
17
  export type { GenerateCustomTypesOptions } from './utils/custom-types-generator.js';
18
18
  export { unresolvedSchemaReferences } from './utils/post-process.js';
19
+ export { findUnusedFunctions, type UnusedFunction, } from './utils/find-unused-functions.js';
19
20
  export type { SurfaceUsageCounts } from './utils/extract-surface-usage.js';
20
21
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
21
22
  export type { ContractEntry, VersionHashEntry, VersionValidateError, VersionManifest, VersionManifestEntry, } from './utils/contract-hashes.js';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { filterInspectorState } from './utils/filter-inspector-state.js';
9
9
  export { incompatibleServicesFor, resolveDeployTarget, } from './utils/resolve-deploy-target.js';
10
10
  export { generateCustomTypes, customTypeNames, sanitizeTypeName, } from './utils/custom-types-generator.js';
11
11
  export { unresolvedSchemaReferences } from './utils/post-process.js';
12
+ export { findUnusedFunctions, } from './utils/find-unused-functions.js';
12
13
  export { createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
13
14
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
14
15
  export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/deserialize-dsl-workflow.js';
@@ -31,6 +31,7 @@ export function getInitialInspectorState(rootDir) {
31
31
  files: new Map(),
32
32
  approvalDescriptions: {},
33
33
  dynamicImportIds: new Set(),
34
+ disabled: {},
34
35
  },
35
36
  http: {
36
37
  metaInputTypes: new Map(),
package/dist/inspector.js CHANGED
@@ -8,6 +8,7 @@ import { createNestedProjectFilter } from './utils/nested-project-filter.js';
8
8
  import { toProgramPath } from './utils/program-path.js';
9
9
  import { resolveLatestVersions } from './utils/resolve-versions.js';
10
10
  import { validateWebhookSourceTriggers } from './add/add-trigger.js';
11
+ import { validatePikkuStudio } from './add/add-pikku-studio.js';
11
12
  import { generateAllSchemas } from './utils/schema-generator.js';
12
13
  import { extractSecretUsage } from './utils/extract-secret-usage.js';
13
14
  import { accumulateSurfaceUsage, surfaceUsageArea, } from './utils/extract-surface-usage.js';
@@ -15,6 +16,7 @@ import { loadAddonFunctionsMeta } from './utils/load-addon-functions-meta.js';
15
16
  import { resolveServiceTypes } from './utils/resolve-service-types.js';
16
17
  import { finalizeInspectorState } from './finalize.js';
17
18
  import { getInitialInspectorState } from './initial-state.js';
19
+ import { checkDisabledWorkflowSteps } from './add/check-disabled-references.js';
18
20
  export { getInitialInspectorState };
19
21
  /**
20
22
  * The path mappings the inspector's program has to share with the project.
@@ -140,7 +142,9 @@ export const analyzeProgram = async (logger, routeFiles, options = {}) => {
140
142
  ts.forEachChild(sourceFile, (child) => visitRoutes(logger, checker, child, state, sourceOptions));
141
143
  }
142
144
  logger.debug(`Visit routes phase completed in ${(performance.now() - startRoutes).toFixed(0)}ms`);
145
+ checkDisabledWorkflowSteps(logger, state);
143
146
  validateWebhookSourceTriggers(logger, state);
147
+ validatePikkuStudio(logger, state);
144
148
  resolveLatestVersions(state, logger);
145
149
  if (options.schemaConfig) {
146
150
  const startSchemas = performance.now();
package/dist/lite.d.ts CHANGED
@@ -19,6 +19,7 @@ export { filterInspectorState } from './utils/filter-inspector-state.js';
19
19
  export { incompatibleServicesFor, resolveDeployTarget, } from './utils/resolve-deploy-target.js';
20
20
  export { customTypeNames, generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
21
21
  export { unresolvedSchemaReferences } from './utils/post-process.js';
22
+ export { findUnusedFunctions, type UnusedFunction, } from './utils/find-unused-functions.js';
22
23
  export { computeContractHash, createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
23
24
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
24
25
  export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/deserialize-dsl-workflow.js';
@@ -27,3 +28,5 @@ export { resolveCoreType } from './utils/resolve-core-type.js';
27
28
  export { addonResolutionDirs, createAddonResolver, } from './utils/addon-resolution.js';
28
29
  export { ADDONS_LOCK_FILE, REMOTE_SDK_DIR, SDK_INTEGRITY_MARKER, checkLockedSdk, emptyAddonsLock, parseRemoteAddonPackage, readAddonsLock, readSdkManifest, remoteSdkDir, serializeAddonsLock, } from './utils/remote-addon-sdk.js';
29
30
  export type { AddonsLock, AddonsLockEntry, RemoteAddonSdkManifest, RemoteAddonPackageSpec, LockedSdk, } from './utils/remote-addon-sdk.js';
31
+ export { declaredAddonNames, findInstalledPackageDir, parseAddonDependencies, readAddonDependencies, } from './utils/addon-dependencies.js';
32
+ export type { AddonDependency, AddonDependencies, } from './utils/addon-dependencies.js';
package/dist/lite.js CHANGED
@@ -18,6 +18,7 @@ export { filterInspectorState } from './utils/filter-inspector-state.js';
18
18
  export { incompatibleServicesFor, resolveDeployTarget, } from './utils/resolve-deploy-target.js';
19
19
  export { customTypeNames, generateCustomTypes, sanitizeTypeName, } from './utils/custom-types-generator.js';
20
20
  export { unresolvedSchemaReferences } from './utils/post-process.js';
21
+ export { findUnusedFunctions, } from './utils/find-unused-functions.js';
21
22
  export { computeContractHash, createEmptyManifest, serializeManifest, } from './utils/contract-hashes.js';
22
23
  export { serializeMCPJson } from './utils/serialize-mcp-json.js';
23
24
  export { deserializeDslWorkflow, deserializeGraphWorkflow, deserializeAllDslWorkflows, } from './utils/workflow/dsl/deserialize-dsl-workflow.js';
@@ -25,3 +26,4 @@ export { resolveFunctionMeta } from './utils/resolve-function-meta.js';
25
26
  export { resolveCoreType } from './utils/resolve-core-type.js';
26
27
  export { addonResolutionDirs, createAddonResolver, } from './utils/addon-resolution.js';
27
28
  export { ADDONS_LOCK_FILE, REMOTE_SDK_DIR, SDK_INTEGRITY_MARKER, checkLockedSdk, emptyAddonsLock, parseRemoteAddonPackage, readAddonsLock, readSdkManifest, remoteSdkDir, serializeAddonsLock, } from './utils/remote-addon-sdk.js';
29
+ export { declaredAddonNames, findInstalledPackageDir, parseAddonDependencies, readAddonDependencies, } from './utils/addon-dependencies.js';
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type * as ts from 'typescript';
2
2
  import type { SecretUsage } from './utils/extract-secret-usage.js';
3
+ import type { AddonDependency } from './utils/addon-dependencies.js';
3
4
  import type { SurfaceUsageCounts } from './utils/extract-surface-usage.js';
4
5
  import type { ChannelMessageMeta, ChannelsMeta } from '@pikku/core/channel';
5
6
  import type { GatewaysMeta } from '@pikku/core/gateway';
@@ -105,6 +106,19 @@ export interface InspectorFunctionState {
105
106
  * inspection pass — deliberately not part of the serialized function meta.
106
107
  */
107
108
  dynamicImportIds: Set<string>;
109
+ /**
110
+ * Functions declared with `disabled`, keyed by pikkuFuncId. They are in no
111
+ * other part of the state: nothing is generated for them, and a reference
112
+ * to one is PKU562.
113
+ */
114
+ disabled: Record<string, DisabledFunctionMeta>;
115
+ }
116
+ export interface DisabledFunctionMeta {
117
+ name: string;
118
+ sourceFile: string;
119
+ exportedName?: string;
120
+ /** The string `disabled` was given, when it was not just `true`. */
121
+ reason?: string;
108
122
  }
109
123
  export interface InspectorChannelState {
110
124
  meta: ChannelsMeta;
@@ -517,11 +531,22 @@ export type CredentialOverrideMeta = string | {
517
531
  name?: string;
518
532
  mode?: 'singleton' | 'wire';
519
533
  };
520
- /** A screen of an addon as `defineScreens` declares it, read statically. */
521
- export type AddonScreenMeta = {
534
+ /** A screen's nav entry as `pikkuStudio` declares it, read statically. */
535
+ export type StudioNavMeta = {
536
+ area: 'rail' | 'team' | 'project' | 'account';
537
+ icon: string;
538
+ order: number;
539
+ badge?: string;
540
+ };
541
+ /** A screen of an addon as `pikkuStudio` declares it, read statically. */
542
+ export type StudioScreenMeta = {
522
543
  path: string;
523
544
  title: string;
524
- nav?: boolean;
545
+ scope: 'studio' | 'team' | 'project';
546
+ nav?: StudioNavMeta;
547
+ replaces?: boolean;
548
+ needsApp?: boolean;
549
+ layout?: 'bare';
525
550
  scopes?: string[];
526
551
  } & ({
527
552
  component: string;
@@ -530,15 +555,14 @@ export type AddonScreenMeta = {
530
555
  app: string;
531
556
  component?: undefined;
532
557
  });
533
- export type ScreensManifestMeta = {
558
+ export type StudioMeta = {
534
559
  title: string;
535
- icon?: string;
536
- /** The source file whose `defineScreens` call declared this manifest. */
560
+ /** The source file whose `pikkuStudio` call declared this manifest. */
537
561
  file: string;
538
- screens: AddonScreenMeta[];
562
+ screens: StudioScreenMeta[];
539
563
  /**
540
564
  * The addon's role: every scope its own functions require, derived by
541
- * `pikku all` and never declared. Absent from the raw `defineScreens` read.
565
+ * `pikku all` and never declared. Absent from the raw `pikkuStudio` read.
542
566
  * On a wired instance it also carries the host's `wireAddon` scopes, which
543
567
  * the runner demands of every function in the package.
544
568
  */
@@ -562,10 +586,10 @@ export interface AddonContributorsMeta {
562
586
  contributors: AddonContributorRef[];
563
587
  }
564
588
  export interface InspectorState {
565
- /** Set when this package calls `defineScreens`: the screens it ships. */
566
- screensManifest?: ScreensManifestMeta;
567
- /** The screens of the addons this project wires with `ui: true`, by namespace. */
568
- addonScreens?: Record<string, ScreensManifestMeta>;
589
+ /** Set when this package calls `pikkuStudio`: the screens it ships. */
590
+ studio?: StudioMeta;
591
+ /** The `pikkuStudio` metadata of the addons this project wires with `ui: true`, by namespace. */
592
+ addonStudio?: Record<string, StudioMeta>;
569
593
  /**
570
594
  * Platform service contributors each wired, non-remote addon declares in its
571
595
  * `package.json` under `pikku.contributors`, by namespace. `module` resolves
@@ -688,8 +712,20 @@ export interface InspectorState {
688
712
  /** Declared with `ui: true`: the package must ship screens, and the host mounts them. */
689
713
  ui?: boolean;
690
714
  rpcEndpoint?: string;
691
- /** The other addons this instance may call: package -> the name it is wired under. */
715
+ /**
716
+ * The other addons this instance may call: the name the addon calls
717
+ * (its declared alias, or the package name) -> the name the target is
718
+ * wired under in this app.
719
+ */
692
720
  uses?: Record<string, string>;
721
+ /**
722
+ * The names the addon's package.json declares under `pikku.uses`, in
723
+ * either form. Read when its meta loads; absent when it declares
724
+ * nothing, which leaves `uses` unchecked against a declaration.
725
+ */
726
+ declaredUses?: string[];
727
+ /** The `{ alias: 'package@range' }` form of `pikku.uses`, per alias. */
728
+ dependencies?: AddonDependency[];
693
729
  /**
694
730
  * `true` offers every function the addon declared `mcp: true`; a list
695
731
  * names the functions to offer, whatever the addon declared. Absent
@@ -0,0 +1,43 @@
1
+ /**
2
+ * One addon an addon calls: the alias its code calls it by
3
+ * (`rpc.invoke('<alias>:<fn>')`) and the package and semver range it was
4
+ * written against.
5
+ */
6
+ export type AddonDependency = {
7
+ alias: string;
8
+ package: string;
9
+ range: string;
10
+ };
11
+ /**
12
+ * What an addon's package.json declares under `pikku.uses`.
13
+ *
14
+ * - `none`: nothing. The wiring's `uses` is not checked against a declaration.
15
+ * - `list`: the older form, a list of names the addon calls. The name is also
16
+ * the package the wiring must map it to.
17
+ * - `map`: `{ "<alias>": "<package>@<range>" }`. `invalid` holds a message for
18
+ * each entry that could not be read; those entries are not in `dependencies`.
19
+ *
20
+ * knowledge: decisions/an-addon-declares-the-addons-it-calls.md
21
+ */
22
+ export type AddonDependencies = {
23
+ form: 'none';
24
+ } | {
25
+ form: 'list';
26
+ names: string[];
27
+ } | {
28
+ form: 'map';
29
+ dependencies: AddonDependency[];
30
+ invalid: string[];
31
+ };
32
+ /** The names a wiring's `uses` may map: the declared aliases, or the listed names. */
33
+ export declare const declaredAddonNames: (declared: AddonDependencies) => string[];
34
+ /** Reads the `pikku.uses` value of an addon's package.json. */
35
+ export declare const parseAddonDependencies: (uses: unknown) => AddonDependencies;
36
+ /** The `pikku.uses` an addon's package.json declares. `none` when there is no readable package.json. */
37
+ export declare const readAddonDependencies: (packageDir: string) => AddonDependencies;
38
+ /**
39
+ * The directory `packageName` is installed in, looking in `node_modules` from
40
+ * each of `dirs` upwards, as Node's resolution would. Null when it is not
41
+ * installed.
42
+ */
43
+ export declare const findInstalledPackageDir: (dirs: string[], packageName: string) => string | null;
@@ -0,0 +1,91 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { dirname, join, parse } from 'path';
3
+ import { parseRemoteAddonPackage } from './remote-addon-sdk.js';
4
+ /** An alias becomes a namespace prefix and part of a generated type name. */
5
+ const ALIAS = /^[A-Za-z][A-Za-z0-9_-]*$/;
6
+ /** The names a wiring's `uses` may map: the declared aliases, or the listed names. */
7
+ export const declaredAddonNames = (declared) => declared.form === 'list'
8
+ ? declared.names
9
+ : declared.form === 'map'
10
+ ? declared.dependencies.map((d) => d.alias)
11
+ : [];
12
+ /** Reads the `pikku.uses` value of an addon's package.json. */
13
+ export const parseAddonDependencies = (uses) => {
14
+ if (uses === undefined || uses === null)
15
+ return { form: 'none' };
16
+ if (Array.isArray(uses)) {
17
+ return {
18
+ form: 'list',
19
+ names: uses.filter((u) => typeof u === 'string'),
20
+ };
21
+ }
22
+ if (typeof uses !== 'object') {
23
+ return {
24
+ form: 'map',
25
+ dependencies: [],
26
+ invalid: [
27
+ `pikku.uses must be an object of { "<alias>": "<package>@<range>" }, got ${JSON.stringify(uses)}.`,
28
+ ],
29
+ };
30
+ }
31
+ const dependencies = [];
32
+ const invalid = [];
33
+ for (const [alias, spec] of Object.entries(uses)) {
34
+ if (!ALIAS.test(alias)) {
35
+ invalid.push(`pikku.uses names '${alias}', which is not a valid alias. An alias starts with a letter and holds only letters, digits, '-' and '_'.`);
36
+ continue;
37
+ }
38
+ if (typeof spec !== 'string') {
39
+ invalid.push(`pikku.uses.${alias} must be a string '<package>@<range>', got ${JSON.stringify(spec)}.`);
40
+ continue;
41
+ }
42
+ let parsed;
43
+ try {
44
+ parsed = parseRemoteAddonPackage(spec);
45
+ }
46
+ catch (e) {
47
+ invalid.push(`pikku.uses.${alias}: ${e instanceof Error ? e.message : String(e)}`);
48
+ continue;
49
+ }
50
+ if (!parsed.range) {
51
+ invalid.push(`pikku.uses.${alias} is '${spec}', which has no version range. Write '${parsed.name}@^1.0.0': the app that wires this addon is checked against it.`);
52
+ continue;
53
+ }
54
+ dependencies.push({ alias, package: parsed.name, range: parsed.range });
55
+ }
56
+ return { form: 'map', dependencies, invalid };
57
+ };
58
+ /** The `pikku.uses` an addon's package.json declares. `none` when there is no readable package.json. */
59
+ export const readAddonDependencies = (packageDir) => {
60
+ let pkg;
61
+ try {
62
+ pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf-8'));
63
+ }
64
+ catch {
65
+ return { form: 'none' };
66
+ }
67
+ return parseAddonDependencies(pkg?.pikku?.uses);
68
+ };
69
+ /**
70
+ * The directory `packageName` is installed in, looking in `node_modules` from
71
+ * each of `dirs` upwards, as Node's resolution would. Null when it is not
72
+ * installed.
73
+ */
74
+ export const findInstalledPackageDir = (dirs, packageName) => {
75
+ for (const start of dirs) {
76
+ const root = parse(start).root;
77
+ let dir = start;
78
+ while (dir) {
79
+ const candidate = join(dir, 'node_modules', packageName);
80
+ if (existsSync(join(candidate, 'package.json')))
81
+ return candidate;
82
+ if (dir === root)
83
+ break;
84
+ const parent = dirname(dir);
85
+ if (parent === dir)
86
+ break;
87
+ dir = parent;
88
+ }
89
+ }
90
+ return null;
91
+ };
@@ -0,0 +1,20 @@
1
+ import type { InspectorState } from '../types.js';
2
+ export interface UnusedFunction {
3
+ pikkuFuncId: string;
4
+ name?: string;
5
+ sourceFile?: string;
6
+ }
7
+ /**
8
+ * The app's own functions that nothing uses: not wired to any transport, not
9
+ * exposed, published or offered to MCP, and not called by name. Addon
10
+ * functions are never judged: an addon project's own functions are all addon
11
+ * functions, and a consumer's state holds no addon functions in
12
+ * `functions.meta`. Neither are scenarios, inline wiring functions or disabled
13
+ * functions. A workflow is a function like any other: it is used when it is
14
+ * wired, exposed, or started by a literal name — `rpc.startWorkflow`,
15
+ * `workflowService.startWorkflow` / `runToCompletion`, `rpc.invoke`, another
16
+ * workflow's step, or the generated client helpers.
17
+ */
18
+ export declare const findUnusedFunctions: (state: InspectorState, { addon }?: {
19
+ addon?: boolean;
20
+ }) => UnusedFunction[];