@weiyentan/opencode-plugin-awx 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.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/auth.d.ts +112 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +180 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +148 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +334 -0
- package/dist/client.js.map +1 -0
- package/dist/contracts/job-detail.d.ts +141 -0
- package/dist/contracts/job-detail.d.ts.map +1 -0
- package/dist/contracts/job-detail.js +98 -0
- package/dist/contracts/job-detail.js.map +1 -0
- package/dist/contracts/sync-project.d.ts +31 -0
- package/dist/contracts/sync-project.d.ts.map +1 -0
- package/dist/contracts/sync-project.js +30 -0
- package/dist/contracts/sync-project.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +754 -0
- package/dist/index.js.map +1 -0
- package/dist/job-status.d.ts +116 -0
- package/dist/job-status.d.ts.map +1 -0
- package/dist/job-status.js +168 -0
- package/dist/job-status.js.map +1 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.d.ts.map +1 -0
- package/dist/launch.js +133 -0
- package/dist/launch.js.map +1 -0
- package/dist/list-projects.d.ts +63 -0
- package/dist/list-projects.d.ts.map +1 -0
- package/dist/list-projects.js +84 -0
- package/dist/list-projects.js.map +1 -0
- package/dist/list-templates.d.ts +60 -0
- package/dist/list-templates.d.ts.map +1 -0
- package/dist/list-templates.js +120 -0
- package/dist/list-templates.js.map +1 -0
- package/dist/metrics.d.ts +174 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +275 -0
- package/dist/metrics.js.map +1 -0
- package/dist/transforms.d.ts +52 -0
- package/dist/transforms.d.ts.map +1 -0
- package/dist/transforms.js +108 -0
- package/dist/transforms.js.map +1 -0
- package/package.json +56 -0
package/dist/metrics.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* metrics.ts — Per-tool counters with file-backed durability
|
|
3
|
+
*
|
|
4
|
+
* Provides structured metrics for operational visibility and phase-gating:
|
|
5
|
+
* - Per-tool call count
|
|
6
|
+
* - Per-tool error count
|
|
7
|
+
* - Per-tool latency accumulation (ms)
|
|
8
|
+
* - Token expiry events (401 detection)
|
|
9
|
+
* - PowerShell fallback count (for deprecation monitoring)
|
|
10
|
+
*
|
|
11
|
+
* ## Durability Model
|
|
12
|
+
*
|
|
13
|
+
* Counters are file-backed so they survive plugin reloads. This is required
|
|
14
|
+
* for the Phase 2→3 gate which demands 14 consecutive days of zero PowerShell
|
|
15
|
+
* AWX calls.
|
|
16
|
+
*
|
|
17
|
+
* **File format:** JSON at a configurable path (default: `.metrics/metrics.json`).
|
|
18
|
+
* **Atomic writes:** Data is written to a `.tmp` file first, then renamed over
|
|
19
|
+
* the target — preventing corruption on partial writes.
|
|
20
|
+
* **Merge-on-load:** When `load()` is called, disk values are merged with
|
|
21
|
+
* in-memory counters using `Math.max()` — counters never decrease.
|
|
22
|
+
* **Missing file:** Treated as a fresh start (no error thrown).
|
|
23
|
+
*
|
|
24
|
+
* ## Integration Point
|
|
25
|
+
*
|
|
26
|
+
* Metrics hook into the client module at pipeline boundaries — not inside
|
|
27
|
+
* individual middleware. The `client.ts` request function calls `recordCall`,
|
|
28
|
+
* `recordError`, and `recordTokenExpiry` at the top level.
|
|
29
|
+
*
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // In client.ts (pipeline boundary):
|
|
32
|
+
* const start = Date.now();
|
|
33
|
+
* try {
|
|
34
|
+
* const response = await fetch(...);
|
|
35
|
+
* metrics.recordCall(toolName, Date.now() - start);
|
|
36
|
+
* if (response.status === 401) metrics.recordTokenExpiry(toolName);
|
|
37
|
+
* return response;
|
|
38
|
+
* } catch (err) {
|
|
39
|
+
* metrics.recordError(toolName);
|
|
40
|
+
* throw err;
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
// ——— Factory ———
|
|
45
|
+
/** Create a zeroed ToolMetrics object */
|
|
46
|
+
export function createDefaultMetrics() {
|
|
47
|
+
return {
|
|
48
|
+
callCount: 0,
|
|
49
|
+
errorCount: 0,
|
|
50
|
+
totalLatencyMs: 0,
|
|
51
|
+
tokenExpiryEvents: 0,
|
|
52
|
+
psFallbackCount: 0,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ——— MetricsStore ———
|
|
56
|
+
/**
|
|
57
|
+
* Thread-safe (in-memory) metrics store with file-backed durability.
|
|
58
|
+
*
|
|
59
|
+
* All recording methods are synchronous and fast — they mutate an in-memory
|
|
60
|
+
* Map. Persistence is explicit (call `persist()`) so the caller controls when
|
|
61
|
+
* to flush to disk.
|
|
62
|
+
*
|
|
63
|
+
* The durability model is **additive-merge**: on `load()`, existing in-memory
|
|
64
|
+
* counters are never decreased. This prevents race conditions where an
|
|
65
|
+
* in-memory increment during a concurrent persist window would be lost.
|
|
66
|
+
*/
|
|
67
|
+
export class MetricsStore {
|
|
68
|
+
/** Per-tool counters, keyed by tool name */
|
|
69
|
+
counters = new Map();
|
|
70
|
+
/** File path for persistence (default: `.metrics/metrics.json`) */
|
|
71
|
+
persistPath;
|
|
72
|
+
/**
|
|
73
|
+
* @param persistPath - Absolute or relative path to the metrics JSON file.
|
|
74
|
+
* Defaults to `.metrics/metrics.json` relative to the current working directory.
|
|
75
|
+
*/
|
|
76
|
+
constructor(persistPath) {
|
|
77
|
+
this.persistPath = persistPath ?? ".metrics/metrics.json";
|
|
78
|
+
}
|
|
79
|
+
// ——— Private helpers ———
|
|
80
|
+
/** Get or create the ToolMetrics entry for a tool name */
|
|
81
|
+
ensure(toolName) {
|
|
82
|
+
let metrics = this.counters.get(toolName);
|
|
83
|
+
if (!metrics) {
|
|
84
|
+
metrics = createDefaultMetrics();
|
|
85
|
+
this.counters.set(toolName, metrics);
|
|
86
|
+
}
|
|
87
|
+
return metrics;
|
|
88
|
+
}
|
|
89
|
+
// ——— Recording methods ———
|
|
90
|
+
/**
|
|
91
|
+
* Record a tool call with its latency.
|
|
92
|
+
* Called by the client at the pipeline boundary after a successful (or
|
|
93
|
+
* error-handled) fetch completes.
|
|
94
|
+
*
|
|
95
|
+
* @param toolName - The registered tool name (e.g., "list-templates", "launch-job")
|
|
96
|
+
* @param latencyMs - Round-trip latency in milliseconds
|
|
97
|
+
*/
|
|
98
|
+
recordCall(toolName, latencyMs) {
|
|
99
|
+
const m = this.ensure(toolName);
|
|
100
|
+
m.callCount++;
|
|
101
|
+
m.totalLatencyMs += latencyMs;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Record an error for a tool.
|
|
105
|
+
* Called by the client when a fetch request fails (any error: 4xx, 5xx,
|
|
106
|
+
* network error, timeout, abort).
|
|
107
|
+
*/
|
|
108
|
+
recordError(toolName) {
|
|
109
|
+
const m = this.ensure(toolName);
|
|
110
|
+
m.errorCount++;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Record a token expiry event (HTTP 401).
|
|
114
|
+
* Called by the client when a request returns 401 Unauthorized, indicating
|
|
115
|
+
* the bearer token has expired or been revoked.
|
|
116
|
+
*/
|
|
117
|
+
recordTokenExpiry(toolName) {
|
|
118
|
+
const m = this.ensure(toolName);
|
|
119
|
+
m.tokenExpiryEvents++;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Record a PowerShell fallback for a tool.
|
|
123
|
+
* Called when the plugin tool cannot complete the operation and falls back
|
|
124
|
+
* to the legacy PowerShell script. This counter is tracked per-tool for
|
|
125
|
+
* granular deprecation monitoring (Phase 2→3 gate requires zero PS calls
|
|
126
|
+
* across all tools for 14 consecutive days).
|
|
127
|
+
*/
|
|
128
|
+
recordPsFallback(toolName) {
|
|
129
|
+
const m = this.ensure(toolName);
|
|
130
|
+
m.psFallbackCount++;
|
|
131
|
+
}
|
|
132
|
+
// ——— Read methods ———
|
|
133
|
+
/**
|
|
134
|
+
* Get metrics for a specific tool.
|
|
135
|
+
* Returns a default zeroed object if the tool has never been recorded.
|
|
136
|
+
*/
|
|
137
|
+
getMetrics(toolName) {
|
|
138
|
+
const m = this.counters.get(toolName);
|
|
139
|
+
if (!m)
|
|
140
|
+
return createDefaultMetrics();
|
|
141
|
+
// Return a shallow copy so callers can't mutate the store
|
|
142
|
+
return { ...m };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get all tracked tools' metrics as a plain object.
|
|
146
|
+
* Returns deep copies — mutations do not affect the store.
|
|
147
|
+
*/
|
|
148
|
+
getAllMetrics() {
|
|
149
|
+
const result = {};
|
|
150
|
+
for (const [name, metrics] of this.counters) {
|
|
151
|
+
result[name] = { ...metrics };
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
// ——— Persistence ———
|
|
156
|
+
/**
|
|
157
|
+
* Persist current metrics to disk using an atomic write strategy.
|
|
158
|
+
*
|
|
159
|
+
* **Atomicity**: Data is written to a `.tmp` file first, then renamed
|
|
160
|
+
* over the target. If the process crashes during the write, the original
|
|
161
|
+
* file remains intact.
|
|
162
|
+
*
|
|
163
|
+
* **Directory creation**: The parent directory is created recursively if
|
|
164
|
+
* it doesn't exist.
|
|
165
|
+
*/
|
|
166
|
+
async persist() {
|
|
167
|
+
// Dynamic imports keep the module dependency-free at the type level
|
|
168
|
+
// and avoid bundling fs/promises for environments that don't need it.
|
|
169
|
+
const fs = await import("fs/promises");
|
|
170
|
+
const path = await import("path");
|
|
171
|
+
const dir = path.dirname(this.persistPath);
|
|
172
|
+
await fs.mkdir(dir, { recursive: true });
|
|
173
|
+
const data = {
|
|
174
|
+
version: 1,
|
|
175
|
+
updatedAt: new Date().toISOString(),
|
|
176
|
+
tools: Object.fromEntries(this.counters),
|
|
177
|
+
};
|
|
178
|
+
const tmpPath = this.persistPath + ".tmp";
|
|
179
|
+
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), "utf-8");
|
|
180
|
+
await fs.rename(tmpPath, this.persistPath);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Load metrics from disk and merge with in-memory counters.
|
|
184
|
+
*
|
|
185
|
+
* **Merge strategy (additive)**: For each tool, each counter is set to
|
|
186
|
+
* `Math.max(inMemory, onDisk)`. This ensures:
|
|
187
|
+
* - Counters never decrease (no lost increments).
|
|
188
|
+
* - Concurrent increments during a load window are preserved.
|
|
189
|
+
*
|
|
190
|
+
* **Missing file**: Treated as a fresh start (no error). In-memory
|
|
191
|
+
* counters are left unchanged — a subsequent `persist()` will create
|
|
192
|
+
* the file.
|
|
193
|
+
*/
|
|
194
|
+
async load() {
|
|
195
|
+
const fs = await import("fs/promises");
|
|
196
|
+
let raw;
|
|
197
|
+
try {
|
|
198
|
+
raw = await fs.readFile(this.persistPath, "utf-8");
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
if (typeof err === "object" &&
|
|
202
|
+
err !== null &&
|
|
203
|
+
"code" in err &&
|
|
204
|
+
err.code === "ENOENT") {
|
|
205
|
+
// File doesn't exist — fresh start, keep in-memory counters
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
const data = JSON.parse(raw);
|
|
211
|
+
if (data.tools) {
|
|
212
|
+
for (const [name, saved] of Object.entries(data.tools)) {
|
|
213
|
+
const existing = this.ensure(name);
|
|
214
|
+
// Merge: take the maximum of each counter (never decrease)
|
|
215
|
+
existing.callCount = Math.max(existing.callCount, saved.callCount ?? 0);
|
|
216
|
+
existing.errorCount = Math.max(existing.errorCount, saved.errorCount ?? 0);
|
|
217
|
+
existing.totalLatencyMs = Math.max(existing.totalLatencyMs, saved.totalLatencyMs ?? 0);
|
|
218
|
+
existing.tokenExpiryEvents = Math.max(existing.tokenExpiryEvents, saved.tokenExpiryEvents ?? 0);
|
|
219
|
+
existing.psFallbackCount = Math.max(existing.psFallbackCount, saved.psFallbackCount ?? 0);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ——— Lifecycle ———
|
|
224
|
+
/**
|
|
225
|
+
* Reset all counters to zero.
|
|
226
|
+
* Useful for testing or for clearing metrics between sessions.
|
|
227
|
+
*/
|
|
228
|
+
reset() {
|
|
229
|
+
this.counters.clear();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// ——— Lifecycle helper ———
|
|
233
|
+
/**
|
|
234
|
+
* Set up periodic persistence for a MetricsStore.
|
|
235
|
+
*
|
|
236
|
+
* Starts a `setInterval` that calls `store.persist()` at the given interval.
|
|
237
|
+
* Returns a `clear()` function that stops the interval and does a final
|
|
238
|
+
* persist to ensure in-memory counters are flushed to disk.
|
|
239
|
+
*
|
|
240
|
+
* This is the integration point for the plugin lifecycle:
|
|
241
|
+
* 1. Plugin's `server()` creates a `MetricsStore` and calls `store.load()`.
|
|
242
|
+
* 2. Plugin's `server()` calls this helper to start periodic persistence.
|
|
243
|
+
* 3. Plugin's `dispose()` hook calls `clear()` to stop the interval and
|
|
244
|
+
* perform a final persist.
|
|
245
|
+
*
|
|
246
|
+
* @param store - The MetricsStore to persist periodically
|
|
247
|
+
* @param intervalMs - Interval in milliseconds (default: 30_000 = 30s)
|
|
248
|
+
* @param onError - Optional callback invoked when a persist attempt fails.
|
|
249
|
+
* Receives the error object so the caller can surface
|
|
250
|
+
* failures (e.g., via app logging) without crashing the
|
|
251
|
+
* interval.
|
|
252
|
+
*/
|
|
253
|
+
export function setupMetricsPersistence(store, intervalMs = 30_000, onError) {
|
|
254
|
+
let persistQueue = Promise.resolve();
|
|
255
|
+
function enqueuePersist() {
|
|
256
|
+
persistQueue = persistQueue
|
|
257
|
+
.then(() => store.persist())
|
|
258
|
+
.catch((err) => {
|
|
259
|
+
// persist failures (e.g., permission denied) should not crash
|
|
260
|
+
// the interval; surface the error via the optional callback.
|
|
261
|
+
onError?.(err);
|
|
262
|
+
});
|
|
263
|
+
return persistQueue;
|
|
264
|
+
}
|
|
265
|
+
const intervalId = setInterval(() => {
|
|
266
|
+
void enqueuePersist();
|
|
267
|
+
}, intervalMs);
|
|
268
|
+
return {
|
|
269
|
+
async clear() {
|
|
270
|
+
clearInterval(intervalId);
|
|
271
|
+
await enqueuePersist();
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAyBH,kBAAkB;AAElB,yCAAyC;AACzC,MAAM,UAAU,oBAAoB;IAClC,OAAO;QACL,SAAS,EAAE,CAAC;QACZ,UAAU,EAAE,CAAC;QACb,cAAc,EAAE,CAAC;QACjB,iBAAiB,EAAE,CAAC;QACpB,eAAe,EAAE,CAAC;KACnB,CAAC;AACJ,CAAC;AAED,uBAAuB;AAEvB;;;;;;;;;;GAUG;AACH,MAAM,OAAO,YAAY;IACvB,4CAA4C;IACpC,QAAQ,GAA6B,IAAI,GAAG,EAAE,CAAC;IAEvD,mEAAmE;IAC3D,WAAW,CAAS;IAE5B;;;OAGG;IACH,YAAY,WAAoB;QAC9B,IAAI,CAAC,WAAW,GAAG,WAAW,IAAI,uBAAuB,CAAC;IAC5D,CAAC;IAED,0BAA0B;IAE1B,0DAA0D;IAClD,MAAM,CAAC,QAAgB;QAC7B,IAAI,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,GAAG,oBAAoB,EAAE,CAAC;YACjC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACvC,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,4BAA4B;IAE5B;;;;;;;OAOG;IACH,UAAU,CAAC,QAAgB,EAAE,SAAiB;QAC5C,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC,CAAC,SAAS,EAAE,CAAC;QACd,CAAC,CAAC,cAAc,IAAI,SAAS,CAAC;IAChC,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,QAAgB;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC,CAAC,UAAU,EAAE,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACH,iBAAiB,CAAC,QAAgB;QAChC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC,CAAC,iBAAiB,EAAE,CAAC;IACxB,CAAC;IAED;;;;;;OAMG;IACH,gBAAgB,CAAC,QAAgB;QAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC,CAAC,eAAe,EAAE,CAAC;IACtB,CAAC;IAED,uBAAuB;IAEvB;;;OAGG;IACH,UAAU,CAAC,QAAgB;QACzB,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,CAAC,CAAC;YAAE,OAAO,oBAAoB,EAAE,CAAC;QACtC,0DAA0D;QAC1D,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC;IAClB,CAAC;IAED;;;OAGG;IACH,aAAa;QACX,MAAM,MAAM,GAAgC,EAAE,CAAC;QAC/C,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5C,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC;QAChC,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,sBAAsB;IAEtB;;;;;;;;;OASG;IACH,KAAK,CAAC,OAAO;QACX,oEAAoE;QACpE,sEAAsE;QACtE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;QAElC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC3C,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEzC,MAAM,IAAI,GAAqB;YAC7B,OAAO,EAAE,CAAC;YACV,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;SACzC,CAAC;QAEF,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC;QAC1C,MAAM,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QACpE,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7C,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,IAAI;QACR,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QAEvC,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACrD,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,IACE,OAAO,GAAG,KAAK,QAAQ;gBACvB,GAAG,KAAK,IAAI;gBACZ,MAAM,IAAI,GAAG;gBACZ,GAA+B,CAAC,IAAI,KAAK,QAAQ,EAClD,CAAC;gBACD,4DAA4D;gBAC5D,OAAO;YACT,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAqB,CAAC;QAEjD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBACnC,2DAA2D;gBAC3D,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC;gBACxE,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;gBAC3E,QAAQ,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,cAAc,EAAE,KAAK,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC;gBACvF,QAAQ,CAAC,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,IAAI,CAAC,CAAC,CAAC;gBAChG,QAAQ,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,eAAe,EAAE,KAAK,CAAC,eAAe,IAAI,CAAC,CAAC,CAAC;YAC5F,CAAC;QACH,CAAC;IACH,CAAC;IAED,oBAAoB;IAEpB;;;OAGG;IACH,KAAK;QACH,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;CACF;AAED,2BAA2B;AAE3B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,uBAAuB,CACrC,KAAmB,EACnB,aAAqB,MAAM,EAC3B,OAAgC;IAEhC,IAAI,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAErC,SAAS,cAAc;QACrB,YAAY,GAAG,YAAY;aACxB,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;aAC3B,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,8DAA8D;YAC9D,6DAA6D;YAC7D,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC,CAAC,CAAC;QACL,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;QAClC,KAAK,cAAc,EAAE,CAAC;IACxB,CAAC,EAAE,UAAU,CAAC,CAAC;IAEf,OAAO;QACL,KAAK,CAAC,KAAK;YACT,aAAa,CAAC,UAAU,CAAC,CAAC;YAC1B,MAAM,cAAc,EAAE,CAAC;QACzB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transforms.ts — Pure functions for transforming job extra variables
|
|
3
|
+
* before they are sent to AWX.
|
|
4
|
+
*
|
|
5
|
+
* All functions are pure: no I/O, no side effects, no network calls.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Converts an SSH Git URL to its HTTPS equivalent.
|
|
9
|
+
*
|
|
10
|
+
* Pattern matched: `git@<host>:<path>[.git]` → `https://<host>/<path>`
|
|
11
|
+
*
|
|
12
|
+
* - Already-HTTPS URLs are returned unchanged.
|
|
13
|
+
* - Non-SSH URLs (HTTP, file://, plain strings) are returned unchanged.
|
|
14
|
+
* - Trailing `.git` suffix is stripped.
|
|
15
|
+
* - Null or undefined input returns an empty string.
|
|
16
|
+
*
|
|
17
|
+
* @param url - The SCM URL to normalize (may be null/undefined)
|
|
18
|
+
* @returns The HTTPS-normalized URL, or the original if not an SSH URL
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeScmUrl(url: string | null | undefined): string;
|
|
21
|
+
/**
|
|
22
|
+
* Extracts a branch/tag name from a Git ref string.
|
|
23
|
+
*
|
|
24
|
+
* Supported ref prefixes:
|
|
25
|
+
* - `refs/heads/<name>` → `<name>`
|
|
26
|
+
* - `refs/tags/<name>` → `<name>`
|
|
27
|
+
*
|
|
28
|
+
* Raw branch names (no `refs/` prefix) are returned unchanged.
|
|
29
|
+
* Unrecognized ref prefixes (e.g., `refs/remotes/...`) are returned as-is.
|
|
30
|
+
* Null or undefined input returns an empty string.
|
|
31
|
+
*
|
|
32
|
+
* @param ref - The Git ref string (e.g., "refs/heads/main", "refs/tags/v1.0", "main")
|
|
33
|
+
* @returns The extracted branch/tag name
|
|
34
|
+
*/
|
|
35
|
+
export declare function inferGitBranch(ref: string | null | undefined): string;
|
|
36
|
+
/**
|
|
37
|
+
* Validates that all required keys are present and have non-empty values
|
|
38
|
+
* in the provided extra vars.
|
|
39
|
+
*
|
|
40
|
+
* Returns a list of missing var names (in the order they appear in `required`).
|
|
41
|
+
* If `vars` is null or undefined, all required vars are reported as missing.
|
|
42
|
+
*
|
|
43
|
+
* A key is considered **missing** if:
|
|
44
|
+
* - The key does not exist on the object, OR
|
|
45
|
+
* - The value is `null`, `undefined`, or an empty/whitespace-only string.
|
|
46
|
+
*
|
|
47
|
+
* @param vars - The extra vars object to validate (may be null/undefined)
|
|
48
|
+
* @param required - The list of required variable names
|
|
49
|
+
* @returns Array of missing variable names (empty if all are present and non-empty)
|
|
50
|
+
*/
|
|
51
|
+
export declare function validateRequiredVars(vars: Record<string, unknown>, required: string[]): string[];
|
|
52
|
+
//# sourceMappingURL=transforms.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transforms.d.ts","sourceRoot":"","sources":["../src/transforms.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CA2BtE;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAiBrE;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,EAAE,CAgBV"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transforms.ts — Pure functions for transforming job extra variables
|
|
3
|
+
* before they are sent to AWX.
|
|
4
|
+
*
|
|
5
|
+
* All functions are pure: no I/O, no side effects, no network calls.
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// normalizeScmUrl
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/**
|
|
11
|
+
* Converts an SSH Git URL to its HTTPS equivalent.
|
|
12
|
+
*
|
|
13
|
+
* Pattern matched: `git@<host>:<path>[.git]` → `https://<host>/<path>`
|
|
14
|
+
*
|
|
15
|
+
* - Already-HTTPS URLs are returned unchanged.
|
|
16
|
+
* - Non-SSH URLs (HTTP, file://, plain strings) are returned unchanged.
|
|
17
|
+
* - Trailing `.git` suffix is stripped.
|
|
18
|
+
* - Null or undefined input returns an empty string.
|
|
19
|
+
*
|
|
20
|
+
* @param url - The SCM URL to normalize (may be null/undefined)
|
|
21
|
+
* @returns The HTTPS-normalized URL, or the original if not an SSH URL
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeScmUrl(url) {
|
|
24
|
+
if (url == null || url === "") {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
// If already HTTPS, passthrough
|
|
28
|
+
if (url.startsWith("https://") || url.startsWith("http://")) {
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
// Match SSH format: git@<host>:<path>
|
|
32
|
+
const sshMatch = url.match(/^git@([^:]+):(.+)$/);
|
|
33
|
+
if (!sshMatch) {
|
|
34
|
+
// Not an SSH URL — return unchanged
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
const [, host, path] = sshMatch;
|
|
38
|
+
if (!host || !path) {
|
|
39
|
+
return url;
|
|
40
|
+
}
|
|
41
|
+
// Strip trailing .git if present
|
|
42
|
+
const cleanPath = path.endsWith(".git") ? path.slice(0, -4) : path;
|
|
43
|
+
return `https://${host}/${cleanPath}`;
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// inferGitBranch
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Extracts a branch/tag name from a Git ref string.
|
|
50
|
+
*
|
|
51
|
+
* Supported ref prefixes:
|
|
52
|
+
* - `refs/heads/<name>` → `<name>`
|
|
53
|
+
* - `refs/tags/<name>` → `<name>`
|
|
54
|
+
*
|
|
55
|
+
* Raw branch names (no `refs/` prefix) are returned unchanged.
|
|
56
|
+
* Unrecognized ref prefixes (e.g., `refs/remotes/...`) are returned as-is.
|
|
57
|
+
* Null or undefined input returns an empty string.
|
|
58
|
+
*
|
|
59
|
+
* @param ref - The Git ref string (e.g., "refs/heads/main", "refs/tags/v1.0", "main")
|
|
60
|
+
* @returns The extracted branch/tag name
|
|
61
|
+
*/
|
|
62
|
+
export function inferGitBranch(ref) {
|
|
63
|
+
if (ref == null || ref === "") {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
// refs/heads/<branch>
|
|
67
|
+
if (ref.startsWith("refs/heads/")) {
|
|
68
|
+
return ref.slice("refs/heads/".length);
|
|
69
|
+
}
|
|
70
|
+
// refs/tags/<tag>
|
|
71
|
+
if (ref.startsWith("refs/tags/")) {
|
|
72
|
+
return ref.slice("refs/tags/".length);
|
|
73
|
+
}
|
|
74
|
+
// Raw name or unrecognized prefix — return as-is
|
|
75
|
+
return ref;
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// validateRequiredVars
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/**
|
|
81
|
+
* Validates that all required keys are present and have non-empty values
|
|
82
|
+
* in the provided extra vars.
|
|
83
|
+
*
|
|
84
|
+
* Returns a list of missing var names (in the order they appear in `required`).
|
|
85
|
+
* If `vars` is null or undefined, all required vars are reported as missing.
|
|
86
|
+
*
|
|
87
|
+
* A key is considered **missing** if:
|
|
88
|
+
* - The key does not exist on the object, OR
|
|
89
|
+
* - The value is `null`, `undefined`, or an empty/whitespace-only string.
|
|
90
|
+
*
|
|
91
|
+
* @param vars - The extra vars object to validate (may be null/undefined)
|
|
92
|
+
* @param required - The list of required variable names
|
|
93
|
+
* @returns Array of missing variable names (empty if all are present and non-empty)
|
|
94
|
+
*/
|
|
95
|
+
export function validateRequiredVars(vars, required) {
|
|
96
|
+
const missing = [];
|
|
97
|
+
for (const name of required) {
|
|
98
|
+
const value = vars?.[name];
|
|
99
|
+
if (vars == null ||
|
|
100
|
+
value === null ||
|
|
101
|
+
value === undefined ||
|
|
102
|
+
(typeof value === "string" && value.trim() === "")) {
|
|
103
|
+
missing.push(name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return missing;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=transforms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transforms.js","sourceRoot":"","sources":["../src/transforms.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,GAA8B;IAC5D,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,gCAAgC;IAChC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC5D,OAAO,GAAG,CAAC;IACb,CAAC;IAED,sCAAsC;IACtC,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACjD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,oCAAoC;QACpC,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC;IAEhC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACnB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,iCAAiC;IACjC,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEnE,OAAO,WAAW,IAAI,IAAI,SAAS,EAAE,CAAC;AACxC,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,GAA8B;IAC3D,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE,EAAE,CAAC;QAC9B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,sBAAsB;IACtB,IAAI,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QAClC,OAAO,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IAED,kBAAkB;IAClB,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QACjC,OAAO,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAED,iDAAiD;IACjD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,8EAA8E;AAC9E,uBAAuB;AACvB,8EAA8E;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAA6B,EAC7B,QAAkB;IAElB,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3B,IACE,IAAI,IAAI,IAAI;YACZ,KAAK,KAAK,IAAI;YACd,KAAK,KAAK,SAAS;YACnB,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAClD,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weiyentan/opencode-plugin-awx",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "OpenCode plugin for AWX / Ansible Automation Platform — native tool access to job templates, projects, and job lifecycle operations.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": ["dist/**", "README.md", "LICENSE"],
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepublishOnly": "npm run build && npm test",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"lint": "tsc --noEmit",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@opencode-ai/plugin": "^1.17.8",
|
|
26
|
+
"zod": "4.1.8"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.12.2",
|
|
30
|
+
"typescript": "^5.8.2",
|
|
31
|
+
"vitest": "^3.1.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@opencode-ai/plugin": "^1.17.8"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"opencode",
|
|
44
|
+
"opencode-plugin",
|
|
45
|
+
"awx",
|
|
46
|
+
"ansible",
|
|
47
|
+
"aap",
|
|
48
|
+
"automation-platform"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/weiyentan/opencode-plugins",
|
|
54
|
+
"directory": "packages/awx"
|
|
55
|
+
}
|
|
56
|
+
}
|