@monosmith/plugin-sdk 0.0.0-stage → 0.2.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 (164) hide show
  1. package/README.md +90 -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 +27 -0
  29. package/codec/wire-message.mapper.js +62 -0
  30. package/concurrency/concurrency.policy.d.ts +21 -0
  31. package/concurrency/concurrency.policy.js +30 -0
  32. package/concurrency/index.d.ts +1 -0
  33. package/concurrency/index.js +1 -0
  34. package/conformance/check-generation.use-case.d.ts +12 -0
  35. package/conformance/check-generation.use-case.js +91 -0
  36. package/conformance/check-plugin.use-case.d.ts +25 -0
  37. package/conformance/check-plugin.use-case.js +81 -0
  38. package/conformance/check-scheduling.use-case.d.ts +11 -0
  39. package/conformance/check-scheduling.use-case.js +98 -0
  40. package/conformance/conformance-report.model.d.ts +22 -0
  41. package/conformance/conformance-report.model.js +23 -0
  42. package/conformance/find-manifests.algorithm.d.ts +7 -0
  43. package/conformance/find-manifests.algorithm.js +34 -0
  44. package/conformance/go-list.mapper.d.ts +4 -0
  45. package/conformance/go-list.mapper.js +8 -0
  46. package/conformance/index.d.ts +5 -0
  47. package/conformance/index.js +3 -0
  48. package/framing/frame-message.algorithm.d.ts +5 -0
  49. package/framing/frame-message.algorithm.js +8 -0
  50. package/framing/frame-reader.store.d.ts +15 -0
  51. package/framing/frame-reader.store.js +59 -0
  52. package/framing/framing.error.d.ts +4 -0
  53. package/framing/framing.error.js +7 -0
  54. package/framing/index.d.ts +3 -0
  55. package/framing/index.js +3 -0
  56. package/host/call-interrupted.error.d.ts +16 -0
  57. package/host/call-interrupted.error.js +20 -0
  58. package/host/connect-plugin.use-case.d.ts +26 -0
  59. package/host/connect-plugin.use-case.js +44 -0
  60. package/host/host-options.contract.d.ts +34 -0
  61. package/host/host-options.contract.js +4 -0
  62. package/host/index.d.ts +9 -0
  63. package/host/index.js +5 -0
  64. package/host/plugin-client.client.d.ts +61 -0
  65. package/host/plugin-client.client.js +297 -0
  66. package/host/start-plugin.use-case.d.ts +10 -0
  67. package/host/start-plugin.use-case.js +57 -0
  68. package/index.d.ts +16 -0
  69. package/index.js +21 -0
  70. package/jsondoc/indent-raw-json.algorithm.d.ts +8 -0
  71. package/jsondoc/indent-raw-json.algorithm.js +60 -0
  72. package/jsondoc/index.d.ts +2 -0
  73. package/jsondoc/index.js +2 -0
  74. package/jsondoc/json-document.error.d.ts +4 -0
  75. package/jsondoc/json-document.error.js +7 -0
  76. package/jsondoc/json-document.model.d.ts +38 -0
  77. package/jsondoc/json-document.model.js +113 -0
  78. package/jsondoc/scan-json-object.algorithm.d.ts +17 -0
  79. package/jsondoc/scan-json-object.algorithm.js +132 -0
  80. package/package.json +34 -4
  81. package/plugin-refs/index.d.ts +1 -0
  82. package/plugin-refs/index.js +1 -0
  83. package/plugin-refs/plugin-ref.policy.d.ts +9 -0
  84. package/plugin-refs/plugin-ref.policy.js +22 -0
  85. package/plugin-roles/index.d.ts +1 -0
  86. package/plugin-roles/index.js +1 -0
  87. package/plugin-roles/plugin-role.policy.d.ts +16 -0
  88. package/plugin-roles/plugin-role.policy.js +30 -0
  89. package/projects/audit-settings.policy.d.ts +5 -0
  90. package/projects/audit-settings.policy.js +8 -0
  91. package/projects/index.d.ts +6 -0
  92. package/projects/index.js +6 -0
  93. package/projects/manifest-match.algorithm.d.ts +7 -0
  94. package/projects/manifest-match.algorithm.js +38 -0
  95. package/projects/project-kind.policy.d.ts +9 -0
  96. package/projects/project-kind.policy.js +28 -0
  97. package/projects/publishes.policy.d.ts +7 -0
  98. package/projects/publishes.policy.js +8 -0
  99. package/projects/set-toolchain.algorithm.d.ts +7 -0
  100. package/projects/set-toolchain.algorithm.js +13 -0
  101. package/projects/stack-plugins.algorithm.d.ts +3 -0
  102. package/projects/stack-plugins.algorithm.js +4 -0
  103. package/protocol/index.d.ts +4 -0
  104. package/protocol/index.js +3 -0
  105. package/protocol/protocol-schema.contract.d.ts +3 -0
  106. package/protocol/protocol-schema.contract.js +1976 -0
  107. package/protocol/protocol-types.contract.d.ts +1057 -0
  108. package/protocol/protocol-types.contract.js +43 -0
  109. package/protocol/rpc-error.error.d.ts +9 -0
  110. package/protocol/rpc-error.error.js +13 -0
  111. package/protocol/schema-document.contract.d.ts +58 -0
  112. package/protocol/schema-document.contract.js +1 -0
  113. package/scaffold/index.d.ts +3 -0
  114. package/scaffold/index.js +2 -0
  115. package/scaffold/scaffold-data.mapper.d.ts +15 -0
  116. package/scaffold/scaffold-data.mapper.js +24 -0
  117. package/scaffold/scaffold-project.use-case.d.ts +20 -0
  118. package/scaffold/scaffold-project.use-case.js +13 -0
  119. package/serve/capabilities-of.algorithm.d.ts +14 -0
  120. package/serve/capabilities-of.algorithm.js +31 -0
  121. package/serve/claim-stdout.use-case.d.ts +10 -0
  122. package/serve/claim-stdout.use-case.js +14 -0
  123. package/serve/dispatch-request.use-case.d.ts +15 -0
  124. package/serve/dispatch-request.use-case.js +57 -0
  125. package/serve/index.d.ts +4 -0
  126. package/serve/index.js +2 -0
  127. package/serve/initialize-plugin.use-case.d.ts +9 -0
  128. package/serve/initialize-plugin.use-case.js +16 -0
  129. package/serve/plugin-members.config.d.ts +16 -0
  130. package/serve/plugin-members.config.js +25 -0
  131. package/serve/plugin.contract.d.ts +80 -0
  132. package/serve/plugin.contract.js +1 -0
  133. package/serve/protocol-streams.contract.d.ts +13 -0
  134. package/serve/protocol-streams.contract.js +1 -0
  135. package/serve/request-outcome.contract.d.ts +9 -0
  136. package/serve/request-outcome.contract.js +1 -0
  137. package/serve/request-pool.store.d.ts +49 -0
  138. package/serve/request-pool.store.js +110 -0
  139. package/serve/serve.use-case.d.ts +31 -0
  140. package/serve/serve.use-case.js +180 -0
  141. package/tasks/contribute-tasks.algorithm.d.ts +15 -0
  142. package/tasks/contribute-tasks.algorithm.js +22 -0
  143. package/tasks/index.d.ts +2 -0
  144. package/tasks/index.js +2 -0
  145. package/tasks/task.algorithm.d.ts +18 -0
  146. package/tasks/task.algorithm.js +23 -0
  147. package/templating/file-exists.error.d.ts +6 -0
  148. package/templating/file-exists.error.js +10 -0
  149. package/templating/index.d.ts +4 -0
  150. package/templating/index.js +4 -0
  151. package/templating/parse-template.algorithm.d.ts +11 -0
  152. package/templating/parse-template.algorithm.js +197 -0
  153. package/templating/render-template.algorithm.d.ts +7 -0
  154. package/templating/render-template.algorithm.js +87 -0
  155. package/templating/template-functions.algorithm.d.ts +10 -0
  156. package/templating/template-functions.algorithm.js +104 -0
  157. package/templating/template-syntax.contract.d.ts +33 -0
  158. package/templating/template-syntax.contract.js +1 -0
  159. package/templating/template.error.d.ts +4 -0
  160. package/templating/template.error.js +7 -0
  161. package/templating/tokenize-action.algorithm.d.ts +20 -0
  162. package/templating/tokenize-action.algorithm.js +57 -0
  163. package/templating/write-templates.use-case.d.ts +11 -0
  164. package/templating/write-templates.use-case.js +44 -0
