@standardagents/code-plugin-sdk 0.0.0-stub.0 → 1.0.0-alpha.1

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/src/index.mjs ADDED
@@ -0,0 +1,13 @@
1
+ import { ensure, identifier, validateManifest, PluginError } from './manifest.mjs'
2
+ import { COLLECTION_FILE, resolveCollection, validateCollection } from './collection.mjs'
3
+ import { SDK_PACKAGE, checkPackageForPublish, lockfileRequirement } from './publish.mjs'
4
+
5
+ export { validateManifest, PluginError }
6
+ export { COLLECTION_FILE, resolveCollection, validateCollection }
7
+ export { SDK_PACKAGE, checkPackageForPublish, lockfileRequirement }
8
+
9
+ export function definePlugin(definition) {
10
+ ensure(definition && identifier(definition.id) && typeof definition.activate === 'function',
11
+ 'invalid_plugin', 'definePlugin requires an id and activate function')
12
+ return Object.freeze({ id: definition.id, activate: definition.activate })
13
+ }
@@ -0,0 +1,39 @@
1
+ import type { ContributionKey, SurfaceContent, Producer, Operation, Json, PluginManifest, PluginContext, PluginDefinition, RequestOptions, Condition } from './index.js';
2
+ export * from './index.js';
3
+ export const RUNNER_PROTOCOL_VERSION: 1;
4
+ export const PLUGIN_API_VERSION: 1;
5
+ export const PLUGIN_PEER_CAPABILITY: 'plugins.v1';
6
+ export const LIMITS: Readonly<Record<string, number>>;
7
+ export interface PreparedPlugin {
8
+ root: string;
9
+ entry: string;
10
+ manifest: PluginManifest;
11
+ manifestDigest: string;
12
+ }
13
+ export type HostOperation =
14
+ { op: 'runner.start' | 'runner.reload'; args: { root: string; manifestDigest: string; capabilities: string[]; linked?: boolean } } |
15
+ { op: 'runner.stop' | 'runner.diagnose' | 'runner.ping'; args: Record<string, never> } |
16
+ { op: 'runtime.invoke'; args: { subscriptionId: string; event: Json } } |
17
+ { op: 'runtime.visibility'; args: { conditions: Condition[] } };
18
+ export type Envelope = { version: 1; producer: Producer } & (
19
+ { kind: 'request'; id: string; operation: Operation | HostOperation; timeoutMs: number } |
20
+ { kind: 'response'; id: string; result: { ok: true; value: Json } | { ok: false; error: { code: string; message: string } } } |
21
+ { kind: 'cancel'; id: string } |
22
+ { kind: 'surface'; key: ContributionKey; sequence: string; content: SurfaceContent | null }
23
+ );
24
+ export interface Clock { now(): number; setTimeout(callback: () => void | Promise<void>, ms: number): unknown; clearTimeout(id: unknown): void }
25
+ export interface Runtime {
26
+ context: PluginContext;
27
+ receive(frame: Envelope): void;
28
+ activate(definition: PluginDefinition): Promise<void>;
29
+ dispose(): Promise<void>;
30
+ }
31
+ export function createRuntime(options: { manifest: PluginManifest; producer: Producer; send(frame: Envelope): void; clock?: Clock; instanceId?: string; onError?(error: Error): void }): Runtime;
32
+ export function authorize(operation: Operation, capabilities: string[]): void;
33
+ export function validateEnvelope(frame: unknown): Envelope;
34
+ export class RpcPeer {
35
+ constructor(options: { producer: Producer; send(frame: Envelope): void; clock?: Clock; idPrefix?: string; onRequest?(operation: Operation | HostOperation, options: { signal: AbortSignal; timeoutMs: number }): Promise<Json> | Json; onSurface?(frame: Envelope): void; onError?(error: Error): void });
36
+ request(operation: Operation | HostOperation, options?: RequestOptions): Promise<Json>;
37
+ receive(frame: Envelope): void;
38
+ dispose(): void;
39
+ }
@@ -0,0 +1,5 @@
1
+ // Re-exports for the plugin runner, which imports this file by relative path.
2
+ // The package does not export this module; plugin code uses the public entry.
3
+ export * from './manifest.mjs'
4
+ export * from './protocol.mjs'
5
+ export { createRuntime } from './runtime.mjs'
@@ -0,0 +1,86 @@
1
+ export const CAPABILITIES = Object.freeze(['surfaces', 'events', 'hooks', 'panes', 'projects',
2
+ 'notifications', 'url', 'fetch', 'secrets', 'webhook'])
3
+ export const SURFACE_KINDS = Object.freeze(['section', 'slot', 'badge', 'panel', 'overlay',
4
+ 'menu', 'command', 'key', 'link'])
5
+ export const ANCHORS = Object.freeze(['plugins', 'machine.before', 'machine.after',
6
+ 'project.before', 'project.after', 'pane.header', 'pane.footer',
7
+ 'account', 'machine', 'project', 'pane', 'section'])
8
+ export const LIMITS = Object.freeze({ manifestBytes: 65536, frameBytes: 262144,
9
+ pendingRequests: 128, subscriptions: 256, schedules: 128, contributions: 256,
10
+ queuedBytes: 4 * 1024 * 1024, hookTimeoutMs: 60000, requestTimeoutMs: 30000,
11
+ canvasColumns: 512, canvasRows: 256 })
12
+
13
+ export class PluginError extends Error {
14
+ constructor(code, message) { super(message); this.name = 'PluginError'; this.code = code }
15
+ }
16
+ export function ensure(condition, code, message) {
17
+ if (!condition) throw new PluginError(code, message)
18
+ }
19
+ export function object(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
20
+ export function identifier(value) { return typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value) }
21
+ export function jsonBytes(value, limit = LIMITS.frameBytes) {
22
+ // Reject non-JSON values before serialization can silently change their meaning.
23
+ const seen = new Set()
24
+ function visit(item, depth) {
25
+ ensure(depth <= 32, 'invalid_payload', 'JSON nesting exceeds 32 levels')
26
+ if (item === null || typeof item === 'string' || typeof item === 'boolean') return
27
+ if (typeof item === 'number') { ensure(Number.isFinite(item), 'invalid_payload', 'JSON numbers must be finite'); return }
28
+ ensure(object(item) || Array.isArray(item), 'invalid_payload', 'Payload must contain JSON values')
29
+ ensure(!seen.has(item), 'invalid_payload', 'Payload contains a cycle')
30
+ ensure(Array.isArray(item) || [Object.prototype, null].includes(Object.getPrototypeOf(item)), 'invalid_payload', 'Payload must contain plain objects')
31
+ seen.add(item)
32
+ for (const child of Object.values(item)) visit(child, depth + 1)
33
+ seen.delete(item)
34
+ }
35
+ visit(value, 0)
36
+ const text = JSON.stringify(value)
37
+ ensure(Buffer.byteLength(text) <= limit, 'payload_too_large', `Payload exceeds ${limit} bytes`)
38
+ return text
39
+ }
40
+ function freeze(value) {
41
+ if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value) }
42
+ return value
43
+ }
44
+ export function validateManifest(value) {
45
+ ensure(object(value), 'invalid_manifest', 'package.json must contain a standardPlugin object')
46
+ jsonBytes(value, LIMITS.manifestBytes)
47
+ const allowed = ['apiVersion', 'id', 'name', 'version', 'entry', 'singleton', 'order',
48
+ 'capabilities', 'contributions', 'configSchema', 'hookTimeoutMs']
49
+ ensure(Object.keys(value).every(key => allowed.includes(key)), 'invalid_manifest', 'Unknown manifest field')
50
+ const manifest = structuredClone({ singleton: false, order: 0, capabilities: [], contributions: [], hookTimeoutMs: 5000, ...value })
51
+ ensure(manifest.apiVersion === 1, 'incompatible_version', 'Plugin requires a different SDK API version')
52
+ ensure(identifier(manifest.id), 'invalid_manifest', 'Plugin id must be a stable identifier')
53
+ ensure(typeof manifest.name === 'string' && manifest.name.trim().length > 0 && manifest.name.length <= 128,
54
+ 'invalid_manifest', 'Plugin name is required and bounded to 128 characters')
55
+ ensure(typeof manifest.version === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$/.test(manifest.version),
56
+ 'invalid_manifest', 'Plugin version must be semantic')
57
+ ensure(typeof manifest.entry === 'string' && manifest.entry.length <= 512 &&
58
+ !/[\\\u0000?#]/.test(manifest.entry) && !manifest.entry.startsWith('/') &&
59
+ manifest.entry.split('/').every(part => part && part !== '..') && /\.(?:mjs|cjs|js|mts|ts)$/.test(manifest.entry),
60
+ 'invalid_manifest', 'Plugin entry must be a relative JavaScript or TypeScript file')
61
+ ensure(typeof manifest.singleton === 'boolean' && Number.isSafeInteger(manifest.order) && Math.abs(manifest.order) <= 2147483647,
62
+ 'invalid_manifest', 'Invalid singleton or order')
63
+ ensure(Number.isSafeInteger(manifest.hookTimeoutMs) && manifest.hookTimeoutMs > 0 && manifest.hookTimeoutMs <= LIMITS.hookTimeoutMs,
64
+ 'invalid_manifest', 'Hook timeout must be between 1 and 60000 ms')
65
+ ensure(Array.isArray(manifest.capabilities) && manifest.capabilities.every(item => CAPABILITIES.includes(item)) &&
66
+ new Set(manifest.capabilities).size === manifest.capabilities.length, 'invalid_manifest', 'Unknown or repeated capability')
67
+ ensure(Array.isArray(manifest.contributions) && manifest.contributions.length <= LIMITS.contributions,
68
+ 'invalid_manifest', 'Too many contributions')
69
+ const ids = new Set()
70
+ for (const declaration of manifest.contributions) {
71
+ ensure(object(declaration) && identifier(declaration.id) && !ids.has(declaration.id) &&
72
+ SURFACE_KINDS.includes(declaration.kind) && ANCHORS.includes(declaration.anchor), 'invalid_manifest', 'Invalid contribution declaration')
73
+ ids.add(declaration.id)
74
+ for (const [key, choices] of Object.entries({ merge: ['by-machine', 'by-identity'], width: ['full', 'half'],
75
+ position: ['top', 'after-open', 'before-danger', 'bottom'] })) {
76
+ ensure(declaration[key] === undefined || choices.includes(declaration[key]), 'invalid_manifest', `Invalid contribution ${key}`)
77
+ }
78
+ for (const key of ['title', 'group', 'chord', 'pattern', 'actionId']) {
79
+ ensure(declaration[key] === undefined || (typeof declaration[key] === 'string' && declaration[key].length <= 512),
80
+ 'invalid_manifest', `Invalid contribution ${key}`)
81
+ }
82
+ }
83
+ ensure(!manifest.contributions.length || manifest.capabilities.includes('surfaces'), 'invalid_manifest', 'Contributions require surfaces capability')
84
+ ensure(manifest.configSchema === undefined || object(manifest.configSchema), 'invalid_manifest', 'Configuration schema must be an object')
85
+ return freeze(manifest)
86
+ }
@@ -0,0 +1,166 @@
1
+ import { ensure, identifier, object, jsonBytes, LIMITS, PluginError } from './manifest.mjs'
2
+
3
+ export const RUNNER_PROTOCOL_VERSION = 1
4
+ export const PLUGIN_API_VERSION = 1
5
+ export const PLUGIN_PEER_CAPABILITY = 'plugins.v1'
6
+ export const OPERATIONS = Object.freeze({
7
+ 'pane.create': 'panes', 'pane.close': 'panes', 'pane.restart': 'panes',
8
+ 'pane.input': 'panes', 'pane.focus': 'panes', 'pane.wait': 'panes',
9
+ 'project.create': 'projects', 'project.remove': 'projects',
10
+ 'notification.show': 'notifications', 'url.open': 'url', fetch: 'fetch',
11
+ 'secret.get': 'secrets', 'config.get': null, 'state.get': null, 'state.set': null,
12
+ 'context.get': null, 'popover.open': 'surfaces', 'canvas.write': 'surfaces',
13
+ 'canvas.focus': 'surfaces', 'subscription.add': null, 'subscription.remove': null,
14
+ 'health.set': null, 'webhook.ack': 'webhook',
15
+ })
16
+ export const HOST_OPERATIONS = Object.freeze(['runner.start', 'runner.stop', 'runner.reload',
17
+ 'runner.diagnose', 'runner.ping', 'runtime.invoke', 'runtime.visibility'])
18
+
19
+ export function authorize(operation, capabilities) {
20
+ ensure(object(operation) && Object.hasOwn(OPERATIONS, operation.op) && object(operation.args),
21
+ 'unsupported_operation', 'Unknown SDK operation or invalid arguments')
22
+ let capability = OPERATIONS[operation.op]
23
+ if (operation.op === 'subscription.add') {
24
+ const { id, kind, name } = operation.args
25
+ ensure(identifier(id) && typeof name === 'string' && name.length <= 256 &&
26
+ ['event', 'hook', 'action', 'input', 'select', 'resize', 'activate', 'deactivate', 'visibility'].includes(kind),
27
+ 'invalid_payload', 'Invalid subscription')
28
+ capability = kind === 'hook' ? 'hooks' : kind === 'event' ? (name === 'webhook' ? 'webhook' : 'events') : 'surfaces'
29
+ }
30
+ ensure(!capability || capabilities.includes(capability), 'capability_denied', `Operation requires ${capability} capability`)
31
+ jsonBytes(operation)
32
+ }
33
+ export function validateProducer(producer) {
34
+ ensure(object(producer) && identifier(producer.pluginId) && typeof producer.machineId === 'string' &&
35
+ producer.machineId.length > 0 && producer.machineId.length <= 128 && typeof producer.epoch === 'string' &&
36
+ /^(0|[1-9]\d{0,19})$/.test(producer.epoch) && BigInt(producer.epoch) <= 18446744073709551615n,
37
+ 'invalid_payload', 'Invalid plugin producer identity')
38
+ return producer
39
+ }
40
+ export function sameProducer(a, b) {
41
+ return a.pluginId === b.pluginId && a.machineId === b.machineId && a.epoch === b.epoch
42
+ }
43
+ export function validateEnvelope(frame) {
44
+ jsonBytes(frame)
45
+ ensure(object(frame) && frame.version === RUNNER_PROTOCOL_VERSION, 'incompatible_version', 'Daemon and runner protocol versions differ')
46
+ validateProducer(frame.producer)
47
+ ensure(['request', 'response', 'cancel', 'surface'].includes(frame.kind), 'invalid_payload', 'Unknown runner frame kind')
48
+ if (frame.kind === 'surface') {
49
+ ensure(object(frame.key) && identifier(frame.key.contributionId) && typeof frame.sequence === 'string' &&
50
+ /^(0|[1-9]\d{0,19})$/.test(frame.sequence) && BigInt(frame.sequence) <= 18446744073709551615n &&
51
+ Object.hasOwn(frame, 'content'), 'invalid_payload', 'Invalid surface replacement')
52
+ } else {
53
+ ensure(typeof frame.id === 'string' && frame.id.length > 0 && frame.id.length <= 128, 'invalid_payload', 'Missing correlation id')
54
+ if (frame.kind === 'request') {
55
+ ensure(object(frame.operation) && (Object.hasOwn(OPERATIONS, frame.operation.op) || HOST_OPERATIONS.includes(frame.operation.op)) &&
56
+ object(frame.operation.args) && Number.isSafeInteger(frame.timeoutMs) && frame.timeoutMs > 0 && frame.timeoutMs <= LIMITS.hookTimeoutMs,
57
+ 'invalid_payload', 'Invalid runner request')
58
+ }
59
+ if (frame.kind === 'response') {
60
+ ensure(object(frame.result) && typeof frame.result.ok === 'boolean' &&
61
+ (frame.result.ok ? Object.hasOwn(frame.result, 'value') :
62
+ object(frame.result.error) && typeof frame.result.error.code === 'string' && typeof frame.result.error.message === 'string'),
63
+ 'invalid_payload', 'Invalid runner response')
64
+ }
65
+ }
66
+ return frame
67
+ }
68
+ export function wireError(error) {
69
+ return { code: error instanceof PluginError ? error.code : 'plugin_error',
70
+ message: String(error?.message ?? 'Plugin operation failed').slice(0, 1024) }
71
+ }
72
+ export const realClock = Object.freeze({ now: () => Date.now(), setTimeout: (fn, ms) => setTimeout(fn, ms), clearTimeout: id => clearTimeout(id) })
73
+
74
+ /** A single correlation path for commands, actions and hooks. Send must refuse overflow synchronously. */
75
+ export class RpcPeer {
76
+ constructor({ producer, send, clock = realClock, onRequest, onSurface, onError = () => {}, idPrefix = 'host' }) {
77
+ this.producer = Object.freeze({ ...validateProducer(producer) })
78
+ this.send = send
79
+ this.clock = clock
80
+ this.onRequest = onRequest
81
+ this.onSurface = onSurface
82
+ this.onError = onError
83
+ this.pending = new Map()
84
+ this.incoming = new Map()
85
+ this.nextId = 0
86
+ this.idPrefix = idPrefix
87
+ this.closed = false
88
+ }
89
+ frame(kind, values) { return { version: RUNNER_PROTOCOL_VERSION, producer: this.producer, kind, ...values } }
90
+ transmit(frame) { validateEnvelope(frame); this.send(frame) }
91
+ request(operation, { signal, timeoutMs = LIMITS.requestTimeoutMs } = {}) {
92
+ if (this.closed) return Promise.reject(new PluginError('disposed', 'Plugin runtime is disposed'))
93
+ if (signal?.aborted) return Promise.reject(new PluginError('cancelled', 'Request was cancelled'))
94
+ if (this.pending.size >= LIMITS.pendingRequests) return Promise.reject(new PluginError('queue_full', 'Too many pending requests'))
95
+ const id = `${this.idPrefix}-${++this.nextId}`
96
+ return new Promise((resolve, reject) => {
97
+ let timer
98
+ const finish = (error, value) => {
99
+ if (!this.pending.delete(id)) return
100
+ this.clock.clearTimeout(timer)
101
+ signal?.removeEventListener('abort', cancel)
102
+ error ? reject(error) : resolve(value)
103
+ }
104
+ const cancelWith = code => {
105
+ finish(new PluginError(code, code === 'timeout' ? 'Request deadline elapsed' : 'Request was cancelled'))
106
+ try { this.transmit(this.frame('cancel', { id })) } catch (error) { this.onError(error) }
107
+ }
108
+ const cancel = () => cancelWith('cancelled')
109
+ this.pending.set(id, { finish })
110
+ signal?.addEventListener('abort', cancel, { once: true })
111
+ timer = this.clock.setTimeout(() => cancelWith('timeout'), timeoutMs)
112
+ try { this.transmit(this.frame('request', { id, operation, timeoutMs })) }
113
+ catch (error) { finish(error) }
114
+ })
115
+ }
116
+ receive(frame) {
117
+ if (this.closed) return
118
+ validateEnvelope(frame)
119
+ ensure(sameProducer(frame.producer, this.producer), 'stale_producer', 'Frame belongs to another plugin instance')
120
+ if (frame.kind === 'surface') { this.onSurface?.(frame); return }
121
+ if (frame.kind === 'response') {
122
+ const result = frame.result
123
+ this.pending.get(frame.id)?.finish(result.ok ? null : new PluginError(result.error.code, result.error.message), result.value)
124
+ return
125
+ }
126
+ if (frame.kind === 'cancel') { this.incoming.get(frame.id)?.cancel(); return }
127
+ ensure(!this.incoming.has(frame.id), 'duplicate_request', 'Request id is still in flight')
128
+ const respond = result => {
129
+ if (!this.closed) {
130
+ try { this.transmit(this.frame('response', { id: frame.id, result })) }
131
+ catch (error) { this.onError(error) }
132
+ }
133
+ }
134
+ if (this.incoming.size >= LIMITS.pendingRequests) {
135
+ respond({ ok: false, error: { code: 'queue_full', message: 'Too many incoming requests' } })
136
+ return
137
+ }
138
+ const controller = new AbortController()
139
+ // Retain the occupied slot until a cancelled handler settles. An uncooperative
140
+ // handler cannot turn repeated cancellation into unbounded concurrent work.
141
+ const cancel = () => { controller.abort(); this.clock.clearTimeout(timer) }
142
+ const timer = this.clock.setTimeout(() => {
143
+ cancel()
144
+ respond({ ok: false, error: { code: 'timeout', message: 'Handler deadline elapsed' } })
145
+ }, frame.timeoutMs)
146
+ this.incoming.set(frame.id, { cancel })
147
+ Promise.resolve().then(() => {
148
+ ensure(!controller.signal.aborted, 'cancelled', 'Request was cancelled')
149
+ ensure(typeof this.onRequest === 'function', 'unsupported_operation', 'No request handler')
150
+ return this.onRequest(frame.operation, { signal: controller.signal, timeoutMs: frame.timeoutMs })
151
+ }).then(value => {
152
+ if (!controller.signal.aborted) respond({ ok: true, value: value ?? null })
153
+ }, error => {
154
+ if (!controller.signal.aborted) respond({ ok: false, error: wireError(error) })
155
+ }).finally(() => { this.clock.clearTimeout(timer); this.incoming.delete(frame.id) })
156
+ }
157
+ dispose() {
158
+ if (this.closed) return
159
+ for (const [id, request] of this.pending) {
160
+ request.finish(new PluginError('disposed', 'Plugin runtime was disposed'))
161
+ try { this.transmit(this.frame('cancel', { id })) } catch (error) { this.onError(error) }
162
+ }
163
+ this.closed = true
164
+ for (const request of this.incoming.values()) request.cancel()
165
+ }
166
+ }
@@ -0,0 +1,62 @@
1
+ import { PluginError, ensure, object, validateManifest } from './manifest.mjs'
2
+
3
+ export const SDK_PACKAGE = '@standardagents/code-plugin-sdk'
4
+ export const SOURCE_KINDS = Object.freeze(['git', 'npm'])
5
+ const LOCKFILES = Object.freeze({ git: Object.freeze(['package-lock.json', 'npm-shrinkwrap.json']), npm: Object.freeze(['npm-shrinkwrap.json']) })
6
+
7
+ function names(section) { return object(section) ? Object.keys(section) : [] }
8
+
9
+ /** Runtime dependencies are the ones npm installs for a consumer of the package. */
10
+ export function runtimeDependencies(packageJson) {
11
+ ensure(object(packageJson), 'invalid_package', 'package.json must contain an object')
12
+ return [...new Set([...names(packageJson.dependencies), ...names(packageJson.optionalDependencies)])]
13
+ }
14
+
15
+ /**
16
+ * A plugin with runtime dependencies ships a lockfile. npm publishes
17
+ * npm-shrinkwrap.json and drops package-lock.json, so an npm source needs the
18
+ * shrinkwrap; a Git source may keep either file.
19
+ */
20
+ export function lockfileRequirement({ packageJson, sourceKind }) {
21
+ ensure(SOURCE_KINDS.includes(sourceKind), 'invalid_source', 'Source kind must be git or npm')
22
+ const dependencies = runtimeDependencies(packageJson)
23
+ return Object.freeze({ required: dependencies.length > 0, dependencies: Object.freeze(dependencies), lockfiles: LOCKFILES[sourceKind] })
24
+ }
25
+
26
+ function normalize(path) { return path.replace(/^\.\//, '') }
27
+
28
+ /**
29
+ * Pure publish checks for one plugin directory. `files` lists the relative
30
+ * POSIX paths that ship with the package. Returns a list of problems; an empty
31
+ * list means the package passes.
32
+ */
33
+ export function checkPackageForPublish({ packageJson, files, sourceKind = 'npm', requireManifest = true, expectedId } = {}) {
34
+ ensure(Array.isArray(files) && files.every(file => typeof file === 'string'), 'invalid_source', 'files must list relative paths')
35
+ const problems = []
36
+ const problem = (code, message) => problems.push(Object.freeze({ code, message }))
37
+ if (!object(packageJson)) return Object.freeze([Object.freeze({ code: 'invalid_package', message: 'package.json must contain an object' })])
38
+ const present = new Set(files.map(normalize))
39
+ if (packageJson.private === true && sourceKind === 'npm') problem('package_private', 'package.json marks the package private, so npm refuses to publish it')
40
+ const requirement = lockfileRequirement({ packageJson, sourceKind })
41
+ if (requirement.required && !requirement.lockfiles.some(name => present.has(name))) {
42
+ problem('lockfile_missing', `Dependencies (${requirement.dependencies.join(', ')}) need ${requirement.lockfiles.join(' or ')} beside package.json`)
43
+ }
44
+ for (const section of ['dependencies', 'optionalDependencies']) {
45
+ if (names(packageJson[section]).includes(SDK_PACKAGE)) problem('sdk_dependency', `${SDK_PACKAGE} belongs under peerDependencies, not ${section}`)
46
+ }
47
+ let manifest = null
48
+ if (packageJson.standardPlugin === undefined) {
49
+ if (requireManifest) problem('manifest_missing', 'package.json has no standardPlugin manifest')
50
+ } else {
51
+ try { manifest = validateManifest(packageJson.standardPlugin) } catch (error) {
52
+ if (!(error instanceof PluginError)) throw error
53
+ problem(error.code, error.message)
54
+ }
55
+ }
56
+ if (manifest) {
57
+ if (!names(packageJson.peerDependencies).includes(SDK_PACKAGE)) problem('sdk_peer_missing', `${SDK_PACKAGE} must be listed under peerDependencies`)
58
+ if (!present.has(normalize(manifest.entry))) problem('entry_missing', `Entry ${manifest.entry} is not among the package files`)
59
+ if (expectedId !== undefined && manifest.id !== expectedId) problem('id_mismatch', `Manifest id ${manifest.id} differs from collection entry ${expectedId}`)
60
+ }
61
+ return Object.freeze(problems)
62
+ }