@monosmith/plugin-sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +89 -2
  2. package/build-dir/build-dir.algorithm.d.ts +16 -0
  3. package/build-dir/build-dir.algorithm.js +26 -0
  4. package/build-dir/index.d.ts +1 -0
  5. package/build-dir/index.js +1 -0
  6. package/ci/ci-contribution.algorithm.d.ts +28 -0
  7. package/ci/ci-contribution.algorithm.js +19 -0
  8. package/ci/index.d.ts +2 -0
  9. package/ci/index.js +1 -0
  10. package/codec/encode-by-schema.algorithm.d.ts +12 -0
  11. package/codec/encode-by-schema.algorithm.js +105 -0
  12. package/codec/encode-error.error.d.ts +4 -0
  13. package/codec/encode-error.error.js +7 -0
  14. package/codec/encode-json-value.algorithm.d.ts +13 -0
  15. package/codec/encode-json-value.algorithm.js +51 -0
  16. package/codec/index.d.ts +6 -0
  17. package/codec/index.js +5 -0
  18. package/codec/message-parse.error.d.ts +4 -0
  19. package/codec/message-parse.error.js +7 -0
  20. package/codec/method-codec.algorithm.d.ts +8 -0
  21. package/codec/method-codec.algorithm.js +31 -0
  22. package/codec/normalize-by-schema.algorithm.d.ts +14 -0
  23. package/codec/normalize-by-schema.algorithm.js +103 -0
  24. package/codec/params-error.error.d.ts +4 -0
  25. package/codec/params-error.error.js +7 -0
  26. package/codec/schema-lookup.algorithm.d.ts +13 -0
  27. package/codec/schema-lookup.algorithm.js +56 -0
  28. package/codec/wire-message.mapper.d.ts +25 -0
  29. package/codec/wire-message.mapper.js +58 -0
  30. package/conformance/check-generation.use-case.d.ts +12 -0
  31. package/conformance/check-generation.use-case.js +91 -0
  32. package/conformance/check-plugin.use-case.d.ts +24 -0
  33. package/conformance/check-plugin.use-case.js +78 -0
  34. package/conformance/conformance-report.model.d.ts +22 -0
  35. package/conformance/conformance-report.model.js +23 -0
  36. package/conformance/find-manifests.algorithm.d.ts +7 -0
  37. package/conformance/find-manifests.algorithm.js +34 -0
  38. package/conformance/go-list.mapper.d.ts +4 -0
  39. package/conformance/go-list.mapper.js +8 -0
  40. package/conformance/index.d.ts +5 -0
  41. package/conformance/index.js +3 -0
  42. package/framing/frame-message.algorithm.d.ts +5 -0
  43. package/framing/frame-message.algorithm.js +8 -0
  44. package/framing/frame-reader.store.d.ts +15 -0
  45. package/framing/frame-reader.store.js +59 -0
  46. package/framing/framing.error.d.ts +4 -0
  47. package/framing/framing.error.js +7 -0
  48. package/framing/index.d.ts +3 -0
  49. package/framing/index.js +3 -0
  50. package/host/connect-plugin.use-case.d.ts +26 -0
  51. package/host/connect-plugin.use-case.js +44 -0
  52. package/host/host-options.contract.d.ts +28 -0
  53. package/host/host-options.contract.js +2 -0
  54. package/host/index.d.ts +7 -0
  55. package/host/index.js +4 -0
  56. package/host/plugin-client.client.d.ts +42 -0
  57. package/host/plugin-client.client.js +166 -0
  58. package/host/start-plugin.use-case.d.ts +10 -0
  59. package/host/start-plugin.use-case.js +57 -0
  60. package/index.d.ts +15 -0
  61. package/index.js +20 -0
  62. package/jsondoc/indent-raw-json.algorithm.d.ts +8 -0
  63. package/jsondoc/indent-raw-json.algorithm.js +60 -0
  64. package/jsondoc/index.d.ts +2 -0
  65. package/jsondoc/index.js +2 -0
  66. package/jsondoc/json-document.error.d.ts +4 -0
  67. package/jsondoc/json-document.error.js +7 -0
  68. package/jsondoc/json-document.model.d.ts +38 -0
  69. package/jsondoc/json-document.model.js +113 -0
  70. package/jsondoc/scan-json-object.algorithm.d.ts +17 -0
  71. package/jsondoc/scan-json-object.algorithm.js +132 -0
  72. package/package.json +34 -4
  73. package/plugin-refs/index.d.ts +1 -0
  74. package/plugin-refs/index.js +1 -0
  75. package/plugin-refs/plugin-ref.policy.d.ts +9 -0
  76. package/plugin-refs/plugin-ref.policy.js +22 -0
  77. package/plugin-roles/index.d.ts +1 -0
  78. package/plugin-roles/index.js +1 -0
  79. package/plugin-roles/plugin-role.policy.d.ts +16 -0
  80. package/plugin-roles/plugin-role.policy.js +30 -0
  81. package/projects/audit-settings.policy.d.ts +5 -0
  82. package/projects/audit-settings.policy.js +8 -0
  83. package/projects/index.d.ts +6 -0
  84. package/projects/index.js +6 -0
  85. package/projects/manifest-match.algorithm.d.ts +7 -0
  86. package/projects/manifest-match.algorithm.js +38 -0
  87. package/projects/project-kind.policy.d.ts +9 -0
  88. package/projects/project-kind.policy.js +28 -0
  89. package/projects/publishes.policy.d.ts +7 -0
  90. package/projects/publishes.policy.js +8 -0
  91. package/projects/set-toolchain.algorithm.d.ts +7 -0
  92. package/projects/set-toolchain.algorithm.js +13 -0
  93. package/projects/stack-plugins.algorithm.d.ts +3 -0
  94. package/projects/stack-plugins.algorithm.js +4 -0
  95. package/protocol/index.d.ts +4 -0
  96. package/protocol/index.js +3 -0
  97. package/protocol/protocol-schema.contract.d.ts +3 -0
  98. package/protocol/protocol-schema.contract.js +1925 -0
  99. package/protocol/protocol-types.contract.d.ts +1017 -0
  100. package/protocol/protocol-types.contract.js +36 -0
  101. package/protocol/rpc-error.error.d.ts +9 -0
  102. package/protocol/rpc-error.error.js +13 -0
  103. package/protocol/schema-document.contract.d.ts +55 -0
  104. package/protocol/schema-document.contract.js +1 -0
  105. package/scaffold/index.d.ts +3 -0
  106. package/scaffold/index.js +2 -0
  107. package/scaffold/scaffold-data.mapper.d.ts +15 -0
  108. package/scaffold/scaffold-data.mapper.js +24 -0
  109. package/scaffold/scaffold-project.use-case.d.ts +20 -0
  110. package/scaffold/scaffold-project.use-case.js +13 -0
  111. package/serve/capabilities-of.algorithm.d.ts +13 -0
  112. package/serve/capabilities-of.algorithm.js +28 -0
  113. package/serve/claim-stdout.use-case.d.ts +10 -0
  114. package/serve/claim-stdout.use-case.js +14 -0
  115. package/serve/dispatch-request.use-case.d.ts +14 -0
  116. package/serve/dispatch-request.use-case.js +56 -0
  117. package/serve/index.d.ts +4 -0
  118. package/serve/index.js +2 -0
  119. package/serve/initialize-plugin.use-case.d.ts +9 -0
  120. package/serve/initialize-plugin.use-case.js +16 -0
  121. package/serve/plugin-members.config.d.ts +16 -0
  122. package/serve/plugin-members.config.js +25 -0
  123. package/serve/plugin.contract.d.ts +65 -0
  124. package/serve/plugin.contract.js +1 -0
  125. package/serve/protocol-streams.contract.d.ts +13 -0
  126. package/serve/protocol-streams.contract.js +1 -0
  127. package/serve/request-outcome.contract.d.ts +9 -0
  128. package/serve/request-outcome.contract.js +1 -0
  129. package/serve/serve.use-case.d.ts +27 -0
  130. package/serve/serve.use-case.js +93 -0
  131. package/tasks/contribute-tasks.algorithm.d.ts +15 -0
  132. package/tasks/contribute-tasks.algorithm.js +22 -0
  133. package/tasks/index.d.ts +2 -0
  134. package/tasks/index.js +2 -0
  135. package/tasks/task.algorithm.d.ts +18 -0
  136. package/tasks/task.algorithm.js +23 -0
  137. package/templating/file-exists.error.d.ts +6 -0
  138. package/templating/file-exists.error.js +10 -0
  139. package/templating/index.d.ts +4 -0
  140. package/templating/index.js +4 -0
  141. package/templating/parse-template.algorithm.d.ts +11 -0
  142. package/templating/parse-template.algorithm.js +197 -0
  143. package/templating/render-template.algorithm.d.ts +7 -0
  144. package/templating/render-template.algorithm.js +87 -0
  145. package/templating/template-functions.algorithm.d.ts +10 -0
  146. package/templating/template-functions.algorithm.js +104 -0
  147. package/templating/template-syntax.contract.d.ts +33 -0
  148. package/templating/template-syntax.contract.js +1 -0
  149. package/templating/template.error.d.ts +4 -0
  150. package/templating/template.error.js +7 -0
  151. package/templating/tokenize-action.algorithm.d.ts +20 -0
  152. package/templating/tokenize-action.algorithm.js +57 -0
  153. package/templating/write-templates.use-case.d.ts +11 -0
  154. package/templating/write-templates.use-case.js +44 -0
