@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/auth.d.ts +112 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +180 -0
  6. package/dist/auth.js.map +1 -0
  7. package/dist/client.d.ts +148 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +334 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/contracts/job-detail.d.ts +141 -0
  12. package/dist/contracts/job-detail.d.ts.map +1 -0
  13. package/dist/contracts/job-detail.js +98 -0
  14. package/dist/contracts/job-detail.js.map +1 -0
  15. package/dist/contracts/sync-project.d.ts +31 -0
  16. package/dist/contracts/sync-project.d.ts.map +1 -0
  17. package/dist/contracts/sync-project.js +30 -0
  18. package/dist/contracts/sync-project.js.map +1 -0
  19. package/dist/index.d.ts +16 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +754 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/job-status.d.ts +116 -0
  24. package/dist/job-status.d.ts.map +1 -0
  25. package/dist/job-status.js +168 -0
  26. package/dist/job-status.js.map +1 -0
  27. package/dist/launch.d.ts +76 -0
  28. package/dist/launch.d.ts.map +1 -0
  29. package/dist/launch.js +133 -0
  30. package/dist/launch.js.map +1 -0
  31. package/dist/list-projects.d.ts +63 -0
  32. package/dist/list-projects.d.ts.map +1 -0
  33. package/dist/list-projects.js +84 -0
  34. package/dist/list-projects.js.map +1 -0
  35. package/dist/list-templates.d.ts +60 -0
  36. package/dist/list-templates.d.ts.map +1 -0
  37. package/dist/list-templates.js +120 -0
  38. package/dist/list-templates.js.map +1 -0
  39. package/dist/metrics.d.ts +174 -0
  40. package/dist/metrics.d.ts.map +1 -0
  41. package/dist/metrics.js +275 -0
  42. package/dist/metrics.js.map +1 -0
  43. package/dist/transforms.d.ts +52 -0
  44. package/dist/transforms.d.ts.map +1 -0
  45. package/dist/transforms.js +108 -0
  46. package/dist/transforms.js.map +1 -0
  47. package/package.json +56 -0
@@ -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":"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"}
@@ -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
+ }