@@ -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,16 @@
1
+ export * from './protocol/index.ts';
2
+ export * from './serve/index.ts';
3
+ export * from './concurrency/index.ts';
4
+ export * from './tasks/index.ts';
5
+ export * from './scaffold/index.ts';
6
+ export * from './ci/index.ts';
7
+ export * from './build-dir/index.ts';
8
+ export * from './plugin-roles/index.ts';
9
+ export * from './plugin-refs/index.ts';
10
+ export * from './projects/index.ts';
11
+ export * from './templating/index.ts';
12
+ export * from './jsondoc/index.ts';
13
+ export * from './host/index.ts';
14
+ export * from './conformance/index.ts';
15
+ export { decodeParams, decodeResult, encodeParams, encodeResult, EncodeError, MessageParseError, ParamsError } from './codec/index.ts';
16
+ export { FrameReader, FramingError, frameMessage } from './framing/index.ts';
package/index.js ADDED
@@ -0,0 +1,21 @@
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 './concurrency/index.js';
7
+ export * from './tasks/index.js';
8
+ export * from './scaffold/index.js';
9
+ export * from './ci/index.js';
10
+ export * from './build-dir/index.js';
11
+ export * from './plugin-roles/index.js';
12
+ export * from './plugin-refs/index.js';
13
+ export * from './projects/index.js';
14
+ export * from './templating/index.js';
15
+ export * from './jsondoc/index.js';
16
+ // Testing a plugin.
17
+ export * from './host/index.js';
18
+ export * from './conformance/index.js';
19
+ // Building blocks: the wire form of the messages, for tools that speak the protocol themselves.
20
+ export { decodeParams, decodeResult, encodeParams, encodeResult, EncodeError, MessageParseError, ParamsError } from './codec/index.js';
21
+ 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;
@@ -0,0 +1,17 @@
1
+ /** A JSON value as the document keeps it: an object edited member by member, anything else as the text it was written in. */
2
+ export type JsonNode = {
3
+ kind: 'object';
4
+ members: Array<[string, JsonNode]>;
5
+ } | {
6
+ kind: 'raw';
7
+ text: string;
8
+ };
9
+ /**
10
+ * Reads the text of a JSON object into a tree that keeps the order of its
11
+ * members and the exact text of every leaf and array (`1.0` stays `1.0`). A
12
+ * duplicate key keeps its first position and takes its last value, as the Go
13
+ * SDK's document does.
14
+ */
15
+ export declare function scanJsonObject(text: string): Extract<JsonNode, {
16
+ kind: 'object';
17
+ }>;
@@ -0,0 +1,132 @@
1
+ import { JsonDocumentError } from './json-document.error.js';
2
+ const WHITESPACE = new Set([' ', '\t', '\n', '\r']);
3
+ /**
4
+ * Reads the text of a JSON object into a tree that keeps the order of its
5
+ * members and the exact text of every leaf and array (`1.0` stays `1.0`). A
6
+ * duplicate key keeps its first position and takes its last value, as the Go
7
+ * SDK's document does.
8
+ */
9
+ export function scanJsonObject(text) {
10
+ const scanner = new Scanner(text);
11
+ scanner.skip();
12
+ if (text[scanner.position] !== '{')
13
+ throw new JsonDocumentError('not a JSON object');
14
+ const root = scanner.object();
15
+ scanner.skip();
16
+ if (scanner.position < text.length)
17
+ throw new JsonDocumentError(`unexpected "${text[scanner.position]}" after the object, at ${scanner.position}`);
18
+ return root;
19
+ }
20
+ class Scanner {
21
+ #text;
22
+ position = 0;
23
+ constructor(text) {
24
+ this.#text = text;
25
+ }
26
+ skip() {
27
+ while (this.position < this.#text.length && WHITESPACE.has(this.#text[this.position]))
28
+ this.position++;
29
+ }
30
+ object() {
31
+ const members = [];
32
+ const index = new Map();
33
+ this.position++; // {
34
+ this.skip();
35
+ if (this.#text[this.position] === '}') {
36
+ this.position++;
37
+ return { kind: 'object', members };
38
+ }
39
+ for (;;) {
40
+ this.skip();
41
+ if (this.#text[this.position] !== '"')
42
+ throw this.fail('an object key');
43
+ const key = JSON.parse(this.string());
44
+ this.skip();
45
+ if (this.#text[this.position] !== ':')
46
+ throw this.fail('":"');
47
+ this.position++;
48
+ const value = this.value();
49
+ const at = index.get(key);
50
+ if (at === undefined) {
51
+ index.set(key, members.length);
52
+ members.push([key, value]);
53
+ }
54
+ else {
55
+ members[at] = [key, value];
56
+ }
57
+ this.skip();
58
+ const next = this.#text[this.position];
59
+ if (next === '}') {
60
+ this.position++;
61
+ return { kind: 'object', members };
62
+ }
63
+ if (next !== ',')
64
+ throw this.fail('"," or "}"');
65
+ this.position++;
66
+ }
67
+ }
68
+ value() {
69
+ this.skip();
70
+ const first = this.#text[this.position];
71
+ if (first === '{')
72
+ return this.object();
73
+ const start = this.position;
74
+ if (first === '[')
75
+ this.array();
76
+ else if (first === '"')
77
+ this.string();
78
+ else
79
+ this.literal();
80
+ const text = this.#text.slice(start, this.position);
81
+ try {
82
+ JSON.parse(text);
83
+ }
84
+ catch (error) {
85
+ throw new JsonDocumentError(`invalid JSON value "${text.slice(0, 40)}" at ${start}`, { cause: error });
86
+ }
87
+ return { kind: 'raw', text };
88
+ }
89
+ /** Moves past an array, whose elements stay text. */
90
+ array() {
91
+ let depth = 0;
92
+ while (this.position < this.#text.length) {
93
+ const c = this.#text[this.position];
94
+ if (c === '"') {
95
+ this.string();
96
+ continue;
97
+ }
98
+ if (c === '[' || c === '{')
99
+ depth++;
100
+ else if (c === ']' || c === '}')
101
+ depth--;
102
+ this.position++;
103
+ if (depth === 0)
104
+ return;
105
+ }
106
+ throw new JsonDocumentError('unterminated array');
107
+ }
108
+ /** Moves past a string literal and returns its text, quotes included. */
109
+ string() {
110
+ const start = this.position;
111
+ this.position++;
112
+ while (this.position < this.#text.length) {
113
+ const c = this.#text[this.position++];
114
+ if (c === '\\')
115
+ this.position++;
116
+ else if (c === '"')
117
+ return this.#text.slice(start, this.position);
118
+ }
119
+ throw new JsonDocumentError('unterminated string');
120
+ }
121
+ literal() {
122
+ const start = this.position;
123
+ while (this.position < this.#text.length && !WHITESPACE.has(this.#text[this.position]) && !',}]'.includes(this.#text[this.position]))
124
+ this.position++;
125
+ if (this.position === start)
126
+ throw this.fail('a value');
127
+ }
128
+ fail(expected) {
129
+ const found = this.position < this.#text.length ? `"${this.#text[this.position]}"` : 'the end of the text';
130
+ return new JsonDocumentError(`expected ${expected}, found ${found} at ${this.position}`);
131
+ }
132
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@monosmith/plugin-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "The TypeScript SDK for writing smith plugins: the protocol's types, the serve loop, a test host, the conformance checks, templating and key-order-keeping JSON editing. No runtime dependencies.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ "default": "./index.js",
9
+ "types": "./index.d.ts"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "engines": {
15
+ "node": ">=22"
16
+ },
17
+ "keywords": [
18
+ "smith",
19
+ "monosmith",
20
+ "plugin",
21
+ "sdk",
22
+ "json-rpc"
23
+ ],
24
+ "author": "Eduardo Russo",
25
+ "homepage": "https://github.com/monosmith-app/monosmith/tree/main/packages/plugin-sdk-ts#readme",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/monosmith-app/monosmith.git",
29
+ "directory": "packages/plugin-sdk-ts"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/monosmith-app/monosmith/issues"
33
+ },
34
+ "main": "./index.js",
35
+ "types": "./index.d.ts"
36
+ }
@@ -0,0 +1 @@
1
+ export { conflictsWith, describeRef, isEmptyRef, requiresLanguage } from './plugin-ref.policy.ts';
@@ -0,0 +1 @@
1
+ export { conflictsWith, describeRef, isEmptyRef, requiresLanguage } from './plugin-ref.policy.js';
@@ -0,0 +1,9 @@
1
+ import type { PluginRef } from '../protocol/index.ts';
2
+ /** A requirement on the project's language plugin: `requiresLanguage('node')`. */
3
+ export declare function requiresLanguage(name: string): PluginRef;
4
+ /** A plugin that must not be in the same project. */
5
+ export declare function conflictsWith(name: string): PluginRef;
6
+ /** Whether the reference names nothing: every member of a reference is optional, and at least one must be set. */
7
+ export declare function isEmptyRef(ref: PluginRef): boolean;
8
+ /** How a message names it: `language plugin node`, `plugin providing test`. */
9
+ export declare function describeRef(ref: PluginRef): string;
@@ -0,0 +1,22 @@
1
+ /** A requirement on the project's language plugin: `requiresLanguage('node')`. */
2
+ export function requiresLanguage(name) {
3
+ return { level: 'language', name };
4
+ }
5
+ /** A plugin that must not be in the same project. */
6
+ export function conflictsWith(name) {
7
+ return { name };
8
+ }
9
+ /** Whether the reference names nothing: every member of a reference is optional, and at least one must be set. */
10
+ export function isEmptyRef(ref) {
11
+ return (ref.level ?? '') === '' && (ref.name ?? '') === '' && (ref.slot ?? '') === '';
12
+ }
13
+ /** How a message names it: `language plugin node`, `plugin providing test`. */
14
+ export function describeRef(ref) {
15
+ const level = ref.level ?? '';
16
+ const parts = [level === '' ? 'plugin' : `${level} plugin`];
17
+ if (ref.name !== undefined && ref.name !== '')
18
+ parts.push(ref.name);
19
+ if (ref.slot !== undefined && ref.slot !== '')
20
+ parts.push(`providing ${ref.slot}`);
21
+ return parts.join(' ');
22
+ }
@@ -0,0 +1 @@
1
+ export { ROLES, defaultRoles, effectiveRoles, isAdditiveRole, isExclusiveRole, isKnownRole } from './plugin-role.policy.ts';
@@ -0,0 +1 @@
1
+ export { ROLES, defaultRoles, effectiveRoles, isAdditiveRole, isExclusiveRole, isKnownRole } from './plugin-role.policy.js';
@@ -0,0 +1,16 @@
1
+ import type { InitializeResult, PluginKind, Role } from '../protocol/index.ts';
2
+ /** Every known role, in the order a project's make-up lists them. */
3
+ export declare const ROLES: readonly Role[];
4
+ /** Whether the role is one of {@link ROLES}. */
5
+ export declare function isKnownRole(role: string): role is Role;
6
+ /** Whether a project has at most one plugin at this level (language, scaffold). A toolchain is exclusive per slot instead. */
7
+ export declare function isExclusiveRole(role: Role): boolean;
8
+ /** Whether any number of plugins can share the level, each only adding (structure, deploy). */
9
+ export declare function isAdditiveRole(role: Role): boolean;
10
+ /**
11
+ * The roles of a plugin that declares none: a language plugin brings its own
12
+ * default toolchain and scaffold; any other kind of plugin is a structure rule.
13
+ */
14
+ export declare function defaultRoles(kind: PluginKind): Role[];
15
+ /** The plugin's roles, or the default of its kind. */
16
+ export declare function effectiveRoles(info: Pick<InitializeResult, 'kind' | 'roles'>): Role[];
@@ -0,0 +1,30 @@
1
+ import { protocolSchema } from '../protocol/index.js';
2
+ /** Every known role, in the order a project's make-up lists them. */
3
+ export const ROLES = protocolSchema['x-roles'].map(row => row.name);
4
+ /** Whether the role is one of {@link ROLES}. */
5
+ export function isKnownRole(role) {
6
+ return ROLES.includes(role);
7
+ }
8
+ /** Whether a project has at most one plugin at this level (language, scaffold). A toolchain is exclusive per slot instead. */
9
+ export function isExclusiveRole(role) {
10
+ return cardinalityOf(role) === 'exclusive';
11
+ }
12
+ /** Whether any number of plugins can share the level, each only adding (structure, deploy). */
13
+ export function isAdditiveRole(role) {
14
+ return cardinalityOf(role) === 'additive';
15
+ }
16
+ function cardinalityOf(role) {
17
+ return protocolSchema['x-roles'].find(row => row.name === role)?.cardinality;
18
+ }
19
+ /**
20
+ * The roles of a plugin that declares none: a language plugin brings its own
21
+ * default toolchain and scaffold; any other kind of plugin is a structure rule.
22
+ */
23
+ export function defaultRoles(kind) {
24
+ const wanted = kind === 'language' ? 'language' : 'convention';
25
+ return protocolSchema['x-roles'].filter(row => row.defaultFor.includes(wanted)).map(row => row.name);
26
+ }
27
+ /** The plugin's roles, or the default of its kind. */
28
+ export function effectiveRoles(info) {
29
+ return info.roles !== undefined && info.roles.length > 0 ? info.roles : defaultRoles(info.kind);
30
+ }
@@ -0,0 +1,5 @@
1
+ import type { AuditSettings } from '../protocol/index.ts';
2
+ /** Whether the ecosystem's (`node`, `go`, `python`, `rust`) audit policy checks are on. */
3
+ export declare function auditEnabled(audit: AuditSettings | undefined, policy: string): boolean;
4
+ /** Whether a failing check of the policy fails the audit: it does unless the workspace made the policy only warn. */
5
+ export declare function auditBlocks(audit: AuditSettings | undefined, policy: string): boolean;
@@ -0,0 +1,8 @@
1
+ /** Whether the ecosystem's (`node`, `go`, `python`, `rust`) audit policy checks are on. */
2
+ export function auditEnabled(audit, policy) {
3
+ return audit?.policies?.includes(policy) ?? false;
4
+ }
5
+ /** Whether a failing check of the policy fails the audit: it does unless the workspace made the policy only warn. */
6
+ export function auditBlocks(audit, policy) {
7
+ return !(audit?.warn?.includes(policy) ?? false);
8
+ }
@@ -0,0 +1,6 @@
1
+ export { auditBlocks, auditEnabled } from './audit-settings.policy.ts';
2
+ export { isManifest } from './manifest-match.algorithm.ts';
3
+ export { projectKindDirectory, resolveProjectKind } from './project-kind.policy.ts';
4
+ export { publishes } from './publishes.policy.ts';
5
+ export { setToolchain } from './set-toolchain.algorithm.ts';
6
+ export { stackPlugins } from './stack-plugins.algorithm.ts';