@aarmos/cli 0.2.0 → 0.2.1

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 (2) hide show
  1. package/dist/index.js +211 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,8 +5,10 @@ import { Command as Command9 } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
7
  import { Command } from "commander";
8
- import { mkdirSync, writeFileSync, existsSync } from "fs";
9
- import { join } from "path";
8
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, statSync, cpSync, mkdtempSync, rmSync } from "fs";
9
+ import { join, dirname, relative } from "path";
10
+ import { tmpdir } from "os";
11
+ import { spawnSync } from "child_process";
10
12
 
11
13
  // src/lib/keys.ts
12
14
  import { generateKeyPairSync } from "crypto";
@@ -53,13 +55,28 @@ var GITIGNORE_TEMPLATE = `# Aarmos local state
53
55
  .aarmos/keys/
54
56
  .aarmos/avar/
55
57
  `;
58
+ var IMPORT_ALLOWLIST = [
59
+ "policy.aarmos.toml",
60
+ "avar.config.json",
61
+ "asp.yaml",
62
+ "asp.yml",
63
+ "policies",
64
+ "playbooks",
65
+ ".aarmos/packs"
66
+ ];
56
67
  function initCommand() {
57
- return new Command("init").description("Scaffold policy.aarmos.toml, avar.config.json, and local signing key").option("--force", "Overwrite existing files", false).action(async (opts) => {
68
+ return new Command("init").description("Scaffold policy.aarmos.toml, avar.config.json, and local signing key").option("--force", "Overwrite existing files", false).option(
69
+ "--from <repo>",
70
+ "Seed policies/playbooks from a GitHub repo (owner/repo[#ref][/subdir]) or tarball URL. Only Aarmos config files are copied; source code and keys are never imported."
71
+ ).action(async (opts) => {
58
72
  const cwd = process.cwd();
59
73
  const keyDir = join(cwd, ".aarmos", "keys");
60
74
  const chainDir = join(cwd, ".aarmos", "avar");
61
75
  mkdirSync(keyDir, { recursive: true });
62
76
  mkdirSync(chainDir, { recursive: true });
77
+ if (opts.from) {
78
+ await seedFromRepo(opts.from, cwd, opts.force);
79
+ }
63
80
  writeIfMissing(join(cwd, "policy.aarmos.toml"), POLICY_TEMPLATE, opts.force);
64
81
  writeIfMissing(join(cwd, "avar.config.json"), AVAR_CONFIG_TEMPLATE, opts.force);
65
82
  const gitignorePath = join(cwd, ".aarmos", ".gitignore");
@@ -78,16 +95,111 @@ function writeIfMissing(path, content, force) {
78
95
  console.log(`\xB7 skipped (exists): ${path}`);
79
96
  return;
80
97
  }
98
+ mkdirSync(dirname(path), { recursive: true });
81
99
  writeFileSync(path, content);
82
100
  console.log(`\u2713 wrote: ${path}`);
83
101
  }
102
+ function parseFrom(from) {
103
+ if (from.startsWith("http://") || from.startsWith("https://")) {
104
+ return { tarballUrl: from, subdir: "", label: from };
105
+ }
106
+ const hashIdx = from.indexOf("#");
107
+ const [pathPart, refPart] = hashIdx >= 0 ? [from.slice(0, hashIdx), from.slice(hashIdx + 1)] : [from, "main"];
108
+ const segs = pathPart.split("/").filter(Boolean);
109
+ if (segs.length < 2) throw new Error(`--from: expected owner/repo, got "${from}"`);
110
+ const [owner, repo, ...rest] = segs;
111
+ const subdir = rest.join("/");
112
+ return {
113
+ tarballUrl: `https://codeload.github.com/${owner}/${repo}/tar.gz/refs/heads/${refPart}`,
114
+ subdir,
115
+ label: `${owner}/${repo}${refPart !== "main" ? `#${refPart}` : ""}${subdir ? `/${subdir}` : ""}`
116
+ };
117
+ }
118
+ async function seedFromRepo(from, cwd, force) {
119
+ const parsed = parseFrom(from);
120
+ console.log(`\u25B8 seeding from ${parsed.label}`);
121
+ const staging = mkdtempSync(join(tmpdir(), "aarmos-init-"));
122
+ try {
123
+ const res = await fetch(parsed.tarballUrl);
124
+ if (!res.ok) throw new Error(`fetch ${parsed.tarballUrl}: ${res.status}`);
125
+ const buf = Buffer.from(await res.arrayBuffer());
126
+ const tarPath = join(staging, "src.tar.gz");
127
+ writeFileSync(tarPath, buf);
128
+ const extractDir = join(staging, "extract");
129
+ mkdirSync(extractDir);
130
+ const tar = spawnSync("tar", ["-xzf", tarPath, "-C", extractDir, "--strip-components=1"], {
131
+ stdio: ["ignore", "inherit", "inherit"]
132
+ });
133
+ if (tar.status !== 0) throw new Error("tar extract failed (is `tar` on PATH?)");
134
+ const root = parsed.subdir ? join(extractDir, parsed.subdir) : extractDir;
135
+ if (!existsSync(root)) throw new Error(`subdir not found in repo: ${parsed.subdir}`);
136
+ let copied = 0;
137
+ for (const rel of IMPORT_ALLOWLIST) {
138
+ const src = join(root, rel);
139
+ if (!existsSync(src)) continue;
140
+ const dest = join(cwd, rel);
141
+ if (existsSync(dest) && !force) {
142
+ console.log(`\xB7 skipped (exists): ${rel} \u2014 pass --force to overwrite`);
143
+ continue;
144
+ }
145
+ if (statSync(src).isDirectory()) {
146
+ const violations = findKeyLikeFiles(src);
147
+ if (violations.length > 0) {
148
+ console.log(`\u2717 refusing to copy ${rel}: contains key-like files: ${violations.slice(0, 3).join(", ")}`);
149
+ continue;
150
+ }
151
+ mkdirSync(dirname(dest), { recursive: true });
152
+ cpSync(src, dest, { recursive: true, force });
153
+ } else {
154
+ mkdirSync(dirname(dest), { recursive: true });
155
+ cpSync(src, dest, { force });
156
+ }
157
+ console.log(`\u2713 imported: ${rel}`);
158
+ copied++;
159
+ }
160
+ if (copied === 0) {
161
+ console.log("\xB7 no Aarmos config artifacts found in that repo; falling back to defaults");
162
+ }
163
+ } finally {
164
+ try {
165
+ rmSync(staging, { recursive: true, force: true });
166
+ } catch {
167
+ }
168
+ }
169
+ }
170
+ function findKeyLikeFiles(dir) {
171
+ const out = [];
172
+ const walk = (d) => {
173
+ for (const name of readdirSync(d)) {
174
+ const full = join(d, name);
175
+ const st = statSync(full);
176
+ if (st.isDirectory()) {
177
+ walk(full);
178
+ continue;
179
+ }
180
+ if (/\.(key|pem|p12|pfx)$/i.test(name) || /signing\.key$/i.test(name)) {
181
+ out.push(relative(dir, full));
182
+ continue;
183
+ }
184
+ if (st.size < 32e3) {
185
+ try {
186
+ const head = readFileSync(full, "utf8").slice(0, 200);
187
+ if (/-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(head)) out.push(relative(dir, full));
188
+ } catch {
189
+ }
190
+ }
191
+ }
192
+ };
193
+ walk(dir);
194
+ return out;
195
+ }
84
196
 
85
197
  // src/commands/run.ts
86
198
  import { Command as Command2 } from "commander";
87
199
  import { spawn } from "child_process";
88
200
 
89
201
  // src/lib/avar.ts
90
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync } from "fs";
202
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
91
203
  import { join as join2 } from "path";
92
204
  import { createHash } from "crypto";
93
205
  var CHAIN_DIR = ".aarmos/avar";
@@ -131,10 +243,10 @@ function serialize(receipt) {
131
243
  }
132
244
 
133
245
  // src/lib/policy.ts
134
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
246
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
135
247
  import { createHash as createHash2 } from "crypto";
136
248
  function loadPolicy(path) {
137
- const raw = existsSync3(path) ? readFileSync2(path, "utf8") : "";
249
+ const raw = existsSync3(path) ? readFileSync3(path, "utf8") : "";
138
250
  const digest = createHash2("sha256").update(raw).digest("hex");
139
251
  return { path, raw, digest };
140
252
  }
@@ -242,7 +354,7 @@ function stripHopByHop(headers) {
242
354
 
243
355
  // src/commands/daemon.ts
244
356
  import { Command as Command4 } from "commander";
245
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
357
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
246
358
  import { join as join3 } from "path";
247
359
  import { spawn as spawn2 } from "child_process";
248
360
  var PID_FILE = join3(process.cwd(), ".aarmos", "daemon.pid");
@@ -267,7 +379,7 @@ function daemonCommand() {
267
379
  console.log("\xB7 no daemon running");
268
380
  return;
269
381
  }
270
- const pid = Number(readFileSync3(PID_FILE, "utf8"));
382
+ const pid = Number(readFileSync4(PID_FILE, "utf8"));
271
383
  try {
272
384
  process.kill(pid);
273
385
  } catch {
@@ -280,14 +392,14 @@ function daemonCommand() {
280
392
  console.log("stopped");
281
393
  return;
282
394
  }
283
- const pid = Number(readFileSync3(PID_FILE, "utf8"));
395
+ const pid = Number(readFileSync4(PID_FILE, "utf8"));
284
396
  console.log(`running (pid ${pid})`);
285
397
  });
286
398
  return cmd;
287
399
  }
288
400
  function isRunning() {
289
401
  if (!existsSync4(PID_FILE)) return false;
290
- const pid = Number(readFileSync3(PID_FILE, "utf8"));
402
+ const pid = Number(readFileSync4(PID_FILE, "utf8"));
291
403
  try {
292
404
  process.kill(pid, 0);
293
405
  return true;
@@ -298,7 +410,7 @@ function isRunning() {
298
410
 
299
411
  // src/commands/verify.ts
300
412
  import { Command as Command5 } from "commander";
301
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
413
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
302
414
  import { unzipSync, strFromU8 } from "fflate";
303
415
 
304
416
  // ../avar-core/src/canonicalize.ts
@@ -737,7 +849,7 @@ async function runVerify(opts) {
737
849
  }
738
850
  let bytes;
739
851
  try {
740
- bytes = readFileSync4(file);
852
+ bytes = readFileSync5(file);
741
853
  } catch (err) {
742
854
  return emitError(
743
855
  aarmosError("FILE_UNREADABLE", {
@@ -863,7 +975,7 @@ function verifyCommand() {
863
975
 
864
976
  // src/commands/lint.ts
865
977
  import { Command as Command6 } from "commander";
866
- import { existsSync as existsSync6, readFileSync as readFileSync5, statSync, readdirSync } from "fs";
978
+ import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync2, readdirSync as readdirSync2 } from "fs";
867
979
  import { join as join4, extname } from "path";
868
980
 
869
981
  // src/lib/asp-compile.ts
@@ -961,12 +1073,12 @@ function keygen() {
961
1073
  // src/commands/lint.ts
962
1074
  function collectFiles(target) {
963
1075
  if (!existsSync6(target)) return [];
964
- const st = statSync(target);
1076
+ const st = statSync2(target);
965
1077
  if (st.isFile()) return [target];
966
1078
  const out = [];
967
- for (const name of readdirSync(target)) {
1079
+ for (const name of readdirSync2(target)) {
968
1080
  const full = join4(target, name);
969
- const s = statSync(full);
1081
+ const s = statSync2(full);
970
1082
  if (s.isDirectory()) {
971
1083
  if (name === "tests" || name === "node_modules" || name.startsWith(".")) continue;
972
1084
  out.push(...collectFiles(full));
@@ -986,7 +1098,7 @@ function lintPath(target) {
986
1098
  return issues;
987
1099
  }
988
1100
  for (const file of files) {
989
- const src = readFileSync5(file, "utf8");
1101
+ const src = readFileSync6(file, "utf8");
990
1102
  const result = compileAspYamlToCanonicalJson(src);
991
1103
  if (!result.ok) {
992
1104
  for (const m of result.errors) issues.push({ severity: "error", file, message: m });
@@ -1041,8 +1153,8 @@ function lintCommand() {
1041
1153
 
1042
1154
  // src/commands/test.ts
1043
1155
  import { Command as Command7 } from "commander";
1044
- import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1045
- import { join as join5, extname as extname2, dirname } from "path";
1156
+ import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
1157
+ import { join as join5, extname as extname2, dirname as dirname2 } from "path";
1046
1158
  import { parse as parseYaml2 } from "yaml";
1047
1159
 
1048
1160
  // src/lib/asp-eval.ts
@@ -1086,7 +1198,7 @@ function findAspYaml(startDir) {
1086
1198
  const p = join5(d, cand);
1087
1199
  if (existsSync7(p)) return p;
1088
1200
  }
1089
- const parent = dirname(d);
1201
+ const parent = dirname2(d);
1090
1202
  if (parent === d) break;
1091
1203
  d = parent;
1092
1204
  }
@@ -1094,12 +1206,12 @@ function findAspYaml(startDir) {
1094
1206
  }
1095
1207
  function collectFixtures(target) {
1096
1208
  if (!existsSync7(target)) return [];
1097
- const st = statSync2(target);
1209
+ const st = statSync3(target);
1098
1210
  if (st.isFile()) return [target];
1099
1211
  const out = [];
1100
- for (const name of readdirSync2(target)) {
1212
+ for (const name of readdirSync3(target)) {
1101
1213
  const full = join5(target, name);
1102
- const s = statSync2(full);
1214
+ const s = statSync3(full);
1103
1215
  if (s.isDirectory()) out.push(...collectFixtures(full));
1104
1216
  else if ((extname2(name) === ".yaml" || extname2(name) === ".yml") && !name.endsWith(".asp.yaml"))
1105
1217
  out.push(full);
@@ -1107,23 +1219,23 @@ function collectFixtures(target) {
1107
1219
  return out;
1108
1220
  }
1109
1221
  function runFixtures(target) {
1110
- const dir = existsSync7(target) && statSync2(target).isDirectory() ? target : "policies/tests";
1222
+ const dir = existsSync7(target) && statSync3(target).isDirectory() ? target : "policies/tests";
1111
1223
  const fixtures = collectFixtures(existsSync7(dir) ? dir : target);
1112
1224
  const results = [];
1113
1225
  for (const file of fixtures) {
1114
1226
  let fx;
1115
1227
  try {
1116
- fx = parseYaml2(readFileSync6(file, "utf8"));
1228
+ fx = parseYaml2(readFileSync7(file, "utf8"));
1117
1229
  } catch (err) {
1118
1230
  results.push({ file, name: "<parse>", ok: false, reason: err.message });
1119
1231
  continue;
1120
1232
  }
1121
- const policyPath = fx.policy ? join5(dirname(file), fx.policy) : findAspYaml(dirname(file));
1233
+ const policyPath = fx.policy ? join5(dirname2(file), fx.policy) : findAspYaml(dirname2(file));
1122
1234
  if (!policyPath || !existsSync7(policyPath)) {
1123
1235
  results.push({ file, name: "<policy>", ok: false, reason: "no asp.yaml found" });
1124
1236
  continue;
1125
1237
  }
1126
- const compiled = compileAspYamlToCanonicalJson(readFileSync6(policyPath, "utf8"));
1238
+ const compiled = compileAspYamlToCanonicalJson(readFileSync7(policyPath, "utf8"));
1127
1239
  if (!compiled.ok) {
1128
1240
  results.push({ file, name: "<compile>", ok: false, reason: compiled.errors.join("; ") });
1129
1241
  continue;
@@ -1162,14 +1274,44 @@ ${results.length - failed.length}/${results.length} passed.
1162
1274
 
1163
1275
  // src/commands/policy.ts
1164
1276
  import { Command as Command8 } from "commander";
1165
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
1277
+ import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync4, watch as fsWatch, statSync as statSync4 } from "fs";
1278
+ import { dirname as dirname3, resolve } from "path";
1166
1279
  function readAspOrDie(path) {
1167
1280
  if (!existsSync8(path)) {
1168
1281
  process.stderr.write(`\u2717 file not found: ${path}
1169
1282
  `);
1170
1283
  process.exit(1);
1171
1284
  }
1172
- return readFileSync7(path, "utf8");
1285
+ return readFileSync8(path, "utf8");
1286
+ }
1287
+ function runPolicyOnce(policyPath, testsDir, opts = {}) {
1288
+ const started = Date.now();
1289
+ let output = `
1290
+ \u25B8 ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} ${opts.label ?? policyPath}
1291
+ `;
1292
+ const src = readFileSync8(policyPath, "utf8");
1293
+ const compiled = compileAspYamlToCanonicalJson(src);
1294
+ if (!compiled.ok) {
1295
+ for (const m of compiled.errors) output += ` \u2717 compile: ${m}
1296
+ `;
1297
+ return { ok: false, output, passed: 0, total: 0 };
1298
+ }
1299
+ const toolCount = (compiled.body.policy.allowedTools?.length ?? 0) + (compiled.body.policy.deniedTools?.length ?? 0);
1300
+ output += ` \u2713 compiled (${toolCount} tool rule[s], issuer=${compiled.body.issuer?.pubKey ?? "(unsigned)"})
1301
+ `;
1302
+ if (!existsSync8(testsDir)) {
1303
+ output += ` \xB7 no fixtures at ${opts.testsLabel ?? testsDir} \u2014 skipping tests
1304
+ `;
1305
+ return { ok: true, output, passed: 0, total: 0 };
1306
+ }
1307
+ const results = runFixtures(testsDir);
1308
+ const failed = results.filter((r) => !r.ok);
1309
+ for (const r of failed) output += ` \u2717 ${r.file} :: ${r.name} \u2014 ${r.reason}
1310
+ `;
1311
+ const passed = results.length - failed.length;
1312
+ output += ` ${failed.length === 0 ? "\u2713" : "\u2717"} ${passed}/${results.length} passed (${Date.now() - started}ms)
1313
+ `;
1314
+ return { ok: failed.length === 0, output, passed, total: results.length };
1173
1315
  }
1174
1316
  function policyCommand() {
1175
1317
  const cmd = new Command8("policy").description("Compile / sign ASP YAML policies.");
@@ -1224,6 +1366,46 @@ AARMOS_POLICY_PRIVATE_KEY (keep secret):
1224
1366
  `
1225
1367
  );
1226
1368
  });
1369
+ cmd.command("watch <file>").description(
1370
+ "Recompile ASP YAML on save and re-run fixtures. Zero network, zero side effects \u2014 the tightest feedback loop for authoring policy."
1371
+ ).option("--tests <dir>", "Fixtures directory to re-run on each change", "policies/tests").option("--debounce <ms>", "Debounce interval in ms", "150").action((file, opts) => {
1372
+ const policyPath = resolve(file);
1373
+ if (!existsSync8(policyPath)) {
1374
+ process.stderr.write(`\u2717 file not found: ${policyPath}
1375
+ `);
1376
+ process.exit(1);
1377
+ }
1378
+ const testsDir = resolve(opts.tests);
1379
+ const debounceMs = Math.max(0, Number(opts.debounce) || 150);
1380
+ const runOnce = () => {
1381
+ const { output } = runPolicyOnce(policyPath, testsDir, { label: file, testsLabel: opts.tests });
1382
+ process.stdout.write(output);
1383
+ };
1384
+ process.stdout.write(`aarmos policy watch
1385
+ policy : ${policyPath}
1386
+ tests : ${testsDir}
1387
+ `);
1388
+ runOnce();
1389
+ let timer = null;
1390
+ const schedule = () => {
1391
+ if (timer) clearTimeout(timer);
1392
+ timer = setTimeout(runOnce, debounceMs);
1393
+ };
1394
+ const watchTarget = (p) => {
1395
+ try {
1396
+ const st = statSync4(p);
1397
+ const opts2 = st.isDirectory() ? { recursive: true } : void 0;
1398
+ fsWatch(p, opts2, () => schedule());
1399
+ } catch {
1400
+ }
1401
+ };
1402
+ watchTarget(policyPath);
1403
+ watchTarget(dirname3(policyPath));
1404
+ if (existsSync8(testsDir)) watchTarget(testsDir);
1405
+ process.stdout.write("\nwatching for changes \u2014 Ctrl+C to exit\n");
1406
+ setInterval(() => {
1407
+ }, 1 << 30);
1408
+ });
1227
1409
  return cmd;
1228
1410
  }
1229
1411
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarmos/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Aarmos CLI — run any agent locally, across any protocol (MCP, OpenAPI, deep-link), with a signed AVAR receipt on every execution. Sovereign runtime, universal tool gateway, verifiable governance.",
5
5
  "keywords": [
6
6
  "aarmos",