@traits-dev/cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Justin Hambleton
3
+ Copyright (c) 2026 FRNTR, LLC
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @traits-dev/cli
2
2
 
3
- CLI for traits.dev voice profile workflows: init, validate, compile, eval, and import.
3
+ CLI for traits.dev voice profile workflows: init, validate, compile, eval, import, and migrate.
4
4
 
5
5
  ## Install
6
6
 
@@ -13,7 +13,9 @@ pnpm add -D @traits-dev/cli
13
13
  ```bash
14
14
  traits init --template resolve profiles/resolve.yaml
15
15
  traits validate profiles/resolve.yaml
16
+ traits validate profiles/resolve.yaml --format sarif
16
17
  traits compile profiles/resolve.yaml --model gpt-4o
18
+ traits migrate profiles/legacy-v1-4.yaml
17
19
  traits eval profiles/resolve.yaml --tier 1
18
20
  ```
19
21
 
package/dist/traits.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin/traits.ts
4
- import fs4 from "fs";
5
- import path6 from "path";
4
+ import fs5 from "fs";
5
+ import path7 from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  // src/commands/compile.ts
@@ -1446,21 +1446,238 @@ function importHelp(out = process.stdout) {
1446
1446
  printImportUsage(out);
1447
1447
  }
1448
1448
 
1449
- // src/commands/validate.ts
1449
+ // src/commands/migrate.ts
1450
+ import fs4 from "fs";
1450
1451
  import path5 from "path";
