@attalabs/vinaya 0.7.1 → 0.8.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 (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +183 -24
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -19,12 +19,13 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
19
19
  | `vinaya doctrine` | Print the absolute path of the bundled doctrine's front door (`aeg-root/skills/aeg/SKILL.md`) on this machine. The committed root `VINAYA.md` pointer names the package, never a filesystem path — this command is the read-time resolution step it hands the reader. `--json` for the enveloped `{ root, entry }` form. |
20
20
  | `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
21
21
  | `vinaya new check <name>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<name>.ts`, ready to register in `vinaya.config.json` |
22
+ | `vinaya studio` | Launch Vinaya Studio. Inside a checkout that carries Studio's source (`apps/vinaya/web` — it lives in the attalabs monorepo, not this repository) it runs the dev app; a published install refuses with a clear message and exit 1, because no published build bundles the Studio app yet. Bundling Studio into the published package is tracked, unshipped work. |
22
23
 
23
24
  ## Config
24
25
 
25
26
  Hierarchical, file-level precedence:
26
27
 
27
- 1. Repo-local `vinaya.config.json` (walked up from `cwd` to the filesystem root)
28
+ 1. Repo-local `vinaya.config.json` (walked up from `cwd`, stopping at the enclosing repository's root — or the filesystem root when run outside a git repository)
28
29
  2. Global `~/.vinaya/config.json`
29
30
  3. `null` if neither exists
30
31
 
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync17 } from "node:fs";
5
- import { dirname as dirname7, join as join19 } from "node:path";
4
+ import { readFileSync as readFileSync18 } from "node:fs";
5
+ import { dirname as dirname8, join as join20 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
7
 
8
8
  // src/commands/archive.ts
@@ -332,6 +332,52 @@ var systemEnv3 = {
332
332
  ...process.env,
333
333
  PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
334
334
  };
335
+ var defaultExec = () => execFileAsync3("git", ["remote", "get-url", "origin"], { env: systemEnv3, timeout: 5000 });
336
+ var cached;
337
+ var inflight;
338
+ async function resolveRepo(exec = defaultExec) {
339
+ if (cached !== undefined)
340
+ return cached;
341
+ const fromEnv = process.env.AEG_REPO;
342
+ if (fromEnv) {
343
+ const parsed = parseOwnerRepo(fromEnv);
344
+ if (parsed) {
345
+ cached = parsed;
346
+ return cached;
347
+ }
348
+ }
349
+ if (inflight)
350
+ return inflight;
351
+ inflight = (async () => {
352
+ try {
353
+ const { stdout } = await exec();
354
+ cached = parseGitRemoteUrl(stdout.trim());
355
+ return cached;
356
+ } catch (err) {
357
+ const message = err instanceof Error ? err.message : String(err);
358
+ console.warn(`[aeg-forge-state] resolveRepo: git remote lookup failed (will retry): ${message}`);
359
+ return null;
360
+ } finally {
361
+ inflight = undefined;
362
+ }
363
+ })();
364
+ return inflight;
365
+ }
366
+ function parseGitRemoteUrl(url) {
367
+ const ssh = url.match(/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
368
+ if (ssh?.[1] && ssh[2])
369
+ return { owner: ssh[1], repo: ssh[2] };
370
+ const https = url.match(/^https?:\/\/(?:[^@]+@)?github\.com\/([^/]+)\/(.+?)(?:\.git)?\/?$/);
371
+ if (https?.[1] && https[2])
372
+ return { owner: https[1], repo: https[2] };
373
+ return null;
374
+ }
375
+ function parseOwnerRepo(value) {
376
+ const match = value.match(/^([^/]+)\/(.+)$/);
377
+ if (!match?.[1] || !match[2])
378
+ return null;
379
+ return { owner: match[1], repo: match[2] };
380
+ }
335
381
  // ../../packages/aeg-core/src/state-machine-model.ts
336
382
  var FORGE_FACT_INPUTS = [
337
383
  {
@@ -880,6 +926,8 @@ import { fileURLToPath } from "node:url";
880
926
  function packageRoot(moduleUrl) {
881
927
  let dir = dirname(fileURLToPath(moduleUrl));
882
928
  while (!existsSync(join(dir, "package.json"))) {
929
+ if (existsSync(join(dir, ".git")))
930
+ break;
883
931
  const parent = dirname(dir);
884
932
  if (parent === dir)
885
933
  break;
@@ -1744,6 +1792,8 @@ function findLocalConfig() {
1744
1792
  const candidate = join3(dir, LOCAL_CONFIG_FILENAME);
1745
1793
  if (existsSync2(candidate))
1746
1794
  return candidate;
1795
+ if (existsSync2(join3(dir, ".git")))
1796
+ return null;
1747
1797
  const parent = dirname2(dir);
1748
1798
  if (parent === dir)
1749
1799
  return null;
@@ -5463,9 +5513,104 @@ async function quickstartCommand(args) {
5463
5513
  process.exit(await runQuickstart(args, realDeps6()));
5464
5514
  }
5465
5515
 
5516
+ // src/commands/studio.ts
5517
+ import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
5518
+ import { existsSync as existsSync16, readFileSync as readFileSync16, renameSync } from "node:fs";
5519
+ import net from "node:net";
5520
+ import { dirname as dirname7, join as join18 } from "node:path";
5521
+ import { promisify as promisify5 } from "node:util";
5522
+
5523
+ // src/lib/studio-bundle.ts
5524
+ var STUDIO_NODE_MODULES_PACKED_DIRNAME = "_node_modules";
5525
+
5526
+ // src/commands/studio.ts
5527
+ var execFileAsync5 = promisify5(execFile5);
5528
+ function execFileForRepo(cwd) {
5529
+ return execFileAsync5("git", ["remote", "get-url", "origin"], { cwd, timeout: 5000 });
5530
+ }
5531
+ var PRIMARY_PORT = 3006;
5532
+ var FALLBACK_PORT = 3106;
5533
+ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
5534
+ let dir = cwd;
5535
+ for (;; ) {
5536
+ const webDir = join18(dir, "apps", "vinaya", "web");
5537
+ const pkgPath = join18(webDir, "package.json");
5538
+ if (existsSync16(pkgPath)) {
5539
+ try {
5540
+ const pkg = JSON.parse(readFileSync16(pkgPath, "utf-8"));
5541
+ if (pkg.name === "@atta/vinaya-web") {
5542
+ return { kind: "workspace", webDir };
5543
+ }
5544
+ } catch {}
5545
+ }
5546
+ if (existsSync16(join18(dir, ".git")))
5547
+ break;
5548
+ const parent = dirname7(dir);
5549
+ if (parent === dir)
5550
+ break;
5551
+ dir = parent;
5552
+ }
5553
+ const standaloneWebDir = join18(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya", "web");
5554
+ if (existsSync16(join18(standaloneWebDir, "server.js"))) {
5555
+ return { kind: "package", packageDir: standaloneWebDir };
5556
+ }
5557
+ return { kind: "missing" };
5558
+ }
5559
+ function spawnDev(webDir, args) {
5560
+ return new Promise((resolve2) => {
5561
+ const child = spawn2("bun", ["run", "dev", ...args], { cwd: webDir, stdio: "inherit" });
5562
+ child.on("exit", (code) => resolve2(code ?? 0));
5563
+ });
5564
+ }
5565
+ function isPortFree(port) {
5566
+ return new Promise((resolve2) => {
5567
+ const tester = net.createServer();
5568
+ tester.once("error", () => resolve2(false));
5569
+ tester.once("listening", () => tester.close(() => resolve2(true)));
5570
+ tester.listen(port, "127.0.0.1");
5571
+ });
5572
+ }
5573
+ function ensureStudioNodeModules(bundleRoot) {
5574
+ const real = join18(bundleRoot, "node_modules");
5575
+ const packed = join18(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
5576
+ if (!existsSync16(real) && existsSync16(packed)) {
5577
+ renameSync(packed, real);
5578
+ }
5579
+ }
5580
+ async function spawnStandalone(cwd, serverPath, bundleRoot) {
5581
+ ensureStudioNodeModules(bundleRoot);
5582
+ const repo = await resolveRepo(() => execFileForRepo(cwd));
5583
+ const primaryFree = await isPortFree(PRIMARY_PORT);
5584
+ const port = primaryFree ? PRIMARY_PORT : FALLBACK_PORT;
5585
+ if (!primaryFree) {
5586
+ console.info(`[studio] port ${PRIMARY_PORT} is taken — falling back to ${FALLBACK_PORT}`);
5587
+ }
5588
+ const env = { HOSTNAME: "127.0.0.1", ...process.env, PORT: String(port), VINAYA_REPO_ROOT: cwd };
5589
+ if (repo)
5590
+ env.AEG_REPO = `${repo.owner}/${repo.repo}`;
5591
+ return new Promise((resolve2) => {
5592
+ const child = spawn2("node", [serverPath], { cwd, stdio: "inherit", env });
5593
+ child.on("exit", (code) => resolve2(code ?? 0));
5594
+ });
5595
+ }
5596
+ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
5597
+ const target = resolveStudioTarget(cwd, moduleUrl);
5598
+ switch (target.kind) {
5599
+ case "workspace":
5600
+ return spawnDev(target.webDir, args);
5601
+ case "package": {
5602
+ const bundleRoot = join18(target.packageDir, "..", "..", "..");
5603
+ return spawnStandalone(cwd, join18(target.packageDir, "server.js"), bundleRoot);
5604
+ }
5605
+ case "missing":
5606
+ console.error("Vinaya Studio isn't available in this install — no published @attalabs/vinaya build bundles the Studio app yet. Inside a checkout that contains Studio's source (apps/vinaya/web), `vinaya studio` runs it directly.");
5607
+ return 1;
5608
+ }
5609
+ }
5610
+
5466
5611
  // src/commands/upgrade.ts
5467
- import { existsSync as existsSync16, readFileSync as readFileSync16, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
5468
- import { join as join18 } from "node:path";
5612
+ import { existsSync as existsSync17, readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "node:fs";
5613
+ import { join as join19 } from "node:path";
5469
5614
  function realDeps7() {
5470
5615
  return {
5471
5616
  detectRepo: detectGitRepo,
@@ -5483,12 +5628,12 @@ function flags2(args) {
5483
5628
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
5484
5629
  }
5485
5630
  function readManifest3(repoRoot) {
5486
- const p = join18(repoRoot, CONFIG_PATH);
5487
- if (!existsSync16(p))
5631
+ const p = join19(repoRoot, CONFIG_PATH);
5632
+ if (!existsSync17(p))
5488
5633
  return { kind: "missing" };
5489
5634
  let raw;
5490
5635
  try {
5491
- raw = JSON.parse(readFileSync16(p, "utf-8"));
5636
+ raw = JSON.parse(readFileSync17(p, "utf-8"));
5492
5637
  } catch (err) {
5493
5638
  return { kind: "invalid", error: `invalid JSON: ${err.message}` };
5494
5639
  }
@@ -5504,17 +5649,17 @@ function readManifest3(repoRoot) {
5504
5649
  return { kind: "ok", manifest: parsed.data.managed };
5505
5650
  }
5506
5651
  function writeManifestVersion(repoRoot, manifest) {
5507
- const configAbs = join18(repoRoot, CONFIG_PATH);
5508
- const seed = JSON.parse(readFileSync16(configAbs, "utf-8"));
5652
+ const configAbs = join19(repoRoot, CONFIG_PATH);
5653
+ const seed = JSON.parse(readFileSync17(configAbs, "utf-8"));
5509
5654
  const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
5510
5655
  writeFileSync9(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
5511
5656
  `, "utf-8");
