@arbiterhq/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +89 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +992 -0
- package/package.json +53 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-06-28
|
|
4
|
+
|
|
5
|
+
First public npm release under **`@arbiterhq/cli`**. The CLI binary remains **`arbiter`**; only the npm scope changed from the previously planned `@arbiter` org.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `arbiter login` — save API credentials locally
|
|
10
|
+
- `arbiter init` — create `arbiter.yaml` (optional `--from-remote`)
|
|
11
|
+
- `arbiter validate` — local manifest validation
|
|
12
|
+
- `arbiter plan` — manifest diff against remote workspace
|
|
13
|
+
- `arbiter apply` — sync manifest (`--yes` for CI)
|
|
14
|
+
- `arbiter drift` — workspace drift and adoption suggestions
|
|
15
|
+
- `arbiter status` — endpoint, manifest version, state signature
|
|
16
|
+
- `arbiter whoami` — credential session context
|
|
17
|
+
|
|
18
|
+
### Dependencies
|
|
19
|
+
|
|
20
|
+
- Requires `@arbiterhq/sdk@^0.2.0` (publish SDK before CLI)
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @arbiterhq/cli
|
|
2
|
+
|
|
3
|
+
Official `arbiter` command line interface. Stateless wrapper over [`@arbiterhq/sdk`](../sdk).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @arbiterhq/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 18+. The CLI depends on `@arbiterhq/sdk` from npm — install the SDK first if you use it directly in agent code:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @arbiterhq/sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Local development (monorepo contributors)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd packages/cli
|
|
21
|
+
npm install
|
|
22
|
+
npm run build
|
|
23
|
+
npm link
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Authentication
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
arbiter login
|
|
30
|
+
# or
|
|
31
|
+
export ARBITER_API_KEY=your_key
|
|
32
|
+
export ARBITER_BASE_URL=https://api.arbitertrust.com
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Credentials are stored at `~/.config/arbiter/credentials.json`. `ARBITER_API_KEY` takes precedence over the file.
|
|
36
|
+
|
|
37
|
+
### Base URL resolution
|
|
38
|
+
|
|
39
|
+
1. `ARBITER_BASE_URL` environment variable
|
|
40
|
+
2. `baseUrl` saved in credentials file (set via `arbiter login --base-url`)
|
|
41
|
+
3. Production default: `https://api.arbitertrust.com`
|
|
42
|
+
|
|
43
|
+
**Local development:** `export ARBITER_BASE_URL=http://localhost:3001` or `arbiter login --base-url http://localhost:3001`
|
|
44
|
+
|
|
45
|
+
`login` performs a local filesystem write only — no network validation.
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
| Command | Network | Description |
|
|
50
|
+
|---------|---------|-------------|
|
|
51
|
+
| `arbiter login` | No | Save credentials locally |
|
|
52
|
+
| `arbiter init` | Optional (`--from-remote`) | Create `arbiter.yaml` |
|
|
53
|
+
| `arbiter validate` | No | Local structural validation |
|
|
54
|
+
| `arbiter plan` | Yes | Diff manifest vs remote |
|
|
55
|
+
| `arbiter apply` | Yes | Sync manifest to remote |
|
|
56
|
+
| `arbiter apply --yes` | Yes | Non-interactive apply for CI |
|
|
57
|
+
| `arbiter drift` | Yes | Drift + adoption suggestions |
|
|
58
|
+
| `arbiter status` | Yes | Endpoint, manifest version, signature |
|
|
59
|
+
| `arbiter whoami` | No | Session context |
|
|
60
|
+
|
|
61
|
+
## Exit codes
|
|
62
|
+
|
|
63
|
+
| Code | `plan` / `drift` | Other commands |
|
|
64
|
+
|------|------------------|----------------|
|
|
65
|
+
| `0` | In sync / success | Success |
|
|
66
|
+
| `1` | Error | Error |
|
|
67
|
+
| `2` | Drift or pending changes | — |
|
|
68
|
+
|
|
69
|
+
## CI example
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
arbiter validate
|
|
73
|
+
arbiter plan
|
|
74
|
+
arbiter apply --yes
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Options
|
|
78
|
+
|
|
79
|
+
Global manifest path override:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
arbiter plan --manifest ./config/arbiter.yaml
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Disable colors in pipelines:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
NO_COLOR=1 arbiter plan
|
|
89
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/apply.ts
|
|
7
|
+
import readline from "readline/promises";
|
|
8
|
+
import { stdin as input, stdout as output } from "process";
|
|
9
|
+
import { ArbiterError } from "@arbiterhq/sdk";
|
|
10
|
+
|
|
11
|
+
// src/config/client.ts
|
|
12
|
+
import { ArbiterAdmin } from "@arbiterhq/sdk";
|
|
13
|
+
|
|
14
|
+
// src/config/credentials.ts
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import path2 from "path";
|
|
17
|
+
|
|
18
|
+
// src/config/paths.ts
|
|
19
|
+
import os from "os";
|
|
20
|
+
import path from "path";
|
|
21
|
+
var CREDENTIALS_DIR = path.join(os.homedir(), ".config", "arbiter");
|
|
22
|
+
var CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
23
|
+
var DEFAULT_MANIFEST_FILE = "arbiter.yaml";
|
|
24
|
+
var DEFAULT_BASE_URL = "https://api.arbitertrust.com";
|
|
25
|
+
|
|
26
|
+
// src/config/credentials.ts
|
|
27
|
+
function loadCredentialsFile() {
|
|
28
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const raw = fs.readFileSync(CREDENTIALS_FILE, "utf8");
|
|
32
|
+
const parsed = JSON.parse(raw);
|
|
33
|
+
if (typeof parsed.apiKey !== "string" || parsed.apiKey.length === 0) {
|
|
34
|
+
throw new Error(`Invalid credentials file at ${CREDENTIALS_FILE}: apiKey is required`);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
function saveCredentialsFile(credentials) {
|
|
39
|
+
fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
40
|
+
const payload = {
|
|
41
|
+
apiKey: credentials.apiKey,
|
|
42
|
+
baseUrl: credentials.baseUrl ?? DEFAULT_BASE_URL
|
|
43
|
+
};
|
|
44
|
+
if (credentials.workspace !== void 0) {
|
|
45
|
+
payload.workspace = credentials.workspace;
|
|
46
|
+
}
|
|
47
|
+
if (credentials.environment !== void 0) {
|
|
48
|
+
payload.environment = credentials.environment;
|
|
49
|
+
}
|
|
50
|
+
fs.writeFileSync(CREDENTIALS_FILE, `${JSON.stringify(payload, null, 2)}
|
|
51
|
+
`, "utf8");
|
|
52
|
+
}
|
|
53
|
+
function resolveSession() {
|
|
54
|
+
const envApiKey = process.env.ARBITER_API_KEY;
|
|
55
|
+
const fileCredentials = loadCredentialsFile();
|
|
56
|
+
const apiKey = envApiKey ?? fileCredentials?.apiKey;
|
|
57
|
+
if (!apiKey) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"No API key found. Run `arbiter login` or set ARBITER_API_KEY."
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const baseUrl = process.env.ARBITER_BASE_URL ?? fileCredentials?.baseUrl ?? DEFAULT_BASE_URL;
|
|
63
|
+
let source = "file";
|
|
64
|
+
if (envApiKey && fileCredentials?.apiKey) {
|
|
65
|
+
source = "env+file";
|
|
66
|
+
} else if (envApiKey) {
|
|
67
|
+
source = "env";
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
apiKey,
|
|
71
|
+
baseUrl,
|
|
72
|
+
workspace: fileCredentials?.workspace ?? null,
|
|
73
|
+
environment: fileCredentials?.environment ?? null,
|
|
74
|
+
source
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function maskApiKey(apiKey) {
|
|
78
|
+
if (apiKey.length <= 8) {
|
|
79
|
+
return "********";
|
|
80
|
+
}
|
|
81
|
+
return `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`;
|
|
82
|
+
}
|
|
83
|
+
function credentialsFilePath() {
|
|
84
|
+
return path2.normalize(CREDENTIALS_FILE);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/config/client.ts
|
|
88
|
+
function createArbiterClient() {
|
|
89
|
+
const session = resolveSession();
|
|
90
|
+
return new ArbiterAdmin({
|
|
91
|
+
apiKey: session.apiKey,
|
|
92
|
+
baseUrl: session.baseUrl
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/config/manifest.ts
|
|
97
|
+
import fs2 from "fs";
|
|
98
|
+
import path3 from "path";
|
|
99
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
100
|
+
|
|
101
|
+
// src/config/manifestSchema.ts
|
|
102
|
+
import { z } from "zod";
|
|
103
|
+
var SNAKE_CASE_ACTION = /^[a-z][a-z0-9_]*$/;
|
|
104
|
+
var RESOURCE_SOURCES = ["dashboard", "sdk", "discovered"];
|
|
105
|
+
var RESOURCE_MANAGEMENTS = ["dashboard", "sdk", "manifest", "discovered"];
|
|
106
|
+
var PERMISSION_STATUSES = [
|
|
107
|
+
"pending_review",
|
|
108
|
+
"allowed",
|
|
109
|
+
"denied",
|
|
110
|
+
"approval_required"
|
|
111
|
+
];
|
|
112
|
+
var AGENT_STATUSES = [
|
|
113
|
+
"pending_review",
|
|
114
|
+
"active",
|
|
115
|
+
"disabled",
|
|
116
|
+
"expired",
|
|
117
|
+
"archived"
|
|
118
|
+
];
|
|
119
|
+
var RISK_TIERS = ["low", "medium", "high", "critical"];
|
|
120
|
+
var OWNER_TYPES = ["user", "team", "service_account"];
|
|
121
|
+
var POLICY_OPERATORS = [
|
|
122
|
+
">",
|
|
123
|
+
">=",
|
|
124
|
+
"<",
|
|
125
|
+
"<=",
|
|
126
|
+
"=",
|
|
127
|
+
"==",
|
|
128
|
+
"!=",
|
|
129
|
+
"contains",
|
|
130
|
+
"not_contains",
|
|
131
|
+
"starts_with",
|
|
132
|
+
"ends_with",
|
|
133
|
+
"in",
|
|
134
|
+
"not_in",
|
|
135
|
+
"exists",
|
|
136
|
+
"not_exists",
|
|
137
|
+
"is_null",
|
|
138
|
+
"is_not_null",
|
|
139
|
+
"between",
|
|
140
|
+
"not_between"
|
|
141
|
+
];
|
|
142
|
+
var POLICY_DECISIONS = ["allow", "deny", "hold"];
|
|
143
|
+
var POLICY_CONDITION_OPERATORS = ["AND", "OR"];
|
|
144
|
+
var VALUELESS_OPERATORS = /* @__PURE__ */ new Set(["exists", "not_exists", "is_null", "is_not_null"]);
|
|
145
|
+
var PolicyScalarValueSchema = z.union([z.number(), z.string(), z.boolean(), z.null()]);
|
|
146
|
+
var PolicyArrayValueSchema = z.array(PolicyScalarValueSchema).max(100);
|
|
147
|
+
var PolicyValueSchema = z.union([PolicyScalarValueSchema, PolicyArrayValueSchema]);
|
|
148
|
+
var ManifestOwnerSchema = z.object({
|
|
149
|
+
slug: z.string().min(1),
|
|
150
|
+
type: z.enum(OWNER_TYPES)
|
|
151
|
+
});
|
|
152
|
+
var GrantMutationSchema = z.object({
|
|
153
|
+
action: z.string().min(1).regex(SNAKE_CASE_ACTION, "Action must be snake_case"),
|
|
154
|
+
grantedBy: z.string().min(1).optional(),
|
|
155
|
+
grantedReason: z.string().max(500).optional()
|
|
156
|
+
});
|
|
157
|
+
var ManifestAgentSchema = z.object({
|
|
158
|
+
externalId: z.string().min(1).optional(),
|
|
159
|
+
name: z.string().min(1, "Agent name is required"),
|
|
160
|
+
description: z.string().optional(),
|
|
161
|
+
source: z.enum(RESOURCE_SOURCES).optional(),
|
|
162
|
+
management: z.enum(RESOURCE_MANAGEMENTS).optional(),
|
|
163
|
+
owner: ManifestOwnerSchema.optional(),
|
|
164
|
+
ownerSlug: z.string().min(1).optional(),
|
|
165
|
+
ownerType: z.enum(OWNER_TYPES).optional(),
|
|
166
|
+
riskTier: z.enum(RISK_TIERS).optional(),
|
|
167
|
+
status: z.enum(AGENT_STATUSES).optional(),
|
|
168
|
+
expiresAt: z.string().datetime().optional(),
|
|
169
|
+
approvedBy: z.string().optional(),
|
|
170
|
+
grants: z.array(GrantMutationSchema).optional()
|
|
171
|
+
});
|
|
172
|
+
var ManifestPermissionSchema = z.object({
|
|
173
|
+
action: z.string().min(1).regex(SNAKE_CASE_ACTION, "Action must be snake_case"),
|
|
174
|
+
description: z.string().optional(),
|
|
175
|
+
status: z.enum(PERMISSION_STATUSES).optional(),
|
|
176
|
+
source: z.enum(RESOURCE_SOURCES).optional(),
|
|
177
|
+
management: z.enum(RESOURCE_MANAGEMENTS).optional(),
|
|
178
|
+
assignedAgents: z.array(z.string().min(1)).optional(),
|
|
179
|
+
displayName: z.string().nullable().optional(),
|
|
180
|
+
category: z.string().nullable().optional(),
|
|
181
|
+
documentation: z.string().nullable().optional(),
|
|
182
|
+
tags: z.array(z.string()).nullable().optional(),
|
|
183
|
+
metadata: z.record(z.unknown()).nullable().optional()
|
|
184
|
+
});
|
|
185
|
+
var PolicyConditionSchema = z.object({
|
|
186
|
+
field: z.string().min(1).max(256),
|
|
187
|
+
operator: z.enum(POLICY_OPERATORS),
|
|
188
|
+
value: PolicyValueSchema.optional(),
|
|
189
|
+
type: z.string().nullable().optional()
|
|
190
|
+
}).superRefine((condition, ctx) => {
|
|
191
|
+
const requiresValue = !VALUELESS_OPERATORS.has(condition.operator);
|
|
192
|
+
if (requiresValue && condition.value === void 0) {
|
|
193
|
+
ctx.addIssue({
|
|
194
|
+
code: z.ZodIssueCode.custom,
|
|
195
|
+
message: `Operator "${condition.operator}" requires a value`,
|
|
196
|
+
path: ["value"]
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if ((condition.operator === "in" || condition.operator === "not_in") && condition.value !== void 0 && !Array.isArray(condition.value)) {
|
|
200
|
+
ctx.addIssue({
|
|
201
|
+
code: z.ZodIssueCode.custom,
|
|
202
|
+
message: `Operator "${condition.operator}" requires an array value`,
|
|
203
|
+
path: ["value"]
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
var ManifestPolicySchema = z.object({
|
|
208
|
+
id: z.string().min(1).optional(),
|
|
209
|
+
action: z.string().regex(SNAKE_CASE_ACTION, "Action must be snake_case"),
|
|
210
|
+
field: z.string().min(1).optional(),
|
|
211
|
+
operator: z.enum(POLICY_OPERATORS).optional(),
|
|
212
|
+
value: PolicyValueSchema.optional(),
|
|
213
|
+
conditions: z.array(PolicyConditionSchema).min(1).optional(),
|
|
214
|
+
conditionOperator: z.enum(POLICY_CONDITION_OPERATORS).optional(),
|
|
215
|
+
priority: z.number().int().optional(),
|
|
216
|
+
displayName: z.string().min(1).optional(),
|
|
217
|
+
decision: z.enum(POLICY_DECISIONS),
|
|
218
|
+
source: z.enum(RESOURCE_SOURCES).optional(),
|
|
219
|
+
management: z.enum(RESOURCE_MANAGEMENTS).optional()
|
|
220
|
+
}).superRefine((data, ctx) => {
|
|
221
|
+
const hasLegacy = data.field !== void 0 && data.operator !== void 0 && data.value !== void 0;
|
|
222
|
+
const hasConditions = (data.conditions?.length ?? 0) > 0;
|
|
223
|
+
if (!hasLegacy && !hasConditions) {
|
|
224
|
+
ctx.addIssue({
|
|
225
|
+
code: z.ZodIssueCode.custom,
|
|
226
|
+
message: "Manifest policy requires field/operator/value or conditions",
|
|
227
|
+
path: ["conditions"]
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
var ManifestWorkspaceSchema = z.object({
|
|
232
|
+
agents: z.array(ManifestAgentSchema),
|
|
233
|
+
permissions: z.array(ManifestPermissionSchema),
|
|
234
|
+
policies: z.array(ManifestPolicySchema)
|
|
235
|
+
});
|
|
236
|
+
var ManifestDocumentSchema = z.object({
|
|
237
|
+
apiVersion: z.string().optional(),
|
|
238
|
+
kind: z.string().optional(),
|
|
239
|
+
metadata: z.object({
|
|
240
|
+
name: z.string().optional(),
|
|
241
|
+
environment: z.string().optional()
|
|
242
|
+
}).optional(),
|
|
243
|
+
spec: ManifestWorkspaceSchema.optional(),
|
|
244
|
+
agents: z.array(ManifestAgentSchema).optional(),
|
|
245
|
+
permissions: z.array(ManifestPermissionSchema).optional(),
|
|
246
|
+
policies: z.array(ManifestPolicySchema).optional()
|
|
247
|
+
}).superRefine((value, ctx) => {
|
|
248
|
+
const hasSpec = value.spec !== void 0;
|
|
249
|
+
const hasRoot = value.agents !== void 0 || value.permissions !== void 0 || value.policies !== void 0;
|
|
250
|
+
if (!hasSpec && !hasRoot) {
|
|
251
|
+
ctx.addIssue({
|
|
252
|
+
code: z.ZodIssueCode.custom,
|
|
253
|
+
message: "Manifest must define spec.agents or root-level agents"
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// src/config/manifestAdapter.ts
|
|
259
|
+
function toSdkManifest(workspace) {
|
|
260
|
+
return {
|
|
261
|
+
agents: workspace.agents.map((agent) => ({ ...agent })),
|
|
262
|
+
permissions: workspace.permissions.map((permission) => ({ ...permission })),
|
|
263
|
+
policies: workspace.policies.map((policy) => ({ ...policy }))
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/config/manifest.ts
|
|
268
|
+
function resolveManifestPath(manifestFlag) {
|
|
269
|
+
return path3.resolve(process.cwd(), manifestFlag ?? DEFAULT_MANIFEST_FILE);
|
|
270
|
+
}
|
|
271
|
+
function loadManifestFile(manifestPath) {
|
|
272
|
+
if (!fs2.existsSync(manifestPath)) {
|
|
273
|
+
throw new Error(`Manifest not found: ${manifestPath}`);
|
|
274
|
+
}
|
|
275
|
+
const content = fs2.readFileSync(manifestPath, "utf8");
|
|
276
|
+
return parseManifestContent(content, manifestPath);
|
|
277
|
+
}
|
|
278
|
+
function parseManifestContent(content, manifestPath) {
|
|
279
|
+
let document;
|
|
280
|
+
try {
|
|
281
|
+
document = parseYaml(content);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const message = error instanceof Error ? error.message : "Invalid YAML";
|
|
284
|
+
throw new Error(`${manifestPath}: ${message}`);
|
|
285
|
+
}
|
|
286
|
+
const parsed = ManifestDocumentSchema.safeParse(document);
|
|
287
|
+
if (!parsed.success) {
|
|
288
|
+
const issues = formatZodIssues(parsed.error.issues);
|
|
289
|
+
throw new Error(`Manifest validation failed:
|
|
290
|
+
${issues.join("\n")}`);
|
|
291
|
+
}
|
|
292
|
+
const doc = parsed.data;
|
|
293
|
+
const workspaceInput = doc.spec ?? {
|
|
294
|
+
agents: doc.agents ?? [],
|
|
295
|
+
permissions: doc.permissions ?? [],
|
|
296
|
+
policies: doc.policies ?? []
|
|
297
|
+
};
|
|
298
|
+
const workspace = toSdkManifest(ManifestWorkspaceSchema.parse(workspaceInput));
|
|
299
|
+
return {
|
|
300
|
+
path: manifestPath,
|
|
301
|
+
workspace,
|
|
302
|
+
apiVersion: doc.apiVersion ?? null,
|
|
303
|
+
metadataName: doc.metadata?.name ?? null,
|
|
304
|
+
environment: doc.metadata?.environment ?? null
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function validateManifestFile(manifestPath) {
|
|
308
|
+
if (!fs2.existsSync(manifestPath)) {
|
|
309
|
+
return [{ path: manifestPath, message: "File not found", line: null }];
|
|
310
|
+
}
|
|
311
|
+
const content = fs2.readFileSync(manifestPath, "utf8");
|
|
312
|
+
try {
|
|
313
|
+
parseYaml(content);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
const message = error instanceof Error ? error.message : "Invalid YAML";
|
|
316
|
+
const line = extractYamlLine(message);
|
|
317
|
+
return [{ path: manifestPath, message, line }];
|
|
318
|
+
}
|
|
319
|
+
let document;
|
|
320
|
+
try {
|
|
321
|
+
document = parseYaml(content);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
const message = error instanceof Error ? error.message : "Invalid YAML";
|
|
324
|
+
return [{ path: manifestPath, message, line: extractYamlLine(message) }];
|
|
325
|
+
}
|
|
326
|
+
const parsed = ManifestDocumentSchema.safeParse(document);
|
|
327
|
+
if (!parsed.success) {
|
|
328
|
+
return parsed.error.issues.map((issue) => ({
|
|
329
|
+
path: issue.path.join(".") || "root",
|
|
330
|
+
message: issue.message,
|
|
331
|
+
line: null
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
const doc = parsed.data;
|
|
335
|
+
const workspaceInput = doc.spec ?? {
|
|
336
|
+
agents: doc.agents ?? [],
|
|
337
|
+
permissions: doc.permissions ?? [],
|
|
338
|
+
policies: doc.policies ?? []
|
|
339
|
+
};
|
|
340
|
+
const workspaceParsed = ManifestWorkspaceSchema.safeParse(workspaceInput);
|
|
341
|
+
if (!workspaceParsed.success) {
|
|
342
|
+
return workspaceParsed.error.issues.map((issue) => ({
|
|
343
|
+
path: issue.path.join(".") || "root",
|
|
344
|
+
message: issue.message,
|
|
345
|
+
line: null
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
function formatZodIssues(issues) {
|
|
351
|
+
return issues.map((issue) => {
|
|
352
|
+
const location = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
353
|
+
return ` - ${location}: ${issue.message}`;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function extractYamlLine(message) {
|
|
357
|
+
const match = message.match(/at line (\d+)/i);
|
|
358
|
+
if (!match?.[1]) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
return Number.parseInt(match[1], 10);
|
|
362
|
+
}
|
|
363
|
+
function starterManifestTemplate() {
|
|
364
|
+
const document = {
|
|
365
|
+
apiVersion: "arbiter.dev/v1",
|
|
366
|
+
kind: "WorkspaceGovernance",
|
|
367
|
+
metadata: {
|
|
368
|
+
name: "my-workspace",
|
|
369
|
+
environment: "development"
|
|
370
|
+
},
|
|
371
|
+
spec: {
|
|
372
|
+
agents: [
|
|
373
|
+
{
|
|
374
|
+
externalId: "finance-agent",
|
|
375
|
+
name: "finance-agent",
|
|
376
|
+
description: "Handles payment operations"
|
|
377
|
+
}
|
|
378
|
+
],
|
|
379
|
+
permissions: [
|
|
380
|
+
{
|
|
381
|
+
action: "send_payment",
|
|
382
|
+
status: "approval_required"
|
|
383
|
+
}
|
|
384
|
+
],
|
|
385
|
+
policies: [
|
|
386
|
+
{
|
|
387
|
+
id: "payment-amount-hold",
|
|
388
|
+
action: "send_payment",
|
|
389
|
+
field: "amount",
|
|
390
|
+
operator: ">",
|
|
391
|
+
value: 1e3,
|
|
392
|
+
decision: "hold"
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
return stringifyYaml(document, { indent: 2 });
|
|
398
|
+
}
|
|
399
|
+
function workspaceToManifestDocument(workspace, metadata) {
|
|
400
|
+
const document = {
|
|
401
|
+
apiVersion: "arbiter.dev/v1",
|
|
402
|
+
kind: "WorkspaceGovernance",
|
|
403
|
+
metadata: {
|
|
404
|
+
name: metadata?.name ?? "imported-workspace",
|
|
405
|
+
environment: metadata?.environment ?? "production"
|
|
406
|
+
},
|
|
407
|
+
spec: workspace
|
|
408
|
+
};
|
|
409
|
+
return stringifyYaml(document, { indent: 2 });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/output/format.ts
|
|
413
|
+
var ANSI = {
|
|
414
|
+
reset: "\x1B[0m",
|
|
415
|
+
green: "\x1B[32m",
|
|
416
|
+
yellow: "\x1B[33m",
|
|
417
|
+
red: "\x1B[31m",
|
|
418
|
+
cyan: "\x1B[36m",
|
|
419
|
+
magenta: "\x1B[35m",
|
|
420
|
+
dim: "\x1B[2m",
|
|
421
|
+
bold: "\x1B[1m"
|
|
422
|
+
};
|
|
423
|
+
function isColorEnabled() {
|
|
424
|
+
if (process.env.NO_COLOR !== void 0) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
return Boolean(process.stdout.isTTY);
|
|
428
|
+
}
|
|
429
|
+
function colorize(text, color) {
|
|
430
|
+
if (!isColorEnabled()) {
|
|
431
|
+
return text;
|
|
432
|
+
}
|
|
433
|
+
return `${ANSI[color]}${text}${ANSI.reset}`;
|
|
434
|
+
}
|
|
435
|
+
function writeLine(line) {
|
|
436
|
+
process.stdout.write(`${line}
|
|
437
|
+
`);
|
|
438
|
+
}
|
|
439
|
+
function writeError(line) {
|
|
440
|
+
process.stderr.write(`${line}
|
|
441
|
+
`);
|
|
442
|
+
}
|
|
443
|
+
function divider() {
|
|
444
|
+
writeLine("\u2500".repeat(64));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/output/plan.ts
|
|
448
|
+
function renderPlanResult(result, options) {
|
|
449
|
+
writeLine(colorize("Arbiter Plan", "bold"));
|
|
450
|
+
writeLine(`Manifest: ${options.manifestPath}`);
|
|
451
|
+
divider();
|
|
452
|
+
if (!result.drift) {
|
|
453
|
+
writeLine(colorize("No changes. Workspace is in sync.", "green"));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const deep = result;
|
|
457
|
+
for (const item of deep.changes.create) {
|
|
458
|
+
writeLine(formatChangeLine("[+]", "CREATE", item, "green"));
|
|
459
|
+
}
|
|
460
|
+
for (const item of deep.changes.update) {
|
|
461
|
+
writeLine(formatChangeLine("[~]", "UPDATE", item, "yellow"));
|
|
462
|
+
}
|
|
463
|
+
for (const item of deep.changes.archive) {
|
|
464
|
+
writeLine(formatChangeLine("[-]", "ARCHIVE", item, "red"));
|
|
465
|
+
}
|
|
466
|
+
for (const suggestion of deep.adoptionSuggestions) {
|
|
467
|
+
writeLine(formatAdoptLine(suggestion));
|
|
468
|
+
}
|
|
469
|
+
if (options.verbose) {
|
|
470
|
+
const unchangedTotal = deep.summary.agents.unchanged + deep.summary.permissions.unchanged + deep.summary.policies.unchanged;
|
|
471
|
+
if (unchangedTotal > 0) {
|
|
472
|
+
writeLine(
|
|
473
|
+
colorize(
|
|
474
|
+
`[=] UNCHANGED ${unchangedTotal} resources (agents: ${deep.summary.agents.unchanged}, permissions: ${deep.summary.permissions.unchanged}, policies: ${deep.summary.policies.unchanged})`,
|
|
475
|
+
"dim"
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
divider();
|
|
481
|
+
writeLine(
|
|
482
|
+
`Plan: ${deep.summary.agents.create + deep.summary.permissions.create + deep.summary.policies.create} to add, ${deep.summary.agents.update + deep.summary.permissions.update + deep.summary.policies.update} to change, ${deep.summary.agents.archive + deep.summary.permissions.archive + deep.summary.policies.archive} to archive, ${deep.adoptionSuggestions.length} adopt suggestions.`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
function planHasActionableChanges(result) {
|
|
486
|
+
if (!result.drift) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
const deep = result;
|
|
490
|
+
const summary = deep.summary;
|
|
491
|
+
const mutations = summary.agents.create + summary.agents.update + summary.agents.archive + summary.permissions.create + summary.permissions.update + summary.permissions.archive + summary.policies.create + summary.policies.update + summary.policies.archive;
|
|
492
|
+
return mutations > 0 || deep.adoptionSuggestions.length > 0;
|
|
493
|
+
}
|
|
494
|
+
function formatChangeLine(indicator, verb, item, color) {
|
|
495
|
+
const resourceType = capitalize(item.type);
|
|
496
|
+
const identifier = resourceIdentifier(item);
|
|
497
|
+
return colorize(`${indicator} ${verb} ${resourceType} ${identifier}`, color);
|
|
498
|
+
}
|
|
499
|
+
function formatAdoptLine(suggestion) {
|
|
500
|
+
const resourceType = capitalize(suggestion.type);
|
|
501
|
+
const identifier = adoptIdentifier(suggestion);
|
|
502
|
+
return colorize(`[?] ADOPT ${resourceType} ${identifier}`, "cyan");
|
|
503
|
+
}
|
|
504
|
+
function resourceIdentifier(item) {
|
|
505
|
+
if (item.type === "agent" && item.externalId) {
|
|
506
|
+
return item.externalId;
|
|
507
|
+
}
|
|
508
|
+
if (item.type === "permission" && item.action) {
|
|
509
|
+
return item.action;
|
|
510
|
+
}
|
|
511
|
+
if (item.type === "policy" && item.id) {
|
|
512
|
+
return item.id;
|
|
513
|
+
}
|
|
514
|
+
if (item.type === "grant" && item.externalId && item.action) {
|
|
515
|
+
return `${item.externalId} -> ${item.action}`;
|
|
516
|
+
}
|
|
517
|
+
if (item.externalId) {
|
|
518
|
+
return item.externalId;
|
|
519
|
+
}
|
|
520
|
+
if (item.action) {
|
|
521
|
+
return item.action;
|
|
522
|
+
}
|
|
523
|
+
if (item.id) {
|
|
524
|
+
return item.id;
|
|
525
|
+
}
|
|
526
|
+
return "unknown";
|
|
527
|
+
}
|
|
528
|
+
function adoptIdentifier(suggestion) {
|
|
529
|
+
if (suggestion.externalId) {
|
|
530
|
+
return suggestion.externalId;
|
|
531
|
+
}
|
|
532
|
+
if (suggestion.action) {
|
|
533
|
+
return suggestion.action;
|
|
534
|
+
}
|
|
535
|
+
if (suggestion.id) {
|
|
536
|
+
return suggestion.id;
|
|
537
|
+
}
|
|
538
|
+
return "unknown";
|
|
539
|
+
}
|
|
540
|
+
function capitalize(value) {
|
|
541
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/commands/apply.ts
|
|
545
|
+
async function runApply(options) {
|
|
546
|
+
try {
|
|
547
|
+
const manifestPath = resolveManifestPath(options.manifest);
|
|
548
|
+
const loaded = loadManifestFile(manifestPath);
|
|
549
|
+
const arbiter = createArbiterClient();
|
|
550
|
+
const planResult = await arbiter.plan(loaded.workspace);
|
|
551
|
+
renderPlanResult(planResult, {
|
|
552
|
+
verbose: options.verbose === true,
|
|
553
|
+
manifestPath
|
|
554
|
+
});
|
|
555
|
+
if (!options.yes) {
|
|
556
|
+
const confirmed = await promptForConfirmation();
|
|
557
|
+
if (!confirmed) {
|
|
558
|
+
writeLine(colorize("Apply cancelled.", "yellow"));
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const applyResult = await arbiter.syncRegistryManifest(loaded.workspace);
|
|
563
|
+
divider();
|
|
564
|
+
writeLine(colorize("Apply complete.", "green"));
|
|
565
|
+
writeLine(
|
|
566
|
+
`Summary: agents(+${applyResult.summary.agents.created}/~${applyResult.summary.agents.updated}/-${applyResult.summary.agents.archived}), permissions(+${applyResult.summary.permissions.created}/~${applyResult.summary.permissions.updated}/-${applyResult.summary.permissions.archived}), policies(+${applyResult.summary.policies.created}/~${applyResult.summary.policies.updated}/-${applyResult.summary.policies.archived})`
|
|
567
|
+
);
|
|
568
|
+
if (applyResult.stateSignature) {
|
|
569
|
+
writeLine(`State signature: ${applyResult.stateSignature}`);
|
|
570
|
+
}
|
|
571
|
+
return 0;
|
|
572
|
+
} catch (error) {
|
|
573
|
+
writeError(formatCommandError(error));
|
|
574
|
+
return 1;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function promptForConfirmation() {
|
|
578
|
+
if (!process.stdin.isTTY) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"Non-interactive environment detected. Re-run with --yes to apply without a prompt."
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const rl = readline.createInterface({ input, output });
|
|
584
|
+
try {
|
|
585
|
+
const answer = (await rl.question("Apply these changes? [y/N] ")).trim().toLowerCase();
|
|
586
|
+
return answer === "y" || answer === "yes";
|
|
587
|
+
} finally {
|
|
588
|
+
rl.close();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function formatCommandError(error) {
|
|
592
|
+
if (error instanceof ArbiterError) {
|
|
593
|
+
return `Error: ${error.message}`;
|
|
594
|
+
}
|
|
595
|
+
if (error instanceof Error) {
|
|
596
|
+
return `Error: ${error.message}`;
|
|
597
|
+
}
|
|
598
|
+
return "Error: Unknown failure";
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/commands/drift.ts
|
|
602
|
+
import { ArbiterError as ArbiterError2 } from "@arbiterhq/sdk";
|
|
603
|
+
|
|
604
|
+
// src/output/drift.ts
|
|
605
|
+
function renderDriftResult(result) {
|
|
606
|
+
writeLine(colorize("Arbiter Drift", "bold"));
|
|
607
|
+
divider();
|
|
608
|
+
if (!result.drift) {
|
|
609
|
+
writeLine(colorize("In sync. No drift or adoption suggestions detected.", "green"));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const configDrift = result.records.filter(
|
|
613
|
+
(record) => record.reason === "manifest_create" || record.reason === "manifest_update" || record.reason === "manifest_archive"
|
|
614
|
+
);
|
|
615
|
+
const adoptionRecords = result.records.filter(
|
|
616
|
+
(record) => record.reason === "discovered_unadopted" || record.reason === "pending_review"
|
|
617
|
+
);
|
|
618
|
+
if (configDrift.length > 0) {
|
|
619
|
+
writeLine(colorize("Configuration Drift", "magenta"));
|
|
620
|
+
for (const record of configDrift) {
|
|
621
|
+
writeLine(formatDriftRecord(record));
|
|
622
|
+
}
|
|
623
|
+
writeLine("");
|
|
624
|
+
}
|
|
625
|
+
if (adoptionRecords.length > 0 || result.adoptionSuggestions.length > 0) {
|
|
626
|
+
writeLine(colorize("Adoption Suggestions", "cyan"));
|
|
627
|
+
for (const record of adoptionRecords) {
|
|
628
|
+
writeLine(formatDriftRecord(record));
|
|
629
|
+
}
|
|
630
|
+
for (const suggestion of result.adoptionSuggestions) {
|
|
631
|
+
const id = suggestion.externalId ?? suggestion.action ?? suggestion.id ?? "unknown";
|
|
632
|
+
writeLine(
|
|
633
|
+
colorize(
|
|
634
|
+
`[?] ADOPT ${capitalize2(suggestion.type)} ${id} (discovered runtime asset)`,
|
|
635
|
+
"cyan"
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
divider();
|
|
641
|
+
writeLine(
|
|
642
|
+
`Drift summary: ${configDrift.length} configuration records, ${adoptionRecords.length + result.adoptionSuggestions.length} adoption suggestions.`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
function formatDriftRecord(record) {
|
|
646
|
+
const id = record.externalId ?? record.action ?? record.id ?? "unknown";
|
|
647
|
+
const label = `${capitalize2(record.type)} ${id}`;
|
|
648
|
+
const reason = record.reason.replace(/_/g, " ");
|
|
649
|
+
return colorize(`[!] ${label} \u2014 ${reason}`, "yellow");
|
|
650
|
+
}
|
|
651
|
+
function capitalize2(value) {
|
|
652
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/commands/drift.ts
|
|
656
|
+
async function runDrift() {
|
|
657
|
+
try {
|
|
658
|
+
const arbiter = createArbiterClient();
|
|
659
|
+
const result = await arbiter.getDrift();
|
|
660
|
+
renderDriftResult(result);
|
|
661
|
+
if (result.drift) {
|
|
662
|
+
return 2;
|
|
663
|
+
}
|
|
664
|
+
return 0;
|
|
665
|
+
} catch (error) {
|
|
666
|
+
writeError(formatCommandError2(error));
|
|
667
|
+
return 1;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function formatCommandError2(error) {
|
|
671
|
+
if (error instanceof ArbiterError2) {
|
|
672
|
+
return `Error: ${error.message}`;
|
|
673
|
+
}
|
|
674
|
+
if (error instanceof Error) {
|
|
675
|
+
return `Error: ${error.message}`;
|
|
676
|
+
}
|
|
677
|
+
return "Error: Unknown failure";
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/commands/init.ts
|
|
681
|
+
import fs3 from "fs";
|
|
682
|
+
import path4 from "path";
|
|
683
|
+
async function runInit(options) {
|
|
684
|
+
const manifestPath = resolveManifestPath(options.manifest);
|
|
685
|
+
if (fs3.existsSync(manifestPath) && !options.force) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`${manifestPath} already exists. Use --force to overwrite.`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
let content;
|
|
691
|
+
if (options.fromRemote) {
|
|
692
|
+
const session = resolveSession();
|
|
693
|
+
const arbiter = createArbiterClient();
|
|
694
|
+
const drift = await arbiter.getDrift();
|
|
695
|
+
const workspace = buildWorkspaceFromDrift(drift.adoptionSuggestions, drift.records);
|
|
696
|
+
content = workspaceToManifestDocument(workspace, {
|
|
697
|
+
name: session.workspace ?? "imported-workspace",
|
|
698
|
+
environment: session.environment ?? "production"
|
|
699
|
+
});
|
|
700
|
+
writeLine(colorize("Imported adoption suggestions from remote workspace.", "cyan"));
|
|
701
|
+
} else {
|
|
702
|
+
content = starterManifestTemplate();
|
|
703
|
+
}
|
|
704
|
+
fs3.writeFileSync(manifestPath, `${content}
|
|
705
|
+
`, "utf8");
|
|
706
|
+
writeLine(colorize("Created manifest template.", "green"));
|
|
707
|
+
writeLine(`Path: ${path4.resolve(manifestPath)}`);
|
|
708
|
+
writeLine("Next: arbiter validate \u2192 arbiter plan \u2192 arbiter apply --yes");
|
|
709
|
+
}
|
|
710
|
+
function buildWorkspaceFromDrift(suggestions, records) {
|
|
711
|
+
const workspace = {
|
|
712
|
+
agents: [],
|
|
713
|
+
permissions: [],
|
|
714
|
+
policies: []
|
|
715
|
+
};
|
|
716
|
+
const seenAgents = /* @__PURE__ */ new Set();
|
|
717
|
+
const seenPermissions = /* @__PURE__ */ new Set();
|
|
718
|
+
const seenPolicies = /* @__PURE__ */ new Set();
|
|
719
|
+
for (const suggestion of suggestions) {
|
|
720
|
+
if (suggestion.type === "agent" && suggestion.externalId && !seenAgents.has(suggestion.externalId)) {
|
|
721
|
+
workspace.agents.push({
|
|
722
|
+
externalId: suggestion.externalId,
|
|
723
|
+
name: suggestion.externalId
|
|
724
|
+
});
|
|
725
|
+
seenAgents.add(suggestion.externalId);
|
|
726
|
+
}
|
|
727
|
+
if (suggestion.type === "permission" && suggestion.action && !seenPermissions.has(suggestion.action)) {
|
|
728
|
+
workspace.permissions.push({
|
|
729
|
+
action: suggestion.action,
|
|
730
|
+
status: "pending_review"
|
|
731
|
+
});
|
|
732
|
+
seenPermissions.add(suggestion.action);
|
|
733
|
+
}
|
|
734
|
+
if (suggestion.type === "policy" && suggestion.id && !seenPolicies.has(suggestion.id)) {
|
|
735
|
+
workspace.policies.push({
|
|
736
|
+
id: suggestion.id,
|
|
737
|
+
action: "discovered_action",
|
|
738
|
+
field: "amount",
|
|
739
|
+
operator: ">",
|
|
740
|
+
value: 0,
|
|
741
|
+
decision: "hold"
|
|
742
|
+
});
|
|
743
|
+
seenPolicies.add(suggestion.id);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const record of records) {
|
|
747
|
+
if (record.type === "agent" && record.externalId && !seenAgents.has(record.externalId)) {
|
|
748
|
+
workspace.agents.push({
|
|
749
|
+
externalId: record.externalId,
|
|
750
|
+
name: record.externalId
|
|
751
|
+
});
|
|
752
|
+
seenAgents.add(record.externalId);
|
|
753
|
+
}
|
|
754
|
+
if (record.type === "permission" && record.action && !seenPermissions.has(record.action)) {
|
|
755
|
+
workspace.permissions.push({
|
|
756
|
+
action: record.action,
|
|
757
|
+
status: "pending_review"
|
|
758
|
+
});
|
|
759
|
+
seenPermissions.add(record.action);
|
|
760
|
+
}
|
|
761
|
+
if (record.type === "policy" && record.id && !seenPolicies.has(record.id)) {
|
|
762
|
+
workspace.policies.push({
|
|
763
|
+
id: record.id,
|
|
764
|
+
action: "discovered_action",
|
|
765
|
+
field: "amount",
|
|
766
|
+
operator: ">",
|
|
767
|
+
value: 0,
|
|
768
|
+
decision: "hold"
|
|
769
|
+
});
|
|
770
|
+
seenPolicies.add(record.id);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return workspace;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/commands/login.ts
|
|
777
|
+
import readline2 from "readline/promises";
|
|
778
|
+
import { stdin as input2, stdout as output2 } from "process";
|
|
779
|
+
async function runLogin(options) {
|
|
780
|
+
const rl = readline2.createInterface({ input: input2, output: output2 });
|
|
781
|
+
try {
|
|
782
|
+
const apiKey = options.apiKey ?? (await rl.question("API key: ")).trim();
|
|
783
|
+
if (apiKey.length === 0) {
|
|
784
|
+
throw new Error("API key is required");
|
|
785
|
+
}
|
|
786
|
+
const workspace = options.workspace ?? (await rl.question("Workspace name (optional): ")).trim();
|
|
787
|
+
const environment = options.environment ?? (await rl.question("Environment (optional): ")).trim();
|
|
788
|
+
const baseUrlInput = options.baseUrl ?? (await rl.question(`Endpoint [${DEFAULT_BASE_URL}]: `)).trim();
|
|
789
|
+
const baseUrl = baseUrlInput.length > 0 ? baseUrlInput : DEFAULT_BASE_URL;
|
|
790
|
+
const existing = loadCredentialsFile();
|
|
791
|
+
const credentials = {
|
|
792
|
+
apiKey,
|
|
793
|
+
baseUrl
|
|
794
|
+
};
|
|
795
|
+
const resolvedWorkspace = workspace.length > 0 ? workspace : existing?.workspace;
|
|
796
|
+
const resolvedEnvironment = environment.length > 0 ? environment : existing?.environment;
|
|
797
|
+
if (resolvedWorkspace !== void 0) {
|
|
798
|
+
credentials.workspace = resolvedWorkspace;
|
|
799
|
+
}
|
|
800
|
+
if (resolvedEnvironment !== void 0) {
|
|
801
|
+
credentials.environment = resolvedEnvironment;
|
|
802
|
+
}
|
|
803
|
+
saveCredentialsFile(credentials);
|
|
804
|
+
writeLine(colorize("Credentials saved.", "green"));
|
|
805
|
+
writeLine(`Path: ${credentialsFilePath()}`);
|
|
806
|
+
writeLine("Validation occurs on the next command that contacts the API.");
|
|
807
|
+
} finally {
|
|
808
|
+
rl.close();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/commands/plan.ts
|
|
813
|
+
import { ArbiterError as ArbiterError3 } from "@arbiterhq/sdk";
|
|
814
|
+
async function runPlan(options) {
|
|
815
|
+
try {
|
|
816
|
+
const manifestPath = resolveManifestPath(options.manifest);
|
|
817
|
+
const loaded = loadManifestFile(manifestPath);
|
|
818
|
+
const arbiter = createArbiterClient();
|
|
819
|
+
const result = await arbiter.plan(loaded.workspace);
|
|
820
|
+
renderPlanResult(result, {
|
|
821
|
+
verbose: options.verbose === true,
|
|
822
|
+
manifestPath
|
|
823
|
+
});
|
|
824
|
+
if (planHasActionableChanges(result)) {
|
|
825
|
+
return 2;
|
|
826
|
+
}
|
|
827
|
+
return 0;
|
|
828
|
+
} catch (error) {
|
|
829
|
+
writeError(formatCommandError3(error));
|
|
830
|
+
return 1;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function formatCommandError3(error) {
|
|
834
|
+
if (error instanceof ArbiterError3) {
|
|
835
|
+
return `Error: ${error.message}`;
|
|
836
|
+
}
|
|
837
|
+
if (error instanceof Error) {
|
|
838
|
+
return `Error: ${error.message}`;
|
|
839
|
+
}
|
|
840
|
+
return "Error: Unknown failure";
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/commands/status.ts
|
|
844
|
+
import { ArbiterError as ArbiterError4 } from "@arbiterhq/sdk";
|
|
845
|
+
async function runStatus(options) {
|
|
846
|
+
try {
|
|
847
|
+
const session = resolveSession();
|
|
848
|
+
const manifestPath = resolveManifestPath(options.manifest);
|
|
849
|
+
let apiVersion = "n/a";
|
|
850
|
+
let manifestName = "n/a";
|
|
851
|
+
try {
|
|
852
|
+
const loaded = loadManifestFile(manifestPath);
|
|
853
|
+
apiVersion = loaded.apiVersion ?? "n/a";
|
|
854
|
+
manifestName = loaded.metadataName ?? manifestPath;
|
|
855
|
+
} catch {
|
|
856
|
+
manifestName = `${manifestPath} (missing)`;
|
|
857
|
+
}
|
|
858
|
+
const arbiter = createArbiterClient();
|
|
859
|
+
const signature = await arbiter.getStateSignature();
|
|
860
|
+
writeLine(colorize("Arbiter Status", "bold"));
|
|
861
|
+
divider();
|
|
862
|
+
writeLine(`Endpoint: ${session.baseUrl}`);
|
|
863
|
+
writeLine(`Manifest: ${manifestName}`);
|
|
864
|
+
writeLine(`Manifest version: ${apiVersion}`);
|
|
865
|
+
writeLine(`State signature: ${signature.stateSignature}`);
|
|
866
|
+
writeLine(`Workspace label: ${session.workspace ?? "not set"}`);
|
|
867
|
+
writeLine(`Environment: ${session.environment ?? "not set"}`);
|
|
868
|
+
return 0;
|
|
869
|
+
} catch (error) {
|
|
870
|
+
writeError(formatCommandError4(error));
|
|
871
|
+
return 1;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function formatCommandError4(error) {
|
|
875
|
+
if (error instanceof ArbiterError4) {
|
|
876
|
+
return `Error: ${error.message}`;
|
|
877
|
+
}
|
|
878
|
+
if (error instanceof Error) {
|
|
879
|
+
return `Error: ${error.message}`;
|
|
880
|
+
}
|
|
881
|
+
return "Error: Unknown failure";
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/commands/validate.ts
|
|
885
|
+
function runValidate(options) {
|
|
886
|
+
const manifestPath = resolveManifestPath(options.manifest);
|
|
887
|
+
const issues = validateManifestFile(manifestPath);
|
|
888
|
+
if (issues.length > 0) {
|
|
889
|
+
writeError(colorize("Manifest validation failed:", "red"));
|
|
890
|
+
for (const issue of issues) {
|
|
891
|
+
const line = issue.line !== null ? ` (line ${issue.line})` : "";
|
|
892
|
+
writeError(` ${issue.path}${line}: ${issue.message}`);
|
|
893
|
+
}
|
|
894
|
+
return 1;
|
|
895
|
+
}
|
|
896
|
+
writeLine(colorize(`\u2713 ${manifestPath} is structurally valid`, "green"));
|
|
897
|
+
return 0;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/commands/whoami.ts
|
|
901
|
+
function runWhoami() {
|
|
902
|
+
try {
|
|
903
|
+
const session = resolveSession();
|
|
904
|
+
const file = loadCredentialsFile();
|
|
905
|
+
const hasEnvKey = process.env.ARBITER_API_KEY !== void 0;
|
|
906
|
+
writeLine(colorize("Arbiter Session", "bold"));
|
|
907
|
+
divider();
|
|
908
|
+
writeLine(`Workspace: ${session.workspace ?? "not set"}`);
|
|
909
|
+
writeLine(`Environment: ${session.environment ?? "not set"}`);
|
|
910
|
+
writeLine(`Endpoint: ${session.baseUrl}`);
|
|
911
|
+
writeLine(`Authenticated: true`);
|
|
912
|
+
writeLine(`Credential: ${credentialsFilePath()}`);
|
|
913
|
+
writeLine(`API key: ${maskApiKey(session.apiKey)} (${session.source})`);
|
|
914
|
+
if (hasEnvKey) {
|
|
915
|
+
writeLine("Note: ARBITER_API_KEY overrides stored credentials");
|
|
916
|
+
}
|
|
917
|
+
if (!file && hasEnvKey) {
|
|
918
|
+
writeLine("Note: Using environment variable only");
|
|
919
|
+
}
|
|
920
|
+
return 0;
|
|
921
|
+
} catch {
|
|
922
|
+
writeLine(colorize("Authenticated: false", "red"));
|
|
923
|
+
writeLine("Run `arbiter login` or set ARBITER_API_KEY.");
|
|
924
|
+
return 1;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/cli.ts
|
|
929
|
+
function buildCli() {
|
|
930
|
+
const program2 = new Command();
|
|
931
|
+
program2.name("arbiter").description("Arbiter governance control plane CLI").version("0.1.0");
|
|
932
|
+
program2.command("login").description("Store API credentials locally (no network validation)").option("--api-key <key>", "API key value").option("--workspace <name>", "Workspace label").option("--environment <name>", "Environment label").option("--base-url <url>", "API base URL").action(async (options) => {
|
|
933
|
+
await runLogin({
|
|
934
|
+
...options.apiKey !== void 0 ? { apiKey: options.apiKey } : {},
|
|
935
|
+
...options.workspace !== void 0 ? { workspace: options.workspace } : {},
|
|
936
|
+
...options.environment !== void 0 ? { environment: options.environment } : {},
|
|
937
|
+
...options.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {}
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
program2.command("init").description("Create a starter arbiter.yaml manifest").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").option("--from-remote", "Import adoption suggestions from the remote workspace").option("--force", "Overwrite an existing manifest file").action(async (options) => {
|
|
941
|
+
await runInit({
|
|
942
|
+
...options.manifest !== void 0 ? { manifest: options.manifest } : {},
|
|
943
|
+
...options.fromRemote !== void 0 ? { fromRemote: options.fromRemote } : {},
|
|
944
|
+
...options.force !== void 0 ? { force: options.force } : {}
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
program2.command("validate").description("Validate local arbiter.yaml structure (no network calls)").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").action((options) => {
|
|
948
|
+
const code = runValidate({
|
|
949
|
+
...options.manifest !== void 0 ? { manifest: options.manifest } : {}
|
|
950
|
+
});
|
|
951
|
+
process.exit(code);
|
|
952
|
+
});
|
|
953
|
+
program2.command("plan").description("Show manifest diff against the remote workspace").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").option("--verbose", "Include unchanged resource summary").action(async (options) => {
|
|
954
|
+
const code = await runPlan({
|
|
955
|
+
...options.manifest !== void 0 ? { manifest: options.manifest } : {},
|
|
956
|
+
...options.verbose !== void 0 ? { verbose: options.verbose } : {}
|
|
957
|
+
});
|
|
958
|
+
process.exit(code);
|
|
959
|
+
});
|
|
960
|
+
program2.command("apply").description("Apply manifest changes to the remote workspace").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").option("--yes", "Skip confirmation prompt (required for CI/CD)").option("--verbose", "Include unchanged resource summary in plan preview").action(async (options) => {
|
|
961
|
+
const code = await runApply({
|
|
962
|
+
...options.manifest !== void 0 ? { manifest: options.manifest } : {},
|
|
963
|
+
...options.yes !== void 0 ? { yes: options.yes } : {},
|
|
964
|
+
...options.verbose !== void 0 ? { verbose: options.verbose } : {}
|
|
965
|
+
});
|
|
966
|
+
process.exit(code);
|
|
967
|
+
});
|
|
968
|
+
program2.command("drift").description("Show workspace drift and adoption suggestions").action(async () => {
|
|
969
|
+
const code = await runDrift();
|
|
970
|
+
process.exit(code);
|
|
971
|
+
});
|
|
972
|
+
program2.command("status").description("Show endpoint, manifest version, and state signature").option("-m, --manifest <path>", "Manifest file path", "arbiter.yaml").action(async (options) => {
|
|
973
|
+
const code = await runStatus({
|
|
974
|
+
...options.manifest !== void 0 ? { manifest: options.manifest } : {}
|
|
975
|
+
});
|
|
976
|
+
process.exit(code);
|
|
977
|
+
});
|
|
978
|
+
program2.command("whoami").description("Show active credential session context").action(() => {
|
|
979
|
+
const code = runWhoami();
|
|
980
|
+
process.exit(code);
|
|
981
|
+
});
|
|
982
|
+
return program2;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/index.ts
|
|
986
|
+
var program = buildCli();
|
|
987
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
988
|
+
const message = error instanceof Error ? error.message : "Unknown CLI failure";
|
|
989
|
+
process.stderr.write(`Error: ${message}
|
|
990
|
+
`);
|
|
991
|
+
process.exit(1);
|
|
992
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arbiterhq/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official Arbiter command line interface",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"arbiter": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@arbiterhq/sdk": "^0.2.0",
|
|
27
|
+
"commander": "^13.1.0",
|
|
28
|
+
"yaml": "^2.8.0",
|
|
29
|
+
"zod": "^3.25.76"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.15.21",
|
|
33
|
+
"tsup": "^8.5.0",
|
|
34
|
+
"typescript": "^5.8.3"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"arbiter",
|
|
38
|
+
"cli",
|
|
39
|
+
"governance"
|
|
40
|
+
],
|
|
41
|
+
"homepage": "https://github.com/sumitbirru1-halo/arbiter-sdk#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/sumitbirru1-halo/arbiter-sdk/issues"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/sumitbirru1-halo/arbiter-sdk.git",
|
|
48
|
+
"directory": "packages/cli"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|