@@ -0,0 +1,44 @@
1
+ import { PassThrough } from 'node:stream';
2
+ import { serveStreams } from '../serve/index.js';
3
+ import { PluginClient } from './plugin-client.client.js';
4
+ /**
5
+ * Performs the handshake with a plugin reachable through a pair of streams.
6
+ * Closing the client shuts the plugin down and ends the streams; there is no
7
+ * process to kill.
8
+ */
9
+ export function connectPlugin(streams, options) {
10
+ return PluginClient.open({
11
+ input: streams.input,
12
+ output: streams.output,
13
+ kill: () => { streams.output.end(); },
14
+ finish: () => Promise.resolve(null),
15
+ }, options);
16
+ }
17
+ /**
18
+ * Serves a plugin object in this process over pipes and connects to it, the way
19
+ * the Go SDK's tests serve a plugin with `serve.Serve` over `io.Pipe`: the
20
+ * real framing, handshake and dispatch, no process. Closing the client waits
21
+ * for the serve loop to end.
22
+ */
23
+ export async function connectInProcess(plugin, options) {
24
+ const toPlugin = new PassThrough();
25
+ const fromPlugin = new PassThrough();
26
+ const served = (async () => {
27
+ try {
28
+ await serveStreams(plugin, { input: toPlugin, output: fromPlugin, log: () => { } });
29
+ return null;
30
+ }
31
+ catch (error) {
32
+ return error instanceof Error ? error.message : String(error);
33
+ }
34
+ finally {
35
+ fromPlugin.end();
36
+ }
37
+ })();
38
+ return await PluginClient.open({
39
+ input: fromPlugin,
40
+ output: toPlugin,
41
+ kill: () => { toPlugin.end(); },
42
+ finish: () => served,
43
+ }, options);
44
+ }
@@ -0,0 +1,28 @@
1
+ import type { Peer } from '../protocol/index.ts';
2
+ /** What the host tells the plugin, and how long it waits. */
3
+ export interface HostOptions {
4
+ /** The absolute workspace root sent in `initialize`. A plugin that reads manifests resolves them against it. */
5
+ workspaceRoot: string;
6
+ /** The host's name and version. Default: `smith`, `dev`. */
7
+ host?: Peer;
8
+ /** How long `initialize` may take. Default: 30 seconds. */
9
+ initializeTimeoutMs?: number;
10
+ }
11
+ /** Options of {@link startPlugin}: a plugin process to launch. */
12
+ export interface StartOptions extends HostOptions {
13
+ /** The plugin's argv: the program and its arguments. */
14
+ command: string[];
15
+ /** The environment. Default: the current one. `SMITH_PLUGIN=1` is set in it unless it already has a value, as smith does. */
16
+ env?: NodeJS.ProcessEnv;
17
+ /** The plugin's working directory. Default: the current one. */
18
+ cwd?: string;
19
+ /** What happens to the plugin's standard error, where it logs. Default: it goes to this process's standard error. */
20
+ stderr?: 'inherit' | 'ignore';
21
+ }
22
+ /** The longest a call may take unless the caller says otherwise: generate and publish run real toolchains. */
23
+ export declare const DEFAULT_CALL_TIMEOUT_MS: number;
24
+ /** Options of one {@link PluginClient.call}. */
25
+ export interface CallOptions {
26
+ /** How long the plugin may take. On expiry the plugin process is killed: its stream can no longer be trusted. Default: 2 minutes. */
27
+ timeoutMs?: number;
28
+ }
@@ -0,0 +1,2 @@
1
+ /** The longest a call may take unless the caller says otherwise: generate and publish run real toolchains. */
2
+ export const DEFAULT_CALL_TIMEOUT_MS = 2 * 60 * 1000;
@@ -0,0 +1,7 @@
1
+ export { PluginClient } from './plugin-client.client.ts';
2
+ export type { PluginConnection } from './plugin-client.client.ts';
3
+ export { startPlugin } from './start-plugin.use-case.ts';
4
+ export { connectInProcess, connectPlugin } from './connect-plugin.use-case.ts';
5
+ export type { PluginStreams } from './connect-plugin.use-case.ts';
6
+ export { DEFAULT_CALL_TIMEOUT_MS } from './host-options.contract.ts';
7
+ export type { CallOptions, HostOptions, StartOptions } from './host-options.contract.ts';
package/host/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { PluginClient } from './plugin-client.client.js';
2
+ export { startPlugin } from './start-plugin.use-case.js';
3
+ export { connectInProcess, connectPlugin } from './connect-plugin.use-case.js';
4
+ export { DEFAULT_CALL_TIMEOUT_MS } from './host-options.contract.js';
@@ -0,0 +1,42 @@
1
+ import type { InitializeResult, MethodName, ParamsOf, ResultOf } from '../protocol/index.ts';
2
+ import type { CallOptions, HostOptions } from './host-options.contract.ts';
3
+ /** The two ends of the pipe to a plugin, and what to do with the plugin when the stream cannot be trusted. */
4
+ export interface PluginConnection {
5
+ /** The plugin's responses. */
6
+ input: AsyncIterable<Uint8Array>;
7
+ /** The plugin's requests. */
8
+ output: {
9
+ write: (chunk: Buffer, callback: (error?: Error | null) => void) => unknown;
10
+ end: () => unknown;
11
+ };
12
+ /** Called when a call timed out: ends the plugin, if it is a process. */
13
+ kill: () => void;
14
+ /** Resolves once the plugin has gone, with the reason it did not end well (or `null`). Called by {@link PluginClient.close}. */
15
+ finish: (graceMs: number) => Promise<string | null>;
16
+ }
17
+ /**
18
+ * The host's side of a connection to one initialized plugin, for a plugin
19
+ * author's tests (smith itself is the host in real use). Calls are serialised,
20
+ * as smith serialises them, each with a timeout. Open one with {@link startPlugin}
21
+ * (a process), {@link connectPlugin} (streams) or {@link connectInProcess}
22
+ * (a plugin object served in this process).
23
+ */
24
+ export declare class PluginClient {
25
+ #private;
26
+ /** Performs the handshake on a connection. */
27
+ static open(connection: PluginConnection, options: HostOptions): Promise<PluginClient>;
28
+ /** What the plugin answered to `initialize`: identity, kind, roles, capabilities. */
29
+ info: InitializeResult;
30
+ private constructor();
31
+ /**
32
+ * Sends one request and resolves with its decoded result. An error answer
33
+ * rejects with an {@link RpcError} carrying its code. If the plugin does not
34
+ * answer in time, the plugin is killed and the call rejects.
35
+ */
36
+ call<M extends MethodName>(method: M, params: ParamsOf<M>, options?: CallOptions): Promise<ResultOf<M>>;
37
+ call(method: string, params?: unknown, options?: CallOptions): Promise<unknown>;
38
+ /** Whether the plugin advertised a method: `initialize` and `shutdown` always, the others by their capability. */
39
+ supports(method: string): boolean;
40
+ /** Asks the plugin to shut down and waits briefly for it to go. Rejects if it did not go quietly. */
41
+ close(): Promise<void>;
42
+ }
@@ -0,0 +1,166 @@
1
+ import { decodeResult, encodeParams, parseResponse, writeRequest } from '../codec/index.js';
2
+ import { FrameReader, frameMessage } from '../framing/index.js';
3
+ import { PROTOCOL_VERSION, RpcError, protocolSchema } from '../protocol/index.js';
4
+ import { DEFAULT_CALL_TIMEOUT_MS } from './host-options.contract.js';
5
+ function writeFrame(output, frame) {
6
+ return new Promise((resolve, reject) => {
7
+ output.write(frame, (error) => {
8
+ if (error === null || error === undefined)
9
+ resolve();
10
+ else
11
+ reject(error);
12
+ });
13
+ });
14
+ }
15
+ function messageOf(thrown) {
16
+ return thrown instanceof Error ? thrown.message : String(thrown);
17
+ }
18
+ /**
19
+ * The host's side of a connection to one initialized plugin, for a plugin
20
+ * author's tests (smith itself is the host in real use). Calls are serialised,
21
+ * as smith serialises them, each with a timeout. Open one with {@link startPlugin}
22
+ * (a process), {@link connectPlugin} (streams) or {@link connectInProcess}
23
+ * (a plugin object served in this process).
24
+ */
25
+ export class PluginClient {
26
+ /** Performs the handshake on a connection. */
27
+ static async open(connection, options) {
28
+ const client = new PluginClient(connection);
29
+ try {
30
+ client.info = await client.call('initialize', {
31
+ protocolVersions: [PROTOCOL_VERSION],
32
+ host: options.host ?? { name: 'smith', version: 'dev' },
33
+ workspaceRoot: options.workspaceRoot,
34
+ }, { timeoutMs: options.initializeTimeoutMs ?? 30_000 });
35
+ }
36
+ catch (error) {
37
+ connection.kill();
38
+ throw new Error(`initializing plugin: ${messageOf(error)}`, { cause: error });
39
+ }
40
+ if (client.info.protocolVersion !== PROTOCOL_VERSION) {
41
+ connection.kill();
42
+ throw new Error(`plugin ${client.info.plugin.name} chose protocol version ${client.info.protocolVersion}, host speaks ${PROTOCOL_VERSION}`);
43
+ }
44
+ return client;
45
+ }
46
+ #connection;
47
+ #waiter;
48
+ #lost;
49
+ #tail = Promise.resolve();
50
+ #nextId = 0;
51
+ /** What the plugin answered to `initialize`: identity, kind, roles, capabilities. */
52
+ info;
53
+ constructor(connection) {
54
+ this.#connection = connection;
55
+ void this.#readResponses();
56
+ }
57
+ /** Runs a task after the ones before it, whatever became of them. */
58
+ async #serialise(task) {
59
+ const previous = this.#tail;
60
+ const current = (async () => {
61
+ await previous;
62
+ return await task();
63
+ })();
64
+ this.#tail = (async () => {
65
+ try {
66
+ await current;
67
+ }
68
+ catch {
69
+ // The caller of this task sees the failure; the next one only waits.
70
+ }
71
+ })();
72
+ return await current;
73
+ }
74
+ async #send(method, params, timeoutMs) {
75
+ if (this.#lost !== undefined)
76
+ throw new Error(`${method}: plugin stopped answering: ${this.#lost.message}`, { cause: this.#lost });
77
+ const id = ++this.#nextId;
78
+ const body = writeRequest(id, method, params === undefined ? undefined : encodeParams(method, params));
79
+ const answer = new Promise((resolve) => { this.#waiter = { id, settle: resolve }; });
80
+ try {
81
+ await writeFrame(this.#connection.output, frameMessage(body));
82
+ }
83
+ catch (error) {
84
+ this.#waiter = undefined;
85
+ throw new Error(`${method}: plugin is not accepting requests: ${messageOf(error)}`, { cause: error });
86
+ }
87
+ let timer;
88
+ const timeout = new Promise((resolve) => {
89
+ timer = setTimeout(() => {
90
+ this.#connection.kill();
91
+ resolve({ timedOut: true });
92
+ }, timeoutMs);
93
+ });
94
+ const outcome = await Promise.race([answer, timeout]);
95
+ clearTimeout(timer);
96
+ this.#waiter = undefined;
97
+ if ('timedOut' in outcome)
98
+ throw new Error(`${method}: plugin did not answer within ${timeoutMs}ms`);
99
+ if ('error' in outcome)
100
+ throw new Error(`${method}: plugin stopped answering: ${outcome.error.message}`, { cause: outcome.error });
101
+ try {
102
+ const response = parseResponse(outcome.body);
103
+ if (response.error !== null)
104
+ throw new RpcError(response.error.code, response.error.message);
105
+ return decodeResult(method, response.result);
106
+ }
107
+ catch (error) {
108
+ if (error instanceof RpcError)
109
+ throw error;
110
+ throw new Error(`${method}: plugin stopped answering: ${messageOf(error)}`, { cause: error });
111
+ }
112
+ }
113
+ async #readResponses() {
114
+ const reader = new FrameReader();
115
+ try {
116
+ for await (const chunk of this.#connection.input) {
117
+ for (const body of reader.push(chunk))
118
+ this.#deliver(body);
119
+ }
120
+ this.#lose(new Error('EOF'));
121
+ }
122
+ catch (error) {
123
+ this.#lose(error instanceof Error ? error : new Error(String(error)));
124
+ }
125
+ }
126
+ #deliver(body) {
127
+ const waiter = this.#waiter;
128
+ if (waiter === undefined)
129
+ return;
130
+ try {
131
+ if (parseResponse(body).id === waiter.id)
132
+ waiter.settle({ body });
133
+ // Responses to other ids cannot happen with serialised calls; skip them.
134
+ }
135
+ catch (error) {
136
+ waiter.settle({ error: new Error(`malformed response: ${messageOf(error)}`, { cause: error }) });
137
+ }
138
+ }
139
+ #lose(error) {
140
+ this.#lost = error;
141
+ this.#waiter?.settle({ error });
142
+ }
143
+ async call(method, params, options = {}) {
144
+ return await this.#serialise(() => this.#send(method, params, options.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS));
145
+ }
146
+ /** Whether the plugin advertised a method: `initialize` and `shutdown` always, the others by their capability. */
147
+ supports(method) {
148
+ if (method === 'initialize' || method === 'shutdown')
149
+ return true;
150
+ const capability = Object.hasOwn(protocolSchema['x-methods'], method) ? protocolSchema['x-methods'][method].capability : null;
151
+ return capability !== null && this.info.capabilities[capability] === true;
152
+ }
153
+ /** Asks the plugin to shut down and waits briefly for it to go. Rejects if it did not go quietly. */
154
+ async close() {
155
+ try {
156
+ await this.call('shutdown', {}, { timeoutMs: 5000 });
157
+ }
158
+ catch {
159
+ // A plugin that is already gone cannot be asked; waiting for it below reports how it went.
160
+ }
161
+ this.#connection.output.end();
162
+ const trouble = await this.#connection.finish(5000);
163
+ if (trouble !== null)
164
+ throw new Error(trouble);
165
+ }
166
+ }
@@ -0,0 +1,10 @@
1
+ import type { StartOptions } from './host-options.contract.ts';
2
+ import { PluginClient } from './plugin-client.client.ts';
3
+ /**
4
+ * Launches a plugin process and performs the handshake, as smith does: the
5
+ * plugin's argv, `SMITH_PLUGIN=1` in its environment, its standard input and
6
+ * output as the protocol's channel, its standard error forwarded to this
7
+ * process's. Close the client when done: it asks the plugin to shut down and
8
+ * kills it if it does not go.
9
+ */
10
+ export declare function startPlugin(options: StartOptions): Promise<PluginClient>;
@@ -0,0 +1,57 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { PluginClient } from './plugin-client.client.js';
3
+ /**
4
+ * Launches a plugin process and performs the handshake, as smith does: the
5
+ * plugin's argv, `SMITH_PLUGIN=1` in its environment, its standard input and
6
+ * output as the protocol's channel, its standard error forwarded to this
7
+ * process's. Close the client when done: it asks the plugin to shut down and
8
+ * kills it if it does not go.
9
+ */
10
+ export async function startPlugin(options) {
11
+ const [program, ...args] = options.command;
12
+ if (program === undefined)
13
+ throw new Error('plugin command is empty');
14
+ const env = { ...(options.env ?? process.env) };
15
+ env.SMITH_PLUGIN ??= '1';
16
+ const child = spawn(program, args, { env, cwd: options.cwd, stdio: ['pipe', 'pipe', options.stderr ?? 'inherit'], windowsHide: true });
17
+ const exited = new Promise((resolve) => {
18
+ child.once('close', (code, signal) => { resolve({ code, signal }); });
19
+ });
20
+ // A program that cannot be started never answers: its streams just end.
21
+ let spawnFailure;
22
+ child.once('error', (cause) => { spawnFailure = cause; });
23
+ // Writing to a plugin that has gone raises EPIPE on stdin; the call that wrote sees the error.
24
+ child.stdin.on('error', () => { });
25
+ const connection = {
26
+ input: child.stdout,
27
+ output: { write: (chunk, callback) => child.stdin.write(chunk, callback), end: () => child.stdin.end() },
28
+ kill: () => { child.kill(); },
29
+ finish: (graceMs) => waitForExit(child, exited, graceMs),
30
+ };
31
+ try {
32
+ return await PluginClient.open(connection, options);
33
+ }
34
+ catch (error) {
35
+ child.kill();
36
+ await exited;
37
+ if (spawnFailure === undefined)
38
+ throw error;
39
+ throw new Error(`starting plugin ${JSON.stringify(options.command)}: ${spawnFailure.message}`, { cause: error });
40
+ }
41
+ }
42
+ async function waitForExit(child, exited, graceMs) {
43
+ let timer;
44
+ const timeout = new Promise((resolve) => {
45
+ timer = setTimeout(() => {
46
+ resolve('timeout');
47
+ }, graceMs);
48
+ });
49
+ const outcome = await Promise.race([exited, timeout]);
50
+ clearTimeout(timer);
51
+ if (outcome === 'timeout') {
52
+ child.kill();
53
+ await exited;
54
+ return 'plugin did not exit after shutdown; killed';
55
+ }
56
+ return outcome.code === 0 ? null : `plugin exited with ${outcome.signal ?? `status ${outcome.code}`}`;
57
+ }
package/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from './protocol/index.ts';
2
+ export * from './serve/index.ts';
3
+ export * from './tasks/index.ts';
4
+ export * from './scaffold/index.ts';
5
+ export * from './ci/index.ts';
6
+ export * from './build-dir/index.ts';
7
+ export * from './plugin-roles/index.ts';
8
+ export * from './plugin-refs/index.ts';
9
+ export * from './projects/index.ts';
10
+ export * from './templating/index.ts';
11
+ export * from './jsondoc/index.ts';
12
+ export * from './host/index.ts';
13
+ export * from './conformance/index.ts';
14
+ export { decodeParams, decodeResult, encodeParams, encodeResult, EncodeError, MessageParseError, ParamsError } from './codec/index.ts';
15
+ export { FrameReader, FramingError, frameMessage } from './framing/index.ts';
package/index.js ADDED
@@ -0,0 +1,20 @@
1
+ // The public API of @monosmith/plugin-sdk.
2
+ // The protocol: every message as a type, generated from the JSON Schema.
3
+ export * from './protocol/index.js';
4
+ // Writing a plugin.
5
+ export * from './serve/index.js';
6
+ export * from './tasks/index.js';
7
+ export * from './scaffold/index.js';
8
+ export * from './ci/index.js';
9
+ export * from './build-dir/index.js';
10
+ export * from './plugin-roles/index.js';
11
+ export * from './plugin-refs/index.js';
12
+ export * from './projects/index.js';
13
+ export * from './templating/index.js';
14
+ export * from './jsondoc/index.js';
15
+ // Testing a plugin.
16
+ export * from './host/index.js';
17
+ export * from './conformance/index.js';
18
+ // Building blocks: the wire form of the messages, for tools that speak the protocol themselves.
19
+ export { decodeParams, decodeResult, encodeParams, encodeResult, EncodeError, MessageParseError, ParamsError } from './codec/index.js';
20
+ export { FrameReader, FramingError, frameMessage } from './framing/index.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Formats JSON text as Go's `json.Indent(dst, src, prefix, " ")` does: the
3
+ * insignificant whitespace is dropped and every element goes on its own line,
4
+ * indented two spaces per level and starting with `prefix`; an empty array or
5
+ * object stays `[]` or `{}`. Text that is not an array is returned as it is. The text
6
+ * of leaves (numbers, strings) is kept exactly.
7
+ */
8
+ export declare function indentRawJson(raw: string, prefix: string): string;
@@ -0,0 +1,60 @@
1
+ /** A string literal (escapes included), a bracket, a comma or colon, or a run of any other characters (a number, `true`, `null`). */
2
+ const TOKEN = /"(?:[^"\\]|\\.)*"|[{}[\],:]|[^\s{}[\],:"]+/g;
3
+ /**
4
+ * Formats JSON text as Go's `json.Indent(dst, src, prefix, " ")` does: the
5
+ * insignificant whitespace is dropped and every element goes on its own line,
6
+ * indented two spaces per level and starting with `prefix`; an empty array or
7
+ * object stays `[]` or `{}`. Text that is not an array is returned as it is. The text
8
+ * of leaves (numbers, strings) is kept exactly.
9
+ */
10
+ export function indentRawJson(raw, prefix) {
11
+ if (!raw.startsWith('['))
12
+ return raw;
13
+ let out = '';
14
+ let depth = 0;
15
+ // Right after an opening bracket: the line break is held back until it is known whether the container is empty.
16
+ let needIndent = false;
17
+ const newline = () => {
18
+ out += `\n${prefix}${' '.repeat(depth)}`;
19
+ };
20
+ const open = (token) => {
21
+ needIndent = true;
22
+ out += token;
23
+ };
24
+ const close = (token) => {
25
+ if (needIndent) {
26
+ needIndent = false;
27
+ }
28
+ else {
29
+ depth--;
30
+ newline();
31
+ }
32
+ out += token;
33
+ };
34
+ const handlers = {
35
+ '{': open,
36
+ '[': open,
37
+ '}': close,
38
+ ']': close,
39
+ ',': (token) => {
40
+ out += token;
41
+ newline();
42
+ },
43
+ ':': () => {
44
+ out += ': ';
45
+ },
46
+ };
47
+ for (const [token] of raw.matchAll(TOKEN)) {
48
+ if (needIndent && token !== '}' && token !== ']') {
49
+ needIndent = false;
50
+ depth++;
51
+ newline();
52
+ }
53
+ const handler = Object.hasOwn(handlers, token) ? handlers[token] : undefined;
54
+ if (handler === undefined)
55
+ out += token;
56
+ else
57
+ handler(token);
58
+ }
59
+ return out;
60
+ }
@@ -0,0 +1,2 @@
1
+ export { JsonDocument } from './json-document.model.ts';
2
+ export { JsonDocumentError } from './json-document.error.ts';
@@ -0,0 +1,2 @@
1
+ export { JsonDocument } from './json-document.model.js';
2
+ export { JsonDocumentError } from './json-document.error.js';
@@ -0,0 +1,4 @@
1
+ /** The text is not a JSON object, or not JSON at all. */
2
+ export declare class JsonDocumentError extends Error {
3
+ constructor(message: string, options?: ErrorOptions);
4
+ }
@@ -0,0 +1,7 @@
1
+ /** The text is not a JSON object, or not JSON at all. */
2
+ export class JsonDocumentError extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = 'JsonDocumentError';
6
+ }
7
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A JSON object that keeps its key order and the text of everything you do not
3
+ * touch, so editing a `package.json` changes only what was edited: set a
4
+ * member and it keeps its place, add one and it goes last, delete one and the
5
+ * others stay where they were. Port of the Go SDK's `jsondoc.Document`.
6
+ *
7
+ * ```ts
8
+ * const doc = JsonDocument.parse(text)
9
+ * doc.setString('version', '1.2.3')
10
+ * writeFileSync(path, doc.render())
11
+ * ```
12
+ */
13
+ export declare class JsonDocument {
14
+ #private;
15
+ /** Reads a JSON object. Throws {@link JsonDocumentError} for anything else. */
16
+ static parse(text: string): JsonDocument;
17
+ /** Whether the key exists. */
18
+ has(key: string): boolean;
19
+ /** The keys, in order. */
20
+ keys(): string[];
21
+ /** The number of keys. */
22
+ get size(): number;
23
+ /** The object under a key, editable in place; `create` adds an empty one (last) when there is none. Undefined for a key that holds something else. */
24
+ object(key: string, create?: boolean): JsonDocument | undefined;
25
+ /** A string value; undefined when the key is missing or does not hold a string. */
26
+ getString(key: string): string | undefined;
27
+ /** Any value under a key as JavaScript sees it, or undefined for a missing key. */
28
+ getValue(key: string): unknown;
29
+ /** Sets a string, keeping the key's position. */
30
+ setString(key: string, value: string): void;
31
+ /** Sets any JSON-encodable value, keeping the key's position. A plain object becomes an editable document, with its members in the order you wrote them. */
32
+ setJson(key: string, value: unknown): void;
33
+ /** Removes a key. */
34
+ delete(key: string): void;
35
+ /** The document as npm writes a package.json: two-space indentation, a final newline. */
36
+ render(): string;
37
+ toString(): string;
38
+ }
@@ -0,0 +1,113 @@
1
+ var _a;
2
+ import { indentRawJson } from './indent-raw-json.algorithm.js';
3
+ import { JsonDocumentError } from './json-document.error.js';
4
+ import { scanJsonObject } from './scan-json-object.algorithm.js';
5
+ /** A string as Go's JSON encoder writes it without HTML escaping: JSON, with U+2028 and U+2029 escaped. */
6
+ function quote(text) {
7
+ return JSON.stringify(text)
8
+ .replaceAll('\u{2028}', String.raw `\u2028`)
9
+ .replaceAll('\u{2029}', String.raw `\u2029`);
10
+ }
11
+ /**
12
+ * A JSON object that keeps its key order and the text of everything you do not
13
+ * touch, so editing a `package.json` changes only what was edited: set a
14
+ * member and it keeps its place, add one and it goes last, delete one and the
15
+ * others stay where they were. Port of the Go SDK's `jsondoc.Document`.
16
+ *
17
+ * ```ts
18
+ * const doc = JsonDocument.parse(text)
19
+ * doc.setString('version', '1.2.3')
20
+ * writeFileSync(path, doc.render())
21
+ * ```
22
+ */
23
+ export class JsonDocument {
24
+ /** Reads a JSON object. Throws {@link JsonDocumentError} for anything else. */
25
+ static parse(text) {
26
+ return _a.#fromNode(scanJsonObject(text));
27
+ }
28
+ static #fromNode(node) {
29
+ const doc = new _a();
30
+ for (const [key, value] of node.members)
31
+ doc.#entries.set(key, value.kind === 'object' ? _a.#fromNode(value) : { raw: value.text });
32
+ return doc;
33
+ }
34
+ /** In the order the keys were first set: a Map keeps it, whatever the keys look like. */
35
+ #entries = new Map();
36
+ #write(indent) {
37
+ if (this.#entries.size === 0)
38
+ return '{}';
39
+ const inner = `${indent} `;
40
+ const lines = [...this.#entries].map(([key, entry]) => {
41
+ const value = entry instanceof _a ? entry.#write(inner) : indentRawJson(entry.raw, inner);
42
+ return `${inner}${quote(key)}: ${value}`;
43
+ });
44
+ return `{\n${lines.join(',\n')}\n${indent}}`;
45
+ }
46
+ /** Whether the key exists. */
47
+ has(key) {
48
+ return this.#entries.has(key);
49
+ }
50
+ /** The keys, in order. */
51
+ keys() {
52
+ // The es2023 lib this package compiles with has no Iterator#toArray.
53
+ // eslint-disable-next-line unicorn/prefer-spread
54
+ return Array.from(this.#entries.keys());
55
+ }
56
+ /** The number of keys. */
57
+ get size() {
58
+ return this.#entries.size;
59
+ }
60
+ /** The object under a key, editable in place; `create` adds an empty one (last) when there is none. Undefined for a key that holds something else. */
61
+ object(key, create = false) {
62
+ const entry = this.#entries.get(key);
63
+ if (entry instanceof _a)
64
+ return entry;
65
+ if (!create)
66
+ return undefined;
67
+ const child = new _a();
68
+ this.#entries.set(key, child);
69
+ return child;
70
+ }
71
+ /** A string value; undefined when the key is missing or does not hold a string. */
72
+ getString(key) {
73
+ const entry = this.#entries.get(key);
74
+ if (entry === undefined || entry instanceof _a)
75
+ return undefined;
76
+ const value = JSON.parse(entry.raw);
77
+ return typeof value === 'string' ? value : undefined;
78
+ }
79
+ /** Any value under a key as JavaScript sees it, or undefined for a missing key. */
80
+ getValue(key) {
81
+ const entry = this.#entries.get(key);
82
+ if (entry === undefined)
83
+ return undefined;
84
+ return JSON.parse(entry instanceof _a ? entry.render() : entry.raw);
85
+ }
86
+ /** Sets a string, keeping the key's position. */
87
+ setString(key, value) {
88
+ this.#entries.set(key, { raw: quote(value) });
89
+ }
90
+ /** Sets any JSON-encodable value, keeping the key's position. A plain object becomes an editable document, with its members in the order you wrote them. */
91
+ setJson(key, value) {
92
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
93
+ this.#entries.set(key, _a.parse(JSON.stringify(value)));
94
+ return;
95
+ }
96
+ const raw = JSON.stringify(value);
97
+ if (raw === undefined)
98
+ throw new JsonDocumentError(`${key}: a ${typeof value} cannot be written as JSON`);
99
+ this.#entries.set(key, { raw });
100
+ }
101
+ /** Removes a key. */
102
+ delete(key) {
103
+ this.#entries.delete(key);
104
+ }
105
+ /** The document as npm writes a package.json: two-space indentation, a final newline. */
106
+ render() {
107
+ return `${this.#write('')}\n`;
108
+ }
109
+ toString() {
110
+ return this.render();
111
+ }
112
+ }
113
+ _a = JsonDocument;