@nullplatform/plugin 0.0.2
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/README.md +85 -0
- package/package.json +51 -0
- package/src/api.ts +206 -0
- package/src/create-plugin.ts +24 -0
- package/src/index.ts +29 -0
- package/src/internal/grpc-server.ts +336 -0
- package/src/internal/manifest.ts +50 -0
- package/src/internal/plugin.proto +78 -0
- package/src/schema.ts +54 -0
- package/src/scope/actions.ts +50 -0
- package/src/scope/describe.ts +40 -0
- package/src/scope/index.ts +259 -0
- package/src/scope/lifecycle.ts +39 -0
- package/src/scope/publish.ts +324 -0
- package/src/testing/factories.ts +133 -0
- package/src/testing/index.ts +27 -0
- package/src/testing/mock-request.ts +20 -0
- package/src/testing/plugin-client.ts +219 -0
- package/src/testing/shell.ts +22 -0
- package/src/types.ts +46 -0
- package/src/workflow.ts +91 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<h2 align="center">
|
|
2
|
+
<a href="https://nullplatform.com" target="blank_">
|
|
3
|
+
<img height="100" alt="Nullplatform" src="https://nullplatform.com/favicon/android-chrome-192x192.png" />
|
|
4
|
+
</a>
|
|
5
|
+
<br>
|
|
6
|
+
<br>
|
|
7
|
+
@nullplatform/plugin
|
|
8
|
+
<br>
|
|
9
|
+
</h2>
|
|
10
|
+
|
|
11
|
+
TypeScript SDK for building nullplatform plugins. Handles gRPC transport (HashiCorp go-plugin protocol), action routing, `--describe`, and `--publish`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @nullplatform/plugin
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Scope plugin
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { defineScope } from "@nullplatform/plugin/scope";
|
|
25
|
+
|
|
26
|
+
defineScope({
|
|
27
|
+
name: "kubernetes-k3d",
|
|
28
|
+
version: "0.0.1",
|
|
29
|
+
category: "containers",
|
|
30
|
+
provider: "kubernetes",
|
|
31
|
+
|
|
32
|
+
schema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
memory: { type: "string", default: "2Gi" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
actions: {
|
|
40
|
+
"create-scope": {
|
|
41
|
+
input: { type: "object" },
|
|
42
|
+
handler: async (notification, emit) => {
|
|
43
|
+
// provision infrastructure
|
|
44
|
+
return { domain: "app.k3d.local" };
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Simple plugin (custom command)
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { createPlugin, registerManifest } from "@nullplatform/plugin";
|
|
55
|
+
|
|
56
|
+
registerManifest({ name: "my-plugin", version: "0.0.1", command_types: ["custom"] });
|
|
57
|
+
|
|
58
|
+
createPlugin({
|
|
59
|
+
async execute(req) {
|
|
60
|
+
return { success: true, data: { message: "done" } };
|
|
61
|
+
},
|
|
62
|
+
}).start();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Subpath exports
|
|
66
|
+
|
|
67
|
+
| Import | Description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `@nullplatform/plugin` | Core: createPlugin, registerManifest, types |
|
|
70
|
+
| `@nullplatform/plugin/scope` | defineScope API |
|
|
71
|
+
| `@nullplatform/plugin/schema` | JSON Schema type inference |
|
|
72
|
+
| `@nullplatform/plugin/testing` | Test harness: startPluginProcess, factories |
|
|
73
|
+
| `@nullplatform/plugin/workflow` | Workflow integration (StreamingExecutionObserver) |
|
|
74
|
+
|
|
75
|
+
## CLI integration
|
|
76
|
+
|
|
77
|
+
Plugins are scaffolded with `np plugin init` and managed via mise tasks:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
mise run dev # local dev with hot reload
|
|
81
|
+
mise run dev:agent # dev with np-agent attached
|
|
82
|
+
mise run test # run tests
|
|
83
|
+
mise run build # compile to standalone binary
|
|
84
|
+
mise run publish # publish to platform
|
|
85
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nullplatform/plugin",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./schema": "./src/schema.ts",
|
|
10
|
+
"./scope": "./src/scope/index.ts",
|
|
11
|
+
"./testing": "./src/testing/index.ts",
|
|
12
|
+
"./workflow": "./src/workflow.ts"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@grpc/grpc-js": "^1.13.0",
|
|
16
|
+
"@grpc/proto-loader": "^0.7.0",
|
|
17
|
+
"yaml": "^2.7.0"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"registry": "https://registry.npmjs.org"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/nullplatform/plugin-libraries.git",
|
|
28
|
+
"directory": "js"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@nullplatform/workflow-engine": ">=0.0.1 <1.0.0",
|
|
33
|
+
"@nullplatform/workflow-engine-in-memory": ">=0.0.1 <1.0.0",
|
|
34
|
+
"@nullplatform/workflow-types": ">=0.0.1 <1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"@nullplatform/workflow-engine": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
40
|
+
"@nullplatform/workflow-engine-in-memory": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"@nullplatform/workflow-types": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/bun": "^1.2.0",
|
|
49
|
+
"json-schema-to-ts": "^3.1.1"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform API helpers — typed wrappers around nullplatform REST API.
|
|
3
|
+
*
|
|
4
|
+
* Generic helpers usable by any plugin type (scope, service, etc.).
|
|
5
|
+
* Replicates what `np service workflow build-context` does in the Go CLI.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { api } from "@nullplatform/plugin";
|
|
9
|
+
*
|
|
10
|
+
* const scope = await api.scope("12345");
|
|
11
|
+
* const deployment = await api.deployment("67890");
|
|
12
|
+
* const providers = await api.providers(scope.nrn, ["cloud-providers"]);
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ─── Configuration ───────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const TIMEOUT_MS = 30_000;
|
|
18
|
+
|
|
19
|
+
function getConfig() {
|
|
20
|
+
const apiUrl = process.env.NP_API_URL ?? "https://api.nullplatform.com";
|
|
21
|
+
const apiKey = process.env.NULLPLATFORM_APIKEY ?? "";
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
throw new Error("NULLPLATFORM_APIKEY environment variable is not set");
|
|
24
|
+
}
|
|
25
|
+
return { apiUrl, apiKey };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function fetchJson<T>(path: string, params?: Record<string, string>): Promise<T> {
|
|
29
|
+
const { apiUrl, apiKey } = getConfig();
|
|
30
|
+
const url = new URL(path, apiUrl);
|
|
31
|
+
if (params) {
|
|
32
|
+
for (const [k, v] of Object.entries(params)) {
|
|
33
|
+
if (v !== undefined && v !== "") url.searchParams.set(k, v);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(url.toString(), {
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${apiKey}`,
|
|
44
|
+
Accept: "application/json",
|
|
45
|
+
},
|
|
46
|
+
signal: controller.signal,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const body = await res.text().catch(() => "");
|
|
51
|
+
throw new Error(`API ${res.status} ${res.statusText}: ${path}${body ? ` — ${body}` : ""}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return res.json() as Promise<T>;
|
|
55
|
+
} finally {
|
|
56
|
+
clearTimeout(timeoutId);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Entity types ────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export interface ScopeInfo {
|
|
63
|
+
id: string;
|
|
64
|
+
slug: string;
|
|
65
|
+
nrn: string;
|
|
66
|
+
domain: string;
|
|
67
|
+
asset_name: string;
|
|
68
|
+
current_active_deployment: string;
|
|
69
|
+
capabilities: Record<string, unknown>;
|
|
70
|
+
dimensions: Record<string, string>;
|
|
71
|
+
[key: string]: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DeploymentInfo {
|
|
75
|
+
id: string;
|
|
76
|
+
status: string;
|
|
77
|
+
release_id: string;
|
|
78
|
+
strategy_data: {
|
|
79
|
+
desired_switched_traffic?: number;
|
|
80
|
+
};
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ApplicationInfo {
|
|
85
|
+
id: string;
|
|
86
|
+
slug: string;
|
|
87
|
+
[key: string]: unknown;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface NamespaceInfo {
|
|
91
|
+
id: string;
|
|
92
|
+
slug: string;
|
|
93
|
+
[key: string]: unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AccountInfo {
|
|
97
|
+
id: string;
|
|
98
|
+
slug: string;
|
|
99
|
+
[key: string]: unknown;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AssetInfo {
|
|
103
|
+
type: string;
|
|
104
|
+
url: string;
|
|
105
|
+
[key: string]: unknown;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ParameterResult {
|
|
109
|
+
variable: string;
|
|
110
|
+
values: Array<{ value: string }>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type ProvidersMap = Record<string, Record<string, unknown>>;
|
|
114
|
+
|
|
115
|
+
// ─── API functions ───────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export async function scope(id: string): Promise<ScopeInfo> {
|
|
118
|
+
return fetchJson<ScopeInfo>(`/scope/${id}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function deployment(id: string): Promise<DeploymentInfo> {
|
|
122
|
+
return fetchJson<DeploymentInfo>(`/deployment/${id}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function application(id: string): Promise<ApplicationInfo> {
|
|
126
|
+
return fetchJson<ApplicationInfo>(`/application/${id}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function namespace(id: string): Promise<NamespaceInfo> {
|
|
130
|
+
return fetchJson<NamespaceInfo>(`/namespace/${id}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function account(id: string): Promise<AccountInfo> {
|
|
134
|
+
return fetchJson<AccountInfo>(`/account/${id}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fetch asset by following the release → build → asset chain.
|
|
139
|
+
* Mirrors the Go CLI's fetchAndStoreAsset logic.
|
|
140
|
+
*/
|
|
141
|
+
export async function asset(releaseId: string, assetName: string): Promise<AssetInfo> {
|
|
142
|
+
const release = await fetchJson<{ build_id: string }>(`/release/${releaseId}`);
|
|
143
|
+
const result = await fetchJson<{ results: AssetInfo[] }>("/asset", {
|
|
144
|
+
build_id: release.build_id,
|
|
145
|
+
name: assetName,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!result.results?.length) {
|
|
149
|
+
throw new Error(`No asset found for build_id=${release.build_id}, name=${assetName}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result.results[0];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fetch parameters for a scope by NRN.
|
|
157
|
+
* Mirrors --include-secrets flag from build-context.
|
|
158
|
+
*/
|
|
159
|
+
export async function parameters(
|
|
160
|
+
nrn: string,
|
|
161
|
+
opts: { includeSecrets?: boolean } = {},
|
|
162
|
+
): Promise<Record<string, string>> {
|
|
163
|
+
const result = await fetchJson<{ results: ParameterResult[] }>("/parameter", {
|
|
164
|
+
nrn,
|
|
165
|
+
show_secret_values: String(opts.includeSecrets ?? false),
|
|
166
|
+
interpolate: "true",
|
|
167
|
+
limit: "500",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const params: Record<string, string> = {};
|
|
171
|
+
for (const p of result.results ?? []) {
|
|
172
|
+
if (p.variable && p.values?.[0]?.value !== undefined) {
|
|
173
|
+
params[p.variable] = p.values[0].value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return params;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Fetch providers by NRN and categories.
|
|
181
|
+
* Mirrors --provider-categories flag from build-context.
|
|
182
|
+
* Returns providers keyed by category slug with their attributes.
|
|
183
|
+
*/
|
|
184
|
+
export async function providers(
|
|
185
|
+
nrn: string,
|
|
186
|
+
categories: string[],
|
|
187
|
+
dimensions?: Record<string, string>,
|
|
188
|
+
): Promise<ProvidersMap> {
|
|
189
|
+
const dimensionFilter = dimensions
|
|
190
|
+
? Object.entries(dimensions).map(([k, v]) => `${k}:${v}`).join(",")
|
|
191
|
+
: undefined;
|
|
192
|
+
|
|
193
|
+
const result = await fetchJson<{
|
|
194
|
+
results: Array<{ category: string; attributes: Record<string, unknown> }>;
|
|
195
|
+
}>("/provider", {
|
|
196
|
+
nrn,
|
|
197
|
+
categories: categories.join(","),
|
|
198
|
+
...(dimensionFilter ? { dimensions: dimensionFilter } : {}),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const map: ProvidersMap = {};
|
|
202
|
+
for (const provider of result.results ?? []) {
|
|
203
|
+
map[provider.category] = provider.attributes ?? {};
|
|
204
|
+
}
|
|
205
|
+
return map;
|
|
206
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createPlugin() — the public entry point for building np-agent plugins.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* createPlugin({
|
|
6
|
+
* async execute(req) {
|
|
7
|
+
* // do work
|
|
8
|
+
* return { success: true, data: { ... } };
|
|
9
|
+
* }
|
|
10
|
+
* }).start();
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PluginHandler, Plugin } from "./types";
|
|
14
|
+
import { loadManifest } from "./internal/manifest";
|
|
15
|
+
import { startGrpcServer } from "./internal/grpc-server";
|
|
16
|
+
|
|
17
|
+
export function createPlugin(handler: PluginHandler): Plugin {
|
|
18
|
+
return {
|
|
19
|
+
start() {
|
|
20
|
+
const manifest = loadManifest();
|
|
21
|
+
startGrpcServer(handler, manifest);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nullplatform/plugin — gRPC transport for np-agent plugins.
|
|
3
|
+
*
|
|
4
|
+
* This SDK is ONLY the transport layer. It handles:
|
|
5
|
+
* - go-plugin handshake
|
|
6
|
+
* - gRPC Execute/Capabilities/Health/Version
|
|
7
|
+
* - Plugin manifest loading
|
|
8
|
+
*
|
|
9
|
+
* For progress tracking, use @nullplatform/workflow.
|
|
10
|
+
* For orchestration, use @nullplatform/workflow.
|
|
11
|
+
* They compose — a plugin CAN use the workflow SDK, but doesn't have to.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { createPlugin } from "./create-plugin";
|
|
15
|
+
export { registerManifest } from "./internal/manifest";
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
PluginHandler,
|
|
19
|
+
Plugin,
|
|
20
|
+
ExecuteRequest,
|
|
21
|
+
ExecuteResult,
|
|
22
|
+
ExecuteOutput,
|
|
23
|
+
PluginManifest,
|
|
24
|
+
} from "./types";
|
|
25
|
+
|
|
26
|
+
export * as api from "./api";
|
|
27
|
+
|
|
28
|
+
export type { NpJSONSchema, NpKeywords } from "./schema";
|
|
29
|
+
export type { infer as InferSchema } from "./schema";
|