5512
5657
  }
5513
5658
  function stripFor(repoRoot, path, marker, comment) {
5514
5659
  const abs2 = resolveManagedBlockPath(repoRoot, path);
5515
- if (!existsSync16(abs2))
5660
+ if (!existsSync17(abs2))
5516
5661
  return { path, marker, comment, present: false, removesHost: false };
5517
- const stripped = stripBlockFromContent(readFileSync16(abs2, "utf-8"), marker, comment);
5662
+ const stripped = stripBlockFromContent(readFileSync17(abs2, "utf-8"), marker, comment);
5518
5663
  if (stripped === null)
5519
5664
  return { path, marker, comment, present: false, removesHost: false };
5520
5665
  return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
@@ -5541,9 +5686,9 @@ function planHookRouting(repoRoot, manifest, recorded, hooksPathValue) {
5541
5686
  const blocked = [];
5542
5687
  for (const b of legacy) {
5543
5688
  const abs2 = resolveManagedBlockPath(repoRoot, b.path);
5544
- if (!existsSync16(abs2))
5689
+ if (!existsSync17(abs2))
5545
5690
  continue;
5546
- const stripped = stripBlockFromContent(readFileSync16(abs2, "utf-8"), b.marker, b.comment);
5691
+ const stripped = stripBlockFromContent(readFileSync17(abs2, "utf-8"), b.marker, b.comment);
5547
5692
  if (stripped === null)
5548
5693
  blocked.push(`${b.path} exists without vinaya's managed block`);
5549
5694
  else if (!blockStripLeavesEmpty(stripped))
@@ -5578,8 +5723,8 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
5578
5723
  const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
5579
5724
  for (const op of ops) {
5580
5725
  if (op.kind === "create-file") {
5581
- const abs2 = join18(repoRoot, op.path);
5582
- const exists = existsSync16(abs2);
5726
+ const abs2 = join19(repoRoot, op.path);
5727
+ const exists = existsSync17(abs2);
5583
5728
  const owned = ownedFiles.has(op.path);
5584
5729
  let action;
5585
5730
  if (op.path === CONFIG_PATH || op.path === DOC_OWNERS_PATH) {
@@ -5589,7 +5734,7 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
5589
5734
  } else if (!exists) {
5590
5735
  action = "recreate";
5591
5736
  hasChanges = true;
5592
- } else if (readFileSync16(abs2, "utf-8") !== op.content) {
5737
+ } else if (readFileSync17(abs2, "utf-8") !== op.content) {
5593
5738
  action = "regenerate";
5594
5739
  hasChanges = true;
5595
5740
  } else {
@@ -5602,11 +5747,11 @@ function planUpgrade(ops, repoRoot, manifest, routing) {
5602
5747
  let action;
5603
5748
  if (!owned) {
5604
5749
  action = "not-installed";
5605
- } else if (!existsSync16(abs2)) {
5750
+ } else if (!existsSync17(abs2)) {
5606
5751
  action = "recreate-host";
5607
5752
  hasChanges = true;
5608
5753
  } else {
5609
- const content = readFileSync16(abs2, "utf-8");
5754
+ const content = readFileSync17(abs2, "utf-8");
5610
5755
  const { begin, end } = markerLines(op.marker, op.comment);
5611
5756
  if (!(content.includes(begin) && content.includes(end))) {
5612
5757
  action = "recreate-append";
@@ -5706,7 +5851,7 @@ function renderUpgradeDiff(plan) {
5706
5851
  }
5707
5852
  function regenerateBlock(repoRoot, op) {
5708
5853
  const abs2 = resolveManagedBlockPath(repoRoot, op.path);
5709
- const content = readFileSync16(abs2, "utf-8");
5854
+ const content = readFileSync17(abs2, "utf-8");
5710
5855
  const stripped = stripBlockFromContent(content, op.marker, op.comment);
5711
5856
  if (stripped !== null) {
5712
5857
  writeFileSync9(abs2, stripped.endsWith(`
@@ -5719,7 +5864,7 @@ function applyUpgrade(plan, repoRoot) {
5719
5864
  for (const e of plan.entries) {
5720
5865
  if (e.kind === "create-file") {
5721
5866
  if (e.action === "regenerate" || e.action === "recreate") {
5722
- writeFileWithDirs(join18(repoRoot, e.op.path), e.op.content, e.op.mode);
5867
+ writeFileWithDirs(join19(repoRoot, e.op.path), e.op.content, e.op.mode);
5723
5868
  }
5724
5869
  } else {
5725
5870
  if (e.action === "recreate-host")
@@ -5734,9 +5879,9 @@ function applyUpgrade(plan, repoRoot) {
5734
5879
  if (!s.present)
5735
5880
  continue;
5736
5881
  const abs2 = resolveManagedBlockPath(repoRoot, s.path);
5737
- if (!existsSync16(abs2))
5882
+ if (!existsSync17(abs2))
5738
5883
  continue;
5739
- const stripped = stripBlockFromContent(readFileSync16(abs2, "utf-8"), s.marker, s.comment);
5884
+ const stripped = stripBlockFromContent(readFileSync17(abs2, "utf-8"), s.marker, s.comment);
5740
5885
  if (stripped === null)
5741
5886
  continue;
5742
5887
  if (blockStripLeavesEmpty(stripped))
@@ -6167,6 +6312,15 @@ var COMMANDS = [
6167
6312
  ],
6168
6313
  status: "shipped"
6169
6314
  },
6315
+ {
6316
+ name: "studio",
6317
+ description: "Launch Vinaya Studio — runs the Studio dev app when its source (apps/vinaya/web) is in a checkout above the current directory; published installs refuse with a clear message, since no published build bundles the Studio app yet",
6318
+ details: [
6319
+ "Resolution happens in this order: a workspace checkout carrying `apps/vinaya/web` (Studio's source, which lives in the attalabs monorepo — not this repository) runs the dev app directly; an install whose package root carries a `studio-standalone/` bundle would run that bundled server; anything else gets an explicit refusal and exit 1 rather than a silent no-op.",
6320
+ "No published `@attalabs/vinaya` build ships the `studio-standalone/` bundle today — producing and shipping it is the Studio-packaging work, tracked separately. Until it lands, this command is honest about the shapes it cannot serve instead of pretending to serve them."
6321
+ ],
6322
+ status: "shipped"
6323
+ },
6170
6324
  {
6171
6325
  name: "quickstart",
6172
6326
  description: "Guided wizard: init, doc-owners, project, commit, demo break, doctor, push — one command",
@@ -6206,9 +6360,9 @@ function printHelp() {
6206
6360
  }
6207
6361
 
6208
6362
  // src/index.ts
6209
- var PACKAGE_ROOT = join19(dirname7(fileURLToPath2(import.meta.url)), "..");
6363
+ var PACKAGE_ROOT = join20(dirname8(fileURLToPath2(import.meta.url)), "..");
6210
6364
  function readVersion2() {
6211
- const pkg = JSON.parse(readFileSync17(join19(PACKAGE_ROOT, "package.json"), "utf-8"));
6365
+ const pkg = JSON.parse(readFileSync18(join20(PACKAGE_ROOT, "package.json"), "utf-8"));
6212
6366
  return pkg.version;
6213
6367
  }
6214
6368
  var [, , command, ...args] = process.argv;
@@ -6228,6 +6382,11 @@ try {
6228
6382
  }
6229
6383
  break;
6230
6384
  }
6385
+ case "studio": {
6386
+ const code = await runStudio(process.cwd(), args);
6387
+ process.exit(code);
6388
+ break;
6389
+ }
6231
6390
  case "init": {
6232
6391
  const [subcommand, ...rest] = args;
6233
6392
  if (subcommand === "product") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attalabs/vinaya",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Vinaya — Agentic Engineering Harness. Deterministic checks every AI coding agent must satisfy before merge.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -38,6 +38,7 @@
38
38
  "scripts": {
39
39
  "build": "bun scripts/build.ts",
40
40
  "bundle-doctrine": "bun scripts/bundle-doctrine.ts",
41
+ "bundle-studio": "bun scripts/bundle-studio.ts",
41
42
  "prepack": "bun scripts/build.ts && bun scripts/bundle-doctrine.ts",
42
43
  "test": "bun test",
43
44
  "typecheck": "tsc --noEmit",
@@ -50,6 +51,7 @@
50
51
  },
51
52
  "devDependencies": {
52
53
  "@atta/aeg-core": "workspace:*",
54
+ "@atta/aeg-forge-state": "workspace:*",
53
55
  "@atta/typescript-config": "workspace:*",
54
56
  "@atta/vinaya-sources": "workspace:*",
55
57
  "@types/bun": "latest",