1452
+ import { loadProfileFile, validateResolvedProfile as validateResolvedProfile2 } from "@traits-dev/core";
1453
+ import { renderImportedProfileYAML, toValidationResultObject as toValidationResultObject4 } from "@traits-dev/core/internal";
1454
+ function printMigrateUsage(out = process.stderr) {
1455
+ out.write(
1456
+ [
1457
+ "Usage:",
1458
+ " traits migrate <profile-path> [options]",
1459
+ "",
1460
+ "Options:",
1461
+ " --to <version> Target schema version (default: v1.5)",
1462
+ " --output <path> Output file path (default: <name>.v1.5.yaml)",
1463
+ " --in-place Overwrite the source file (requires --force if file exists)",
1464
+ " --force Overwrite existing destination file",
1465
+ " --json Output structured JSON summary",
1466
+ " --verbose Include additional command metadata",
1467
+ " --no-color Disable colorized output",
1468
+ ""
1469
+ ].join("\n")
1470
+ );
1471
+ }
1472
+ function parseMigrateArgs(args) {
1473
+ const result = {
1474
+ profilePath: null,
1475
+ to: "v1.5",
1476
+ outputPath: null,
1477
+ inPlace: false,
1478
+ force: false,
1479
+ json: false,
1480
+ verbose: false,
1481
+ noColor: false
1482
+ };
1483
+ const positionals = [];
1484
+ for (let index = 0; index < args.length; index += 1) {
1485
+ const arg = args[index];
1486
+ if (arg === "--in-place") {
1487
+ result.inPlace = true;
1488
+ continue;
1489
+ }
1490
+ if (arg === "--force") {
1491
+ result.force = true;
1492
+ continue;
1493
+ }
1494
+ if (arg === "--json") {
1495
+ result.json = true;
1496
+ continue;
1497
+ }
1498
+ if (arg === "--verbose") {
1499
+ result.verbose = true;
1500
+ continue;
1501
+ }
1502
+ if (arg === "--no-color") {
1503
+ result.noColor = true;
1504
+ continue;
1505
+ }
1506
+ if (arg === "--to" || arg === "--output") {
1507
+ const value = args[index + 1];
1508
+ if (!value) return { error: `Missing value for "${arg}"` };
1509
+ if (arg === "--to") {
1510
+ if (value !== "v1.5") {
1511
+ return {
1512
+ error: `Unsupported "--to" value "${value}". Currently supported: v1.5`
1513
+ };
1514
+ }
1515
+ result.to = "v1.5";
1516
+ } else {
1517
+ result.outputPath = value;
1518
+ }
1519
+ index += 1;
1520
+ continue;
1521
+ }
1522
+ if (arg.startsWith("--")) {
1523
+ return { error: `Unknown option "${arg}"` };
1524
+ }
1525
+ positionals.push(arg);
1526
+ }
1527
+ if (positionals.length !== 1) {
1528
+ return { error: "Expected exactly one profile path argument" };
1529
+ }
1530
+ result.profilePath = positionals[0];
1531
+ if (result.inPlace && result.outputPath) {
1532
+ return { error: 'Use either "--in-place" or "--output", not both.' };
1533
+ }
1534
+ return { value: result };
1535
+ }
1536
+ function resolveDefaultOutputPath(sourcePath, toVersion) {
1537
+ const ext = path5.extname(sourcePath) || ".yaml";
1538
+ const base = sourcePath.slice(0, sourcePath.length - ext.length);
1539
+ return `${base}.${toVersion}${ext}`;
1540
+ }
1541
+ function writeFileAtomic2(filePath, contents) {
1542
+ fs4.mkdirSync(path5.dirname(filePath), { recursive: true });
1543
+ const tempFile = path5.join(
1544
+ path5.dirname(filePath),
1545
+ `.${path5.basename(filePath)}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
1546
+ );
1547
+ fs4.writeFileSync(tempFile, contents, "utf8");
1548
+ fs4.renameSync(tempFile, filePath);
1549
+ }
1550
+ function migrateV14ToV15(profile) {
1551
+ const migrated = JSON.parse(JSON.stringify(profile));
1552
+ migrated.schema = "v1.5";
1553
+ if (!migrated.capabilities) {
1554
+ migrated.capabilities = {
1555
+ tools: [],
1556
+ constraints: [],
1557
+ handoff: {
1558
+ trigger: "Request requires unavailable operations.",
1559
+ action: "Offer handoff to a human operator."
1560
+ }
1561
+ };
1562
+ return { migrated, capabilitiesAdded: true };
1563
+ }
1564
+ return { migrated, capabilitiesAdded: false };
1565
+ }
1566
+ function runMigrate(args, io = process) {
1567
+ const parsed = parseMigrateArgs(args);
1568
+ if ("error" in parsed) {
1569
+ io.stderr.write(`Error: ${parsed.error}
1570
+
1571
+ `);
1572
+ printMigrateUsage(io.stderr);
1573
+ return 1;
1574
+ }
1575
+ const options = parsed.value;
1576
+ if (!options.profilePath) {
1577
+ io.stderr.write("Error: Missing profile path\n\n");
1578
+ printMigrateUsage(io.stderr);
1579
+ return 1;
1580
+ }
1581
+ const sourcePath = path5.resolve(io.cwd(), options.profilePath);
1582
+ const destinationPath = options.inPlace ? sourcePath : options.outputPath ? path5.resolve(io.cwd(), options.outputPath) : resolveDefaultOutputPath(sourcePath, options.to);
1583
+ if (!fs4.existsSync(sourcePath)) {
1584
+ io.stderr.write(`Error: Source profile does not exist: ${sourcePath}
1585
+ `);
1586
+ return 1;
1587
+ }
1588
+ if (fs4.existsSync(destinationPath) && !options.force) {
1589
+ io.stderr.write(
1590
+ `Error: Destination file already exists: ${destinationPath}. Use --force to overwrite.
1591
+ `
1592
+ );
1593
+ return 1;
1594
+ }
1595
+ let loaded;
1596
+ try {
1597
+ loaded = loadProfileFile(sourcePath);
1598
+ } catch (error) {
1599
+ io.stderr.write(
1600
+ `Error: Failed to read profile: ${error instanceof Error ? error.message : String(error)}
1601
+ `
1602
+ );
1603
+ return 1;
1604
+ }
1605
+ if (loaded.schema !== "v1.4") {
1606
+ io.stderr.write(
1607
+ `Error: Migration source schema must be "v1.4". Found "${loaded.schema ?? "unknown"}".
1608
+ `
1609
+ );
1610
+ return 1;
1611
+ }
1612
+ const migration = migrateV14ToV15(loaded);
1613
+ const validation = validateResolvedProfile2(migration.migrated, {
1614
+ strict: false
1615
+ });
1616
+ if (validation.errors.length > 0) {
1617
+ io.stderr.write(
1618
+ `Error: Migrated profile is invalid and was not written (${validation.errors.length} error(s)).
1619
+ `
1620
+ );
1621
+ return 2;
1622
+ }
1623
+ const yaml = renderImportedProfileYAML(migration.migrated);
1624
+ writeFileAtomic2(destinationPath, yaml);
1625
+ if (options.json) {
1626
+ io.stdout.write(
1627
+ `${JSON.stringify(
1628
+ {
1629
+ migrated: true,
1630
+ sourcePath,
1631
+ outputPath: destinationPath,
1632
+ fromSchema: "v1.4",
1633
+ toSchema: "v1.5",
1634
+ capabilitiesAdded: migration.capabilitiesAdded,
1635
+ validation: toValidationResultObject4(validation)
1636
+ },
1637
+ null,
1638
+ 2
1639
+ )}
1640
+ `
1641
+ );
1642
+ return 0;
1643
+ }
1644
+ if (options.verbose) {
1645
+ io.stdout.write(`Source: ${sourcePath}
1646
+ `);
1647
+ io.stdout.write(`Output: ${destinationPath}
1648
+ `);
1649
+ io.stdout.write(`Capabilities added: ${migration.capabilitiesAdded ? "yes" : "no"}
1650
+ `);
1651
+ io.stdout.write(`Validation warnings: ${validation.warnings.length}
1652
+
1653
+ `);
1654
+ }
1655
+ io.stdout.write(`Migrated profile schema v1.4 -> v1.5
1656
+ `);
1657
+ io.stdout.write(`Wrote: ${destinationPath}
1658
+ `);
1659
+ return 0;
1660
+ }
1661
+ function migrateHelp(out = process.stdout) {
1662
+ printMigrateUsage(out);
1663
+ }
1664
+
1665
+ // src/commands/validate.ts
1666
+ import path6 from "path";
1451
1667
  import { validateProfile as validateProfile2 } from "@traits-dev/core";
1452
1668
  import {
1453
1669
  formatValidationResult as formatValidationResult4,
1454
- toValidationResultObject as toValidationResultObject4
1670
+ toValidationResultObject as toValidationResultObject5
1455
1671
  } from "@traits-dev/core/internal";
1456
1672
  function printValidateUsage(out = process.stderr) {
1457
1673
  out.write(
1458
1674
  [
1459
1675
  "Usage:",
1460
- " traits validate <profile-path> [--json] [--strict] [--bundled-profiles-dir <dir>]",
1676
+ " traits validate <profile-path> [options]",
1461
1677
  "",
1462
1678
  "Options:",
1463
1679
  " --json Output structured JSON",
1680
+ " --format <text|json|sarif> Output format (default: text)",
1464
1681
  " --strict Promote warnings to errors",
1465
1682
  " --verbose Include additional command metadata",
1466
1683
  " --no-color Disable colorized output",
@@ -1474,6 +1691,7 @@ function parseValidateArgs(args) {
1474
1691
  profilePath: null,
1475
1692
  strict: false,
1476
1693
  json: false,
1694
+ format: "text",
1477
1695
  verbose: false,
1478
1696
  noColor: false,
1479
1697
  bundledProfilesDir: null
@@ -1487,6 +1705,7 @@ function parseValidateArgs(args) {
1487
1705
  }
1488
1706
  if (arg === "--json") {
1489
1707
  result.json = true;
1708
+ result.format = "json";
1490
1709
  continue;
1491
1710
  }
1492
1711
  if (arg === "--verbose") {
@@ -1506,6 +1725,20 @@ function parseValidateArgs(args) {
1506
1725
  index += 1;
1507
1726
  continue;
1508
1727
  }
1728
+ if (arg === "--format") {
1729
+ const nextValue = args[index + 1];
1730
+ if (!nextValue) {
1731
+ return { error: 'Missing value for "--format"' };
1732
+ }
1733
+ const normalized = String(nextValue).toLowerCase();
1734
+ if (normalized !== "text" && normalized !== "json" && normalized !== "sarif") {
1735
+ return { error: 'Invalid "--format" value. Expected text, json, or sarif.' };
1736
+ }
1737
+ result.format = normalized;
1738
+ result.json = normalized === "json";
1739
+ index += 1;
1740
+ continue;
1741
+ }
1509
1742
  if (arg.startsWith("--")) {
1510
1743
  return { error: `Unknown option "${arg}"` };
1511
1744
  }
@@ -1517,6 +1750,54 @@ function parseValidateArgs(args) {
1517
1750
  result.profilePath = positionals[0];
1518
1751
  return { value: result };
1519
1752
  }
1753
+ function toRelativePath(cwd, filePath) {
1754
+ const relative = path6.relative(cwd, filePath);
1755
+ if (relative && !relative.startsWith("..")) return relative;
1756
+ return filePath;
1757
+ }
1758
+ function buildSarifReport(validation, profilePath, cwd) {
1759
+ const diagnostics = [...validation.errors, ...validation.warnings];
1760
+ const uniqueRuleIds = [...new Set(diagnostics.map((diagnostic) => diagnostic.code))];
1761
+ const artifactUri = toRelativePath(cwd, profilePath);
1762
+ const rules = uniqueRuleIds.map((ruleId) => ({
1763
+ id: ruleId,
1764
+ shortDescription: {
1765
+ text: `traits.dev ${ruleId}`
1766
+ }
1767
+ }));
1768
+ const results = diagnostics.map((diagnostic) => ({
1769
+ ruleId: diagnostic.code,
1770
+ level: diagnostic.severity === "error" ? "error" : "warning",
1771
+ message: {
1772
+ text: diagnostic.message
1773
+ },
1774
+ locations: [
1775
+ {
1776
+ physicalLocation: {
1777
+ artifactLocation: {
1778
+ uri: artifactUri
1779
+ }
1780
+ }
1781
+ }
1782
+ ]
1783
+ }));
1784
+ return {
1785
+ version: "2.1.0",
1786
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
1787
+ runs: [
1788
+ {
1789
+ tool: {
1790
+ driver: {
1791
+ name: "traits.dev",
1792
+ informationUri: "https://github.com/justinhambleton/traits",
1793
+ rules
1794
+ }
1795
+ },
1796
+ results
1797
+ }
1798
+ ]
1799
+ };
1800
+ }
1520
1801
  function runValidate(args, io = process) {
1521
1802
  const parsed = parseValidateArgs(args);
1522
1803
  if ("error" in parsed) {
@@ -1532,14 +1813,18 @@ function runValidate(args, io = process) {
1532
1813
  printValidateUsage(io.stderr);
1533
1814
  return 1;
1534
1815
  }
1535
- const bundledProfilesDir = options.bundledProfilesDir ? path5.resolve(io.cwd(), options.bundledProfilesDir) : path5.resolve(io.cwd(), "profiles");
1536
- const profilePath = path5.resolve(io.cwd(), options.profilePath);
1816
+ const bundledProfilesDir = options.bundledProfilesDir ? path6.resolve(io.cwd(), options.bundledProfilesDir) : path6.resolve(io.cwd(), "profiles");
1817
+ const profilePath = path6.resolve(io.cwd(), options.profilePath);
1537
1818
  const result = validateProfile2(profilePath, {
1538
1819
  strict: options.strict,
1539
1820
  bundledProfilesDir
1540
1821
  });
1541
- if (options.json) {
1542
- io.stdout.write(`${JSON.stringify(toValidationResultObject4(result), null, 2)}
1822
+ if (options.format === "json") {
1823
+ io.stdout.write(`${JSON.stringify(toValidationResultObject5(result), null, 2)}
1824
+ `);
1825
+ } else if (options.format === "sarif") {
1826
+ const sarif = buildSarifReport(result, profilePath, io.cwd());
1827
+ io.stdout.write(`${JSON.stringify(sarif, null, 2)}
1543
1828
  `);
1544
1829
  } else {
1545
1830
  if (options.verbose) {
@@ -1561,14 +1846,14 @@ function validateHelp(out = process.stdout) {
1561
1846
  }
1562
1847
 
1563
1848
  // src/bin/traits.ts
1564
- var CLI_DIR = path6.dirname(fileURLToPath(import.meta.url));
1849
+ var CLI_DIR = path7.dirname(fileURLToPath(import.meta.url));
1565
1850
  var PACKAGE_JSON_CANDIDATES = [
1566
- path6.resolve(CLI_DIR, "../package.json"),
1567
- path6.resolve(CLI_DIR, "../../package.json")
1851
+ path7.resolve(CLI_DIR, "../package.json"),
1852
+ path7.resolve(CLI_DIR, "../../package.json")
1568
1853
  ];
1569
1854
  function resolvePackageJsonPath() {
1570
1855
  for (const candidate of PACKAGE_JSON_CANDIDATES) {
1571
- if (fs4.existsSync(candidate)) return candidate;
1856
+ if (fs5.existsSync(candidate)) return candidate;
1572
1857
  }
1573
1858
  return PACKAGE_JSON_CANDIDATES[0];
1574
1859
  }
@@ -1586,6 +1871,7 @@ function printRootUsage(out = process.stdout) {
1586
1871
  " compile <profile-path> Compile a profile for a target model",
1587
1872
  " eval <profile-path> Evaluate profile responses (Tier 1 scaffold)",
1588
1873
  " import [prompt-path] Import a profile from an existing system prompt",
1874
+ " migrate <profile-path> Migrate profile schema (v1.4 -> v1.5)",
1589
1875
  " validate <profile-path> Validate a voice profile",
1590
1876
  "",
1591
1877
  "Global flags:",
@@ -1605,7 +1891,7 @@ function printRootUsage(out = process.stdout) {
1605
1891
  }
1606
1892
  function readCliVersion() {
1607
1893
  try {
1608
- const raw = fs4.readFileSync(PACKAGE_JSON_PATH, "utf8");
1894
+ const raw = fs5.readFileSync(PACKAGE_JSON_PATH, "utf8");
1609
1895
  const pkg = JSON.parse(raw);
1610
1896
  return String(pkg.version ?? "0.0.0");
1611
1897
  } catch {
@@ -1651,7 +1937,7 @@ function withGlobalFlags(command, commandArgs, flags) {
1651
1937
  if (flags.noColor && !args.includes("--no-color")) {
1652
1938
  args.push("--no-color");
1653
1939
  }
1654
- if ((command === "validate" || command === "compile" || command === "eval" || command === "import") && flags.json && !args.includes("--json")) {
1940
+ if ((command === "validate" || command === "compile" || command === "eval" || command === "import" || command === "migrate") && flags.json && !args.includes("--json")) {
1655
1941
  args.push("--json");
1656
1942
  }
1657
1943
  return args;
@@ -1717,6 +2003,13 @@ async function run(argv, io = process) {
1717
2003
  }
1718
2004
  return runImport(commandArgs, io);
1719
2005
  }
2006
+ if (command === "migrate") {
2007
+ if (commandArgs.includes("--help") || commandArgs.includes("-h")) {
2008
+ migrateHelp(io.stdout);
2009
+ return 0;
2010
+ }
2011
+ return runMigrate(commandArgs, io);
2012
+ }
1720
2013
  io.stderr.write(`Error: Unknown command "${command}"
1721
2014
 
1722
2015
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@traits-dev/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "traits.dev command-line interface for voice profile init, validate, compile, eval, and import workflows.",
5
5
  "keywords": [
6
6
  "traits-dev",
@@ -41,7 +41,7 @@
41
41
  "provenance": true
42
42
  },
43
43
  "dependencies": {
44
- "@traits-dev/core": "^0.4.0"
44
+ "@traits-dev/core": "^0.5.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^25.2.3",