@haus-tech/haus-workflow 0.18.2 → 0.19.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.19.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.18.2...v0.19.0) (2026-06-11)
4
+
5
+ ### Features
6
+
7
+ - **clone:** implement single repository cloning command and related … ([#89](https://github.com/WeAreHausTech/haus-workflow/issues/89)) ([3ac9883](https://github.com/WeAreHausTech/haus-workflow/commit/3ac98838db79bf69a602f5c3c17cfd6a1a7f924a))
8
+
3
9
  ## [0.18.2](https://github.com/WeAreHausTech/haus-workflow/compare/v0.18.1...v0.18.2) (2026-06-11)
4
10
 
5
11
  ### Bug Fixes
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { readFileSync as readFileSync4 } from "fs";
5
- import path34 from "path";
5
+ import path35 from "path";
6
6
  import { Command } from "commander";
7
7
 
8
8
  // src/commands/apply.ts
@@ -554,8 +554,8 @@ function buildDenyRules() {
554
554
  for (const command of DANGEROUS_COMMANDS) {
555
555
  rules.push(`Bash(${command}:*)`);
556
556
  }
557
- for (const path35 of SENSITIVE_PATHS) {
558
- const pattern = SENSITIVE_DIRS.has(path35) ? `${path35}/**` : path35;
557
+ for (const path36 of SENSITIVE_PATHS) {
558
+ const pattern = SENSITIVE_DIRS.has(path36) ? `${path36}/**` : path36;
559
559
  for (const tool of FILE_TOOLS) {
560
560
  rules.push(`${tool}(${pattern})`);
561
561
  }
@@ -1643,8 +1643,67 @@ async function runCatalogAudit() {
1643
1643
  log("Catalog audit passed.");
1644
1644
  }
1645
1645
 
1646
- // src/commands/config.ts
1646
+ // src/commands/clone.ts
1647
+ import { existsSync as existsSync2 } from "fs";
1647
1648
  import path14 from "path";
1649
+
1650
+ // src/utils/exec.ts
1651
+ import { execa } from "execa";
1652
+ async function runCommand(command, args = [], options = {}) {
1653
+ try {
1654
+ const result = await execa(command, args, {
1655
+ reject: false,
1656
+ // non-zero exits are returned, not thrown
1657
+ ...options
1658
+ });
1659
+ return {
1660
+ command,
1661
+ args,
1662
+ stdout: String(result.stdout ?? ""),
1663
+ stderr: String(result.stderr ?? ""),
1664
+ exitCode: result.exitCode ?? 0
1665
+ };
1666
+ } catch (error2) {
1667
+ const message = error2 instanceof Error ? error2.message : String(error2);
1668
+ throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
1669
+ }
1670
+ }
1671
+ async function runGit(args, options = {}) {
1672
+ return runCommand("git", args, options);
1673
+ }
1674
+
1675
+ // src/commands/clone.ts
1676
+ function repoNameFromUrl(url) {
1677
+ const trimmed = url.trim().replace(/\.git$/, "").replace(/\/+$/, "");
1678
+ const tail = trimmed.split(/[/:]/).pop() ?? "";
1679
+ return tail || "repo";
1680
+ }
1681
+ async function runClone(url, opts = {}) {
1682
+ if (!url || !url.trim()) {
1683
+ error("A git URL is required: `haus clone <url> [dir]`.");
1684
+ process.exitCode = 1;
1685
+ return;
1686
+ }
1687
+ const target = path14.resolve(opts.dir?.trim() || repoNameFromUrl(url));
1688
+ if (existsSync2(target)) {
1689
+ log(`\u2022 ${path14.basename(target)} already present at ${target} \u2014 skipped`);
1690
+ return;
1691
+ }
1692
+ if (opts.dryRun) {
1693
+ log(`would clone ${url} \u2192 ${target}`);
1694
+ return;
1695
+ }
1696
+ const res = await runGit(["clone", url, target]);
1697
+ if (res.exitCode !== 0) {
1698
+ error(`clone failed for ${url}: ${(res.stderr || res.stdout).trim()}`);
1699
+ process.exitCode = 1;
1700
+ return;
1701
+ }
1702
+ log(`\u2713 cloned ${url} \u2192 ${target}`);
1703
+ }
1704
+
1705
+ // src/commands/config.ts
1706
+ import path15 from "path";
1648
1707
  var CONFIG_PATH2 = ".haus-workflow/config.json";
1649
1708
  var HOOK_ALIASES = {
1650
1709
  "hook.context": "context"
@@ -1657,7 +1716,7 @@ async function runConfig(key, action) {
1657
1716
  );
1658
1717
  }
1659
1718
  const root = process.cwd();
1660
- const configPath = path14.join(root, CONFIG_PATH2);
1719
+ const configPath = path15.join(root, CONFIG_PATH2);
1661
1720
  const existing = await readJson(configPath);
1662
1721
  const cfg = existing ?? structuredClone(DEFAULT_HOOKS_CONFIG);
1663
1722
  cfg.hooks ??= {};
@@ -2029,7 +2088,7 @@ function selectRules(recommended, task, taskIntents) {
2029
2088
 
2030
2089
  // src/scanner/scan-project.ts
2031
2090
  import { readFile as readFile2 } from "fs/promises";
2032
- import path18 from "path";
2091
+ import path19 from "path";
2033
2092
 
2034
2093
  // src/utils/audit-checks.ts
2035
2094
  function isRecord(v) {
@@ -2056,7 +2115,7 @@ function compareVersions(a, b) {
2056
2115
  }
2057
2116
 
2058
2117
  // src/scanner/detect-package-manager.ts
2059
- import path15 from "path";
2118
+ import path16 from "path";
2060
2119
  import fs13 from "fs-extra";
2061
2120
  function detectPackageManager(root, packageManagerField) {
2062
2121
  const field = String(packageManagerField ?? "").trim();
@@ -2075,9 +2134,9 @@ function detectPackageManager(root, packageManagerField) {
2075
2134
  if (satisfiesVersion(version, ">=9")) return "npm";
2076
2135
  return "unknown";
2077
2136
  }
2078
- if (fs13.existsSync(path15.join(root, "yarn.lock"))) return "yarn";
2079
- if (fs13.existsSync(path15.join(root, "pnpm-lock.yaml"))) return "pnpm";
2080
- if (fs13.existsSync(path15.join(root, "package-lock.json"))) return "npm";
2137
+ if (fs13.existsSync(path16.join(root, "yarn.lock"))) return "yarn";
2138
+ if (fs13.existsSync(path16.join(root, "pnpm-lock.yaml"))) return "pnpm";
2139
+ if (fs13.existsSync(path16.join(root, "package-lock.json"))) return "npm";
2081
2140
  return "unknown";
2082
2141
  }
2083
2142
 
@@ -2250,7 +2309,7 @@ function runDetection(ctx, rules = STACK_RULES) {
2250
2309
  }
2251
2310
 
2252
2311
  // src/scanner/detection.ts
2253
- import path16 from "path";
2312
+ import path17 from "path";
2254
2313
  var UNSUPPORTED_MARKERS = {
2255
2314
  "requirements.txt": "python",
2256
2315
  "pyproject.toml": "python",
@@ -2304,14 +2363,14 @@ function finalizeRoles(registryRoles, deps, files) {
2304
2363
  function collectUnsupportedSignals(files) {
2305
2364
  return [
2306
2365
  ...new Set(
2307
- files.map((f) => UNSUPPORTED_MARKERS[path16.basename(f)]).filter((s) => Boolean(s))
2366
+ files.map((f) => UNSUPPORTED_MARKERS[path17.basename(f)]).filter((s) => Boolean(s))
2308
2367
  )
2309
2368
  ].sort();
2310
2369
  }
2311
2370
 
2312
2371
  // src/scanner/render.ts
2313
2372
  import { readFile } from "fs/promises";
2314
- import path17 from "path";
2373
+ import path18 from "path";
2315
2374
 
2316
2375
  // src/scanner/role-labels.ts
2317
2376
  var ROLE_LABELS = {
@@ -2373,7 +2432,7 @@ async function buildContentBlob(root, files) {
2373
2432
  const slice = candidates.slice(0, 300);
2374
2433
  const parts = await mapWithConcurrency(slice, async (rel) => {
2375
2434
  try {
2376
- return await readFile(path17.join(root, rel), "utf8");
2435
+ return await readFile(path18.join(root, rel), "utf8");
2377
2436
  } catch {
2378
2437
  return "";
2379
2438
  }
@@ -2471,8 +2530,8 @@ var SAFE_FILES = [
2471
2530
  "Gemfile"
2472
2531
  ];
2473
2532
  async function scanProject(root, mode = "fast") {
2474
- const pkg = await readJson(path18.join(root, "package.json"));
2475
- const composer = await readJson(path18.join(root, "composer.json"));
2533
+ const pkg = await readJson(path19.join(root, "package.json"));
2534
+ const composer = await readJson(path19.join(root, "composer.json"));
2476
2535
  const files = await listFiles(root, SAFE_FILES);
2477
2536
  const safeFiles = files.filter((f) => !blocked(f));
2478
2537
  const deps = dependencySet(pkg, composer);
@@ -2506,7 +2565,7 @@ async function scanProject(root, mode = "fast") {
2506
2565
  mode,
2507
2566
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2508
2567
  root,
2509
- repoName: String(pkg?.name ?? path18.basename(root)),
2568
+ repoName: String(pkg?.name ?? path19.basename(root)),
2510
2569
  packageManager,
2511
2570
  repoRoles: roles,
2512
2571
  detectedStacks: stacks,
@@ -2524,7 +2583,7 @@ async function scanProject(root, mode = "fast") {
2524
2583
  const scanHashes = Object.fromEntries(
2525
2584
  await mapWithConcurrency(
2526
2585
  safeFiles,
2527
- async (f) => [f, hashText(await readFile2(path18.join(root, f), "utf8"))]
2586
+ async (f) => [f, hashText(await readFile2(path19.join(root, f), "utf8"))]
2528
2587
  )
2529
2588
  );
2530
2589
  const repoSummary = renderSummary(context);
@@ -2609,7 +2668,7 @@ async function runContext(options) {
2609
2668
  }
2610
2669
 
2611
2670
  // src/commands/doctor.ts
2612
- import path19 from "path";
2671
+ import path20 from "path";
2613
2672
  import fs14 from "fs-extra";
2614
2673
 
2615
2674
  // src/update/npm-version.ts
@@ -2690,7 +2749,7 @@ async function runDoctor(options) {
2690
2749
  const enabled = await isHookEnabled(root, key);
2691
2750
  ok(`- HOOK ${key}: ${enabled ? "enabled" : "disabled (default)"}`);
2692
2751
  }
2693
- const rootClaudeMdPath = path19.join(root, "CLAUDE.md");
2752
+ const rootClaudeMdPath = path20.join(root, "CLAUDE.md");
2694
2753
  const rootClaudeMdContent = await readText(rootClaudeMdPath);
2695
2754
  if (!rootClaudeMdContent) {
2696
2755
  flag(
@@ -2743,8 +2802,8 @@ async function runDoctor(options) {
2743
2802
  ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
2744
2803
  } else {
2745
2804
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
2746
- const cachePath = path19.join(getCacheDir(), "templates/agentic-workflow-standard.md");
2747
- const bundledPath = path19.join(
2805
+ const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
2806
+ const bundledPath = path20.join(
2748
2807
  packageRoot(),
2749
2808
  "library",
2750
2809
  "global",
@@ -2809,7 +2868,7 @@ async function runDoctor(options) {
2809
2868
  ok(`- CATALOG CACHE: OK (${cacheAgeDays}d old)`);
2810
2869
  }
2811
2870
  }
2812
- const pkgJson = await readJson(path19.join(packageRoot(), "package.json"));
2871
+ const pkgJson = await readJson(path20.join(packageRoot(), "package.json"));
2813
2872
  const currentVersion = pkgJson?.version ?? "0.0.0";
2814
2873
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
2815
2874
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -2952,7 +3011,7 @@ async function runGuard(kind, _options) {
2952
3011
  }
2953
3012
 
2954
3013
  // src/commands/init.ts
2955
- import path20 from "path";
3014
+ import path21 from "path";
2956
3015
  import fs15 from "fs-extra";
2957
3016
 
2958
3017
  // src/utils/prompts.ts
@@ -2973,31 +3032,6 @@ async function confirm(question) {
2973
3032
  return answer === "y" || answer === "yes";
2974
3033
  }
2975
3034
 
2976
- // src/utils/exec.ts
2977
- import { execa } from "execa";
2978
- async function runCommand(command, args = [], options = {}) {
2979
- try {
2980
- const result = await execa(command, args, {
2981
- reject: false,
2982
- // non-zero exits are returned, not thrown
2983
- ...options
2984
- });
2985
- return {
2986
- command,
2987
- args,
2988
- stdout: String(result.stdout ?? ""),
2989
- stderr: String(result.stderr ?? ""),
2990
- exitCode: result.exitCode ?? 0
2991
- };
2992
- } catch (error2) {
2993
- const message = error2 instanceof Error ? error2.message : String(error2);
2994
- throw new Error(`Failed to run command: ${command} ${args.join(" ")} (${message})`);
2995
- }
2996
- }
2997
- async function runGit(args, options = {}) {
2998
- return runCommand("git", args, options);
2999
- }
3000
-
3001
3035
  // src/recommender/git-signal.ts
3002
3036
  async function readChangedFiles(root) {
3003
3037
  if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
@@ -3340,7 +3374,7 @@ async function runSetupProject(options) {
3340
3374
  // src/commands/init.ts
3341
3375
  async function runInit(options) {
3342
3376
  const root = process.cwd();
3343
- const hausDir = path20.join(root, ".haus-workflow");
3377
+ const hausDir = path21.join(root, ".haus-workflow");
3344
3378
  const alreadyInit = await fs15.pathExists(hausDir);
3345
3379
  if (alreadyInit) {
3346
3380
  log("Haus AI already initialized in this project.");
@@ -3353,7 +3387,7 @@ async function runInit(options) {
3353
3387
 
3354
3388
  // src/install/apply.ts
3355
3389
  import crypto2 from "crypto";
3356
- import path21 from "path";
3390
+ import path22 from "path";
3357
3391
  import fs16 from "fs-extra";
3358
3392
 
3359
3393
  // src/install/header.ts
@@ -3442,7 +3476,7 @@ function hashContent(content2) {
3442
3476
  }
3443
3477
  function sourceVersion() {
3444
3478
  try {
3445
- const pkgPath = path21.join(packageRoot(), "package.json");
3479
+ const pkgPath = path22.join(packageRoot(), "package.json");
3446
3480
  const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
3447
3481
  return `${pkg.name ?? "haus"}@${pkg.version ?? "0.0.0"}`;
3448
3482
  } catch {
@@ -3450,32 +3484,32 @@ function sourceVersion() {
3450
3484
  }
3451
3485
  }
3452
3486
  function globalSrcDir() {
3453
- return path21.join(packageRoot(), "library", "global");
3487
+ return path22.join(packageRoot(), "library", "global");
3454
3488
  }
3455
3489
  function collectSourceFiles(srcDir, claudeDir) {
3456
3490
  const entries = [];
3457
- const skillsDir = path21.join(srcDir, "skills");
3491
+ const skillsDir = path22.join(srcDir, "skills");
3458
3492
  if (fs16.pathExistsSync(skillsDir)) {
3459
3493
  for (const skillName of fs16.readdirSync(skillsDir)) {
3460
- const skillFile = path21.join(skillsDir, skillName, "SKILL.md");
3494
+ const skillFile = path22.join(skillsDir, skillName, "SKILL.md");
3461
3495
  if (fs16.pathExistsSync(skillFile)) {
3462
3496
  entries.push({
3463
3497
  stableId: `skill.${skillName}`,
3464
- srcRelPath: path21.join("library", "global", "skills", skillName, "SKILL.md"),
3465
- destPath: path21.join(claudeDir, "skills", skillName, "SKILL.md")
3498
+ srcRelPath: path22.join("library", "global", "skills", skillName, "SKILL.md"),
3499
+ destPath: path22.join(claudeDir, "skills", skillName, "SKILL.md")
3466
3500
  });
3467
3501
  }
3468
3502
  }
3469
3503
  }
3470
- const commandsDir = path21.join(srcDir, "commands");
3504
+ const commandsDir = path22.join(srcDir, "commands");
3471
3505
  if (fs16.pathExistsSync(commandsDir)) {
3472
3506
  for (const fileName of fs16.readdirSync(commandsDir)) {
3473
3507
  if (!fileName.endsWith(".md")) continue;
3474
3508
  const commandName = fileName.slice(0, -".md".length);
3475
3509
  entries.push({
3476
3510
  stableId: `command.${commandName}`,
3477
- srcRelPath: path21.join("library", "global", "commands", fileName),
3478
- destPath: path21.join(claudeDir, "commands", fileName)
3511
+ srcRelPath: path22.join("library", "global", "commands", fileName),
3512
+ destPath: path22.join(claudeDir, "commands", fileName)
3479
3513
  });
3480
3514
  }
3481
3515
  }
@@ -3499,7 +3533,7 @@ async function applyInstall(options = {}) {
3499
3533
  };
3500
3534
  const manifestFiles = [];
3501
3535
  for (const entry of sourceFiles) {
3502
- const srcPath = path21.join(packageRoot(), entry.srcRelPath);
3536
+ const srcPath = path22.join(packageRoot(), entry.srcRelPath);
3503
3537
  const rawContent = await readText(srcPath);
3504
3538
  if (rawContent === void 0) {
3505
3539
  warn(`Source file not found: ${entry.srcRelPath}`);
@@ -3555,7 +3589,7 @@ async function applyInstall(options = {}) {
3555
3589
  schemaVersion: SCHEMA_VERSION2
3556
3590
  });
3557
3591
  }
3558
- const fragmentPath = path21.join(srcDir, "settings-fragments", "hooks.json");
3592
+ const fragmentPath = path22.join(srcDir, "settings-fragments", "hooks.json");
3559
3593
  const fragments = await loadHooksFragment(fragmentPath);
3560
3594
  const settings = await readSettings();
3561
3595
  const { settings: hookSettings, addedIds } = mergeHooks(settings, fragments);
@@ -3699,7 +3733,7 @@ async function runScan(options) {
3699
3733
  }
3700
3734
 
3701
3735
  // src/commands/undo.ts
3702
- import path22 from "path";
3736
+ import path23 from "path";
3703
3737
  import fs17 from "fs-extra";
3704
3738
 
3705
3739
  // src/claude/managed-paths.ts
@@ -3726,7 +3760,7 @@ async function collectManagedPaths(root) {
3726
3760
  const lock = await readJson(hausPath(root, "haus.lock.json"));
3727
3761
  for (const row of lock ?? []) {
3728
3762
  for (const rel of row.paths ?? []) {
3729
- paths.add(path22.resolve(root, rel));
3763
+ paths.add(path23.resolve(root, rel));
3730
3764
  }
3731
3765
  }
3732
3766
  const existing = [];
@@ -3742,7 +3776,7 @@ async function settingsHasHausContent(root) {
3742
3776
  return settings._haus != null;
3743
3777
  }
3744
3778
  async function claudeMdHasHausBlock(root) {
3745
- const filePath = path22.join(root, "CLAUDE.md");
3779
+ const filePath = path23.join(root, "CLAUDE.md");
3746
3780
  if (!await fs17.pathExists(filePath)) return false;
3747
3781
  const text = await fs17.readFile(filePath, "utf8");
3748
3782
  return text.includes(BLOCK_BEGIN);
@@ -3755,15 +3789,15 @@ async function stripProjectSettings(root) {
3755
3789
  const hasContent = Object.keys(settings).length > 0;
3756
3790
  if (hasContent) {
3757
3791
  await writeProjectSettings(root, settings);
3758
- log(`Stripped haus rules from ${path22.relative(root, settingsPath)} (user settings preserved).`);
3792
+ log(`Stripped haus rules from ${path23.relative(root, settingsPath)} (user settings preserved).`);
3759
3793
  return true;
3760
3794
  }
3761
3795
  await fs17.remove(settingsPath);
3762
- log(`Removed ${path22.relative(root, settingsPath)} (no user-owned settings remained).`);
3796
+ log(`Removed ${path23.relative(root, settingsPath)} (no user-owned settings remained).`);
3763
3797
  return true;
3764
3798
  }
3765
3799
  async function stripRootClaudeMd(root) {
3766
- const filePath = path22.join(root, "CLAUDE.md");
3800
+ const filePath = path23.join(root, "CLAUDE.md");
3767
3801
  if (!await fs17.pathExists(filePath)) return false;
3768
3802
  const prev = await fs17.readFile(filePath, "utf8");
3769
3803
  if (!prev.includes(BLOCK_BEGIN)) return false;
@@ -3791,7 +3825,7 @@ async function runUndo(options) {
3791
3825
  log("Nothing to remove: no haus-managed files found in this directory.");
3792
3826
  return;
3793
3827
  }
3794
- const relTargets = managed.map((p) => path22.relative(root, p));
3828
+ const relTargets = managed.map((p) => path23.relative(root, p));
3795
3829
  const summaryParts = [...relTargets];
3796
3830
  if (stripSettings) summaryParts.push(".claude/settings.json (haus rules only)");
3797
3831
  if (stripClaudeMd) summaryParts.push("CLAUDE.md (haus import block only)");
@@ -3809,7 +3843,7 @@ User-owned .claude/ files will be preserved.`
3809
3843
  for (const abs of managed) {
3810
3844
  if (!await fs17.pathExists(abs)) continue;
3811
3845
  await fs17.remove(abs);
3812
- log(`Removed ${path22.relative(root, abs)}`);
3846
+ log(`Removed ${path23.relative(root, abs)}`);
3813
3847
  }
3814
3848
  if (stripSettings) await stripProjectSettings(root);
3815
3849
  if (stripClaudeMd) await stripRootClaudeMd(root);
@@ -3820,7 +3854,7 @@ User-owned .claude/ files will be preserved.`
3820
3854
 
3821
3855
  // src/install/uninstall.ts
3822
3856
  import crypto3 from "crypto";
3823
- import path23 from "path";
3857
+ import path24 from "path";
3824
3858
  import fs18 from "fs-extra";
3825
3859
  async function runUninstall(options = {}) {
3826
3860
  const { force = false } = options;
@@ -3850,14 +3884,14 @@ async function runUninstall(options = {}) {
3850
3884
  continue;
3851
3885
  }
3852
3886
  await fs18.remove(entry.destPath);
3853
- await pruneEmptyDir(path23.dirname(entry.destPath));
3887
+ await pruneEmptyDir(path24.dirname(entry.destPath));
3854
3888
  result.deleted.push(entry.destPath);
3855
3889
  }
3856
3890
  const settings = await readSettings();
3857
3891
  const stripped = stripHausHooks(stripHausAllow(stripHausDeny(settings)));
3858
3892
  await writeSettings(stripped);
3859
3893
  result.hooksStripped = true;
3860
- const hausDir = path23.join(globalClaudeDir(), "haus");
3894
+ const hausDir = path24.join(globalClaudeDir(), "haus");
3861
3895
  const manifestPath2 = hausManifestPath();
3862
3896
  if (fs18.pathExistsSync(manifestPath2)) {
3863
3897
  await fs18.remove(manifestPath2);
@@ -3895,7 +3929,7 @@ async function runUninstallCommand(options) {
3895
3929
  }
3896
3930
 
3897
3931
  // src/commands/update.ts
3898
- import path25 from "path";
3932
+ import path26 from "path";
3899
3933
 
3900
3934
  // src/update/diff-generated-files.ts
3901
3935
  function diffGeneratedFiles() {
@@ -3922,7 +3956,7 @@ function summarizeLockDiff(before, after) {
3922
3956
 
3923
3957
  // src/update/lockfile.ts
3924
3958
  import { mkdir, readFile as readFile3, copyFile } from "fs/promises";
3925
- import path24 from "path";
3959
+ import path25 from "path";
3926
3960
  async function checkLock(root) {
3927
3961
  const lock = await readJson(hausPath(root, "haus.lock.json")) ?? [];
3928
3962
  const hasValidVersions = lock.every(
@@ -3953,7 +3987,7 @@ async function applyLock(root) {
3953
3987
  try {
3954
3988
  const backupDir = hausPath(root, "backups");
3955
3989
  await mkdir(backupDir, { recursive: true });
3956
- await copyFile(lockPath, path24.join(backupDir, `haus.lock.${Date.now()}.json`));
3990
+ await copyFile(lockPath, path25.join(backupDir, `haus.lock.${Date.now()}.json`));
3957
3991
  } catch {
3958
3992
  }
3959
3993
  const enriched = await Promise.all(
@@ -3975,7 +4009,7 @@ function diffLock(before, after) {
3975
4009
  }
3976
4010
  async function hasLocalOverrides(root) {
3977
4011
  try {
3978
- await readFile3(path24.join(root, ".claude", "settings.json"), "utf8");
4012
+ await readFile3(path25.join(root, ".claude", "settings.json"), "utf8");
3979
4013
  return true;
3980
4014
  } catch {
3981
4015
  return false;
@@ -3987,7 +4021,7 @@ var NPM_PACKAGE_NAME2 = "@haus-tech/haus-workflow";
3987
4021
  async function runUpdate(options) {
3988
4022
  const root = process.cwd();
3989
4023
  if (options.check) {
3990
- const pkgJson2 = await readJson(path25.join(packageRoot(), "package.json"));
4024
+ const pkgJson2 = await readJson(path26.join(packageRoot(), "package.json"));
3991
4025
  const currentVersion2 = pkgJson2?.version ?? "0.0.0";
3992
4026
  const [status, npmVersion, latestCatalogTag, globalInstallDrift] = await Promise.all([
3993
4027
  checkLock(root),
@@ -4017,7 +4051,7 @@ async function runUpdate(options) {
4017
4051
  if (status.driftCount > 0) process.exitCode = 1;
4018
4052
  return;
4019
4053
  }
4020
- const pkgJson = await readJson(path25.join(packageRoot(), "package.json"));
4054
+ const pkgJson = await readJson(path26.join(packageRoot(), "package.json"));
4021
4055
  const currentVersion = pkgJson?.version ?? "0.0.0";
4022
4056
  const npmStatus = await fetchNpmVersionStatus(currentVersion);
4023
4057
  if (npmStatus.updateAvailable && npmStatus.latest !== null) {
@@ -4095,7 +4129,7 @@ async function detectGlobalInstallDrift() {
4095
4129
 
4096
4130
  // src/commands/validate-catalog.ts
4097
4131
  import fs19 from "fs";
4098
- import path26 from "path";
4132
+ import path27 from "path";
4099
4133
 
4100
4134
  // src/catalog/forbidden-content.ts
4101
4135
  var PROSE_FORBIDDEN_TAGS = FORBIDDEN_TAGS.filter((t) => t.toLowerCase() !== "go");
@@ -4227,17 +4261,17 @@ function auditShippedFiles(manifestDir, items) {
4227
4261
  const failures = [];
4228
4262
  for (const item of items) {
4229
4263
  if (!item.path) continue;
4230
- const absPath = path26.join(manifestDir, item.path);
4264
+ const absPath = path27.join(manifestDir, item.path);
4231
4265
  if (item.type === "skill") {
4232
- const skillMd = path26.join(absPath, "SKILL.md");
4266
+ const skillMd = path27.join(absPath, "SKILL.md");
4233
4267
  if (!fs19.existsSync(skillMd)) {
4234
- failures.push(`${item.id}: missing ${path26.relative(manifestDir, skillMd)}`);
4268
+ failures.push(`${item.id}: missing ${path27.relative(manifestDir, skillMd)}`);
4235
4269
  continue;
4236
4270
  }
4237
4271
  const text = fs19.readFileSync(skillMd, "utf8");
4238
4272
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: SKILL.md`));
4239
4273
  failures.push(
4240
- ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, skillMd)}`)
4274
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, skillMd)}`)
4241
4275
  );
4242
4276
  } else if (item.type === "agent") {
4243
4277
  if (!fs19.existsSync(absPath)) {
@@ -4255,7 +4289,7 @@ function auditShippedFiles(manifestDir, items) {
4255
4289
  failures.push(`${item.id}: agent file contains disallowed phrase "${phrase}"`);
4256
4290
  }
4257
4291
  failures.push(
4258
- ...auditForbiddenTagsInText(text, `${item.id}: ${path26.relative(manifestDir, absPath)}`)
4292
+ ...auditForbiddenTagsInText(text, `${item.id}: ${path27.relative(manifestDir, absPath)}`)
4259
4293
  );
4260
4294
  } else if (item.type === "template") {
4261
4295
  if (!fs19.existsSync(absPath)) {
@@ -4269,7 +4303,7 @@ function auditShippedFiles(manifestDir, items) {
4269
4303
  continue;
4270
4304
  }
4271
4305
  const text = fs19.readFileSync(absPath, "utf8");
4272
- const rel = path26.relative(manifestDir, absPath);
4306
+ const rel = path27.relative(manifestDir, absPath);
4273
4307
  failures.push(...checkRequiredFrontmatter(text, `${item.id}: ${rel}`));
4274
4308
  failures.push(...auditTemplateContent(manifestDir, absPath, item.id));
4275
4309
  }
@@ -4277,7 +4311,7 @@ function auditShippedFiles(manifestDir, items) {
4277
4311
  return failures;
4278
4312
  }
4279
4313
  function auditTemplateContent(manifestDir, absPath, itemId) {
4280
- const rel = path26.relative(manifestDir, absPath);
4314
+ const rel = path27.relative(manifestDir, absPath);
4281
4315
  const text = fs19.readFileSync(absPath, "utf8");
4282
4316
  const failures = [];
4283
4317
  const lines = text.split(/\r?\n/);
@@ -4300,11 +4334,11 @@ function auditMarkdownContent(manifestDir) {
4300
4334
  const failures = [];
4301
4335
  const dirs = ["skills", "agents", "templates", "commands"];
4302
4336
  for (const dir of dirs) {
4303
- const abs = path26.join(manifestDir, dir);
4337
+ const abs = path27.join(manifestDir, dir);
4304
4338
  if (!fs19.existsSync(abs)) continue;
4305
4339
  walkMd(abs, (file) => {
4306
4340
  const text = fs19.readFileSync(file, "utf8");
4307
- const rel = path26.relative(manifestDir, file);
4341
+ const rel = path27.relative(manifestDir, file);
4308
4342
  const lines = text.split(/\r?\n/);
4309
4343
  for (let i = 0; i < lines.length; i++) {
4310
4344
  const line2 = lines[i] ?? "";
@@ -4324,7 +4358,7 @@ function auditMarkdownContent(manifestDir) {
4324
4358
  }
4325
4359
  function walkMd(dir, fn) {
4326
4360
  for (const entry of fs19.readdirSync(dir, { withFileTypes: true })) {
4327
- const full = path26.join(dir, entry.name);
4361
+ const full = path27.join(dir, entry.name);
4328
4362
  if (entry.isDirectory()) walkMd(full, fn);
4329
4363
  else if (entry.name.endsWith(".md")) fn(full);
4330
4364
  }
@@ -4335,8 +4369,8 @@ async function runValidateCatalog(manifestPath2) {
4335
4369
  process.exitCode = 1;
4336
4370
  return;
4337
4371
  }
4338
- const abs = path26.resolve(process.cwd(), manifestPath2);
4339
- const manifestDir = path26.dirname(abs);
4372
+ const abs = path27.resolve(process.cwd(), manifestPath2);
4373
+ const manifestDir = path27.dirname(abs);
4340
4374
  const data = await readJson(abs);
4341
4375
  if (!data?.items) {
4342
4376
  error(`Could not read catalog manifest at ${abs}`);
@@ -4365,8 +4399,8 @@ async function runValidateCatalog(manifestPath2) {
4365
4399
  }
4366
4400
 
4367
4401
  // src/commands/workspace.ts
4368
- import { existsSync as existsSync4, statSync as statSync2 } from "fs";
4369
- import path33 from "path";
4402
+ import { existsSync as existsSync5, statSync as statSync2 } from "fs";
4403
+ import path34 from "path";
4370
4404
 
4371
4405
  // src/commands/workspace/aggregate.ts
4372
4406
  async function writeWorkspaceArtifacts(workspaceRoot, repos, relationships = []) {
@@ -4420,7 +4454,7 @@ ${summaries.map(
4420
4454
  }
4421
4455
 
4422
4456
  // src/commands/workspace/config.ts
4423
- import path27 from "path";
4457
+ import path28 from "path";
4424
4458
  import YAML from "yaml";
4425
4459
  var WORKSPACE_FILE = "haus.workspace.yaml";
4426
4460
  function parseWorkspaceConfig(text) {
@@ -4443,11 +4477,11 @@ function parseWorkspaceConfig(text) {
4443
4477
  };
4444
4478
  }
4445
4479
  async function readWorkspaceConfig(workspaceRoot) {
4446
- return parseWorkspaceConfig(await readText(path27.join(workspaceRoot, WORKSPACE_FILE)));
4480
+ return parseWorkspaceConfig(await readText(path28.join(workspaceRoot, WORKSPACE_FILE)));
4447
4481
  }
4448
4482
 
4449
4483
  // src/commands/workspace/discover.ts
4450
- import path28 from "path";
4484
+ import path29 from "path";
4451
4485
  import fg3 from "fast-glob";
4452
4486
  import YAML2 from "yaml";
4453
4487
  var DEFAULT_MAX_DEPTH = 3;
@@ -4475,8 +4509,8 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4475
4509
  const gitDirs = /* @__PURE__ */ new Set();
4476
4510
  const manifestDirs = /* @__PURE__ */ new Set();
4477
4511
  for (const match of matches) {
4478
- const base = path28.posix.basename(match);
4479
- const dir = path28.posix.dirname(match);
4512
+ const base = path29.posix.basename(match);
4513
+ const dir = path29.posix.dirname(match);
4480
4514
  const owner = dir === "." ? "." : dir;
4481
4515
  if (base === ".git") gitDirs.add(owner);
4482
4516
  else manifestDirs.add(owner);
@@ -4492,9 +4526,9 @@ async function discoverRepos(workspaceRoot, maxDepth = DEFAULT_MAX_DEPTH) {
4492
4526
  }
4493
4527
  repoRoots.sort((a, b) => a.localeCompare(b));
4494
4528
  return mapWithConcurrency(repoRoots, async (relDir) => {
4495
- const absDir = path28.resolve(workspaceRoot, relDir);
4496
- const pkg = await readJson(path28.join(absDir, "package.json"));
4497
- const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path28.basename(relDir === "." ? workspaceRoot : absDir);
4529
+ const absDir = path29.resolve(workspaceRoot, relDir);
4530
+ const pkg = await readJson(path29.join(absDir, "package.json"));
4531
+ const name = typeof pkg?.name === "string" && pkg.name.length > 0 ? pkg.name : path29.basename(relDir === "." ? workspaceRoot : absDir);
4498
4532
  let role = "auto";
4499
4533
  try {
4500
4534
  const scan = await scanProject(absDir, "fast");
@@ -4537,7 +4571,7 @@ function renderWorkspaceYaml(config2) {
4537
4571
  });
4538
4572
  }
4539
4573
  async function runDiscover(workspaceRoot, opts = {}) {
4540
- const yamlPath = path28.join(workspaceRoot, "haus.workspace.yaml");
4574
+ const yamlPath = path29.join(workspaceRoot, "haus.workspace.yaml");
4541
4575
  const existingText = await readText(yamlPath);
4542
4576
  const existing = parseWorkspaceConfig(existingText);
4543
4577
  if (existingText && !existing) {
@@ -4570,19 +4604,19 @@ async function runDiscover(workspaceRoot, opts = {}) {
4570
4604
  }
4571
4605
 
4572
4606
  // src/commands/workspace/doctor.ts
4573
- import { existsSync as existsSync2 } from "fs";
4574
- import path30 from "path";
4607
+ import { existsSync as existsSync3 } from "fs";
4608
+ import path31 from "path";
4575
4609
 
4576
4610
  // src/commands/workspace/manifest.ts
4577
4611
  import { readFileSync as readFileSync3 } from "fs";
4578
- import path29 from "path";
4612
+ import path30 from "path";
4579
4613
  var MANIFEST_FILE = "workspace.manifest.json";
4580
4614
  function manifestPath(workspaceRoot) {
4581
4615
  return hausPath(workspaceRoot, MANIFEST_FILE);
4582
4616
  }
4583
4617
  function hausVersion() {
4584
4618
  try {
4585
- const pkg = JSON.parse(readFileSync3(path29.join(packageRoot(), "package.json"), "utf8"));
4619
+ const pkg = JSON.parse(readFileSync3(path30.join(packageRoot(), "package.json"), "utf8"));
4586
4620
  return pkg.version ?? "0.0.0";
4587
4621
  } catch {
4588
4622
  return "0.0.0";
@@ -4648,7 +4682,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4648
4682
  }
4649
4683
  const manifestByName = new Map(manifest.repos.map((r) => [r.name, r]));
4650
4684
  for (const repo of config2.repos) {
4651
- const repoRoot = path30.resolve(workspaceRoot, repo.path);
4685
+ const repoRoot = path31.resolve(workspaceRoot, repo.path);
4652
4686
  const entry = manifestByName.get(repo.name);
4653
4687
  if (!entry) {
4654
4688
  flag({
@@ -4673,7 +4707,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4673
4707
  detail: `Set up at haus ${entry.hausVersionAtSetup}, current is ${currentVersion} \u2014 re-run setup.`
4674
4708
  });
4675
4709
  }
4676
- if (!existsSync2(claudePath(repoRoot))) {
4710
+ if (!existsSync3(claudePath(repoRoot))) {
4677
4711
  flag({
4678
4712
  repo: repo.name,
4679
4713
  kind: "missing-claude",
@@ -4681,7 +4715,7 @@ async function runWorkspaceDoctor(workspaceRoot, opts = {}) {
4681
4715
  });
4682
4716
  }
4683
4717
  const lock = await checkLock(repoRoot);
4684
- if (!existsSync2(hausPath(repoRoot, "haus.lock.json"))) {
4718
+ if (!existsSync3(hausPath(repoRoot, "haus.lock.json"))) {
4685
4719
  flag({
4686
4720
  repo: repo.name,
4687
4721
  kind: "missing-lock",
@@ -4721,11 +4755,11 @@ function emit(args) {
4721
4755
  }
4722
4756
 
4723
4757
  // src/commands/workspace/setup.ts
4724
- import { existsSync as existsSync3, statSync } from "fs";
4725
- import path32 from "path";
4758
+ import { existsSync as existsSync4, statSync } from "fs";
4759
+ import path33 from "path";
4726
4760
 
4727
4761
  // src/claude/write-workspace-claude-md.ts
4728
- import path31 from "path";
4762
+ import path32 from "path";
4729
4763
  import fs20 from "fs-extra";
4730
4764
  function buildWorkspaceImportBlock(client, members) {
4731
4765
  const memberLines = members.map((m) => `- ${m.name} (${m.path})`);
@@ -4744,7 +4778,7 @@ ${BLOCK_END}`;
4744
4778
  async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4745
4779
  const block = buildWorkspaceImportBlock(opts.client, opts.members);
4746
4780
  const dryRun = opts.dryRun ?? false;
4747
- const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path31.join(workspaceRoot, "CLAUDE.md");
4781
+ const filePath = opts.collision ? hausPath(workspaceRoot, "WORKSPACE.md") : path32.join(workspaceRoot, "CLAUDE.md");
4748
4782
  const prev = await fs20.pathExists(filePath) ? await fs20.readFile(filePath, "utf8") : "";
4749
4783
  const next = opts.collision ? `${block}
4750
4784
  ` : injectHausBlock(prev, block);
@@ -4770,21 +4804,21 @@ async function writeWorkspaceClaudeMd(workspaceRoot, opts) {
4770
4804
 
4771
4805
  // src/commands/workspace/setup.ts
4772
4806
  function resolveWorkspaceRoot(start = process.cwd()) {
4773
- let dir = path32.resolve(start);
4807
+ let dir = path33.resolve(start);
4774
4808
  for (; ; ) {
4775
- if (existsSync3(path32.join(dir, WORKSPACE_FILE))) return dir;
4776
- const parent = path32.dirname(dir);
4777
- if (parent === dir) return path32.resolve(start);
4809
+ if (existsSync4(path33.join(dir, WORKSPACE_FILE))) return dir;
4810
+ const parent = path33.dirname(dir);
4811
+ if (parent === dir) return path33.resolve(start);
4778
4812
  dir = parent;
4779
4813
  }
4780
4814
  }
4781
4815
  function isRootRepo(workspaceRoot, repoPath) {
4782
- return path32.resolve(workspaceRoot, repoPath) === path32.resolve(workspaceRoot);
4816
+ return path33.resolve(workspaceRoot, repoPath) === path33.resolve(workspaceRoot);
4783
4817
  }
4784
4818
  async function runWorkspaceSetup(workspaceRoot, options = {}) {
4785
4819
  const mode = options.mode ?? "fast";
4786
4820
  const apply = options.write ?? false;
4787
- const configText = await readText(path32.join(workspaceRoot, WORKSPACE_FILE));
4821
+ const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
4788
4822
  if (!configText) {
4789
4823
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
4790
4824
  process.exitCode = 1;
@@ -4801,11 +4835,11 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
4801
4835
  const statuses = [];
4802
4836
  const aggregateInputs = [];
4803
4837
  for (const repo of repos) {
4804
- const repoRoot = path32.resolve(workspaceRoot, repo.path);
4838
+ const repoRoot = path33.resolve(workspaceRoot, repo.path);
4805
4839
  log(`
4806
4840
  \u2192 ${repo.name} (${repo.path})`);
4807
4841
  try {
4808
- if (!existsSync3(repoRoot) || !statSync(repoRoot).isDirectory()) {
4842
+ if (!existsSync4(repoRoot) || !statSync(repoRoot).isDirectory()) {
4809
4843
  throw new Error(`Repo path is not a directory: ${repo.path}`);
4810
4844
  }
4811
4845
  const res = await runSetupCore(repoRoot, {
@@ -4859,7 +4893,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
4859
4893
  if (apply && !options.dryRun) {
4860
4894
  const statusByName = new Map(statuses.map((s) => [s.name, s]));
4861
4895
  const prior = await readManifest2(workspaceRoot);
4862
- if (!prior && existsSync3(manifestPath(workspaceRoot))) {
4896
+ if (!prior && existsSync4(manifestPath(workspaceRoot))) {
4863
4897
  warn(
4864
4898
  "Existing workspace.manifest.json is unreadable \u2014 prior per-repo state will not be carried forward."
4865
4899
  );
@@ -4870,7 +4904,7 @@ async function runWorkspaceSetup(workspaceRoot, options = {}) {
4870
4904
  const status = statusByName.get(repo.name);
4871
4905
  const role = repo.role ?? status?.roles?.[0] ?? "auto";
4872
4906
  if (status?.status === "ok") {
4873
- const lock = await checkLock(path32.resolve(workspaceRoot, repo.path));
4907
+ const lock = await checkLock(path33.resolve(workspaceRoot, repo.path));
4874
4908
  manifestRepos.push({
4875
4909
  name: repo.name,
4876
4910
  path: repo.path,
@@ -4950,7 +4984,7 @@ relationships: []
4950
4984
  log("Workspace initialized.");
4951
4985
  }
4952
4986
  async function scanWorkspace(workspaceRoot, opts) {
4953
- const configText = await readText(path33.join(workspaceRoot, WORKSPACE_FILE));
4987
+ const configText = await readText(path34.join(workspaceRoot, WORKSPACE_FILE));
4954
4988
  if (!configText) {
4955
4989
  error(`Missing ${WORKSPACE_FILE}. Run \`haus workspace discover\` or \`init\` first.`);
4956
4990
  process.exitCode = 1;
@@ -4971,8 +5005,8 @@ async function scanWorkspace(workspaceRoot, opts) {
4971
5005
  }
4972
5006
  const inputs = [];
4973
5007
  for (const repo of config2.repos) {
4974
- const repoRoot = path33.resolve(workspaceRoot, repo.path);
4975
- if (!existsSync4(repoRoot) || !statSync2(repoRoot).isDirectory()) {
5008
+ const repoRoot = path34.resolve(workspaceRoot, repo.path);
5009
+ if (!existsSync5(repoRoot) || !statSync2(repoRoot).isDirectory()) {
4976
5010
  throw new Error(`Repo path is not a directory: ${repo.path}`);
4977
5011
  }
4978
5012
  const result = await scanProject(repoRoot, "fast");
@@ -5022,7 +5056,7 @@ async function runWorkspace(action, options = {}) {
5022
5056
  // src/cli.ts
5023
5057
  function cliVersion() {
5024
5058
  try {
5025
- const pkgPath = path34.join(packageRoot(), "package.json");
5059
+ const pkgPath = path35.join(packageRoot(), "package.json");
5026
5060
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5027
5061
  return pkg.version ?? "0.0.0";
5028
5062
  } catch {
@@ -5032,7 +5066,7 @@ function cliVersion() {
5032
5066
  var program = new Command();
5033
5067
  function validateRuntimeNodeVersion() {
5034
5068
  try {
5035
- const pkgPath = path34.join(packageRoot(), "package.json");
5069
+ const pkgPath = path35.join(packageRoot(), "package.json");
5036
5070
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
5037
5071
  const requiredRange = pkg.engines?.node;
5038
5072
  if (requiredRange && !satisfiesVersion(process.version, requiredRange)) {
@@ -5061,6 +5095,9 @@ program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo)
5061
5095
  program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
5062
5096
  program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
5063
5097
  program.command("init").option("--json").action(runInit);
5098
+ program.command("clone").description("Clone a single git repository by URL").argument("<url>", "Git URL to clone").argument("[dir]", "Target directory (default: repo name derived from the URL)").option("--dry-run", "Print what would happen without cloning").action(
5099
+ (url, dir, opts) => runClone(url, { dir, dryRun: opts.dryRun })
5100
+ );
5064
5101
  program.command("refresh").action(runRefresh);
5065
5102
  program.command("catalog-audit").action(runCatalogAudit);
5066
5103
  program.command("validate-catalog").argument("[manifest]").action(runValidateCatalog);
@@ -0,0 +1,32 @@
1
+ Clone repositories for this project. Per-repo setup (install, Docker, `.env`) is a separate step that isn't wired yet — this command only gets repos onto disk.
2
+
3
+ Cloning a single repo is always `haus clone <url> [dir]`. This command picks _which_ repos to clone and runs that primitive for each. There are two modes, chosen by whether a name was given.
4
+
5
+ ## Mode A — a project name was given (`project:clone <name>`)
6
+
7
+ Find one repo by name on GitHub and clone it. Does **not** require a workspace or `repos.manifest.json`.
8
+
9
+ 1. Make sure GitHub CLI is ready: run `gh auth status`. If not authenticated, tell the user to run `gh auth login` and stop.
10
+ 2. Scope the search to repos the user owns or belongs to: get their login with `gh api user -q .login` and their orgs with `gh api user/orgs -q '.[].login'`.
11
+ 3. Search by name, passing one `--owner` per login/org from step 2:
12
+ `gh search repos "<name>" --match name --limit 10 --json fullName,description,url,isPrivate,pushedAt --owner <login> [--owner <org> …]`
13
+ If that returns nothing, retry **without** `--owner` (a broader, all-of-GitHub search) and tell the user you widened it.
14
+ 4. Decide from the results:
15
+ - **0 matches** — tell the user nothing matched `<name>`; offer to try a different name or broaden. Stop.
16
+ - **1 match** — show `fullName` + description and confirm "Clone this one?" before proceeding.
17
+ - **2+ matches** — use `AskUserQuestion` to let the user pick which repo (list each `fullName` with its description; private repos noted). Include a final option like "None of these — search again / broaden" so they can refine.
18
+ 5. Clone the chosen repo with `haus clone <url> [dir]`, using the `url` from the search result. Default target is a folder named after the repo under the current directory; confirm where it will land before running. Quote the exact command first.
19
+ 6. Report the result (cloned / skipped if already present / failed). Remind the user that installing dependencies and configuring the repo is still a manual step for now.
20
+
21
+ ## Mode B — no name was given (`project:clone`)
22
+
23
+ Clone a whole **workspace** from its manifest. Workspace-only (a `repos.manifest.json` at the repo root); for a lone repo without a manifest, use Mode A with a name instead.
24
+
25
+ 1. Confirm `repos.manifest.json` exists at the workspace root. If not, tell the user this mode is for multi-repo workspaces (or they can pass a `<name>` to clone a single repo) and stop.
26
+ 2. Read `repos.manifest.json`. Each entry has an `id`, a `folder`, and a git URL (`repo`). If entries have no `repo` URL, ask the user to add them (or supply the URLs) — `haus clone` needs a URL per repo.
27
+ 3. Read `repos.local.json` if present — its `pathOverrides` map (`folder` → absolute path) marks repos the user already has locally.
28
+ 4. Ask the user, via `AskUserQuestion`, how to obtain the repos:
29
+ - **Clean clone** — clone every manifest repo fresh into its `folder` under the workspace.
30
+ - **Reuse local** — skip any repo already in `repos.local.json` `pathOverrides`; clone only the rest.
31
+ 5. For each repo to clone, run (quoting it first): `haus clone <repo-url> <folder>` from the workspace root. Offer `--dry-run` first if the user wants a preview. If one repo fails, report it and continue to the next.
32
+ 6. After the loop, report which repos were cloned, skipped (already present or reused local), and failed. Remind the user that installing dependencies and configuring each repo (`.env`, services) is still a manual step for now.
@@ -21,15 +21,16 @@ The unprefixed verbs (`update`, `catalog`, `install`, `uninstall`) act on **this
21
21
  haus install** (`~/.claude`, npm) — they manage the haus tool itself, like `npm install -g`.
22
22
  The short legacy aliases still work but the names below are canonical.
23
23
 
24
- | Task name (legacy aliases) | Command | Scope | What it does |
25
- | ----------------------------------------------------------------- | ----------------------- | ------- | ------------------------------------------------------------------------------------------- |
26
- | `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs |
27
- | `project:refresh` (`apply`, `refresh`, `claude-md`, `regenerate`) | `haus apply --write` | project | Re-run setup / refresh `.claude/` context + regenerate root `CLAUDE.md` import block |
28
- | `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
29
- | `update` (`upgrade`) | `haus update` | global | Update npm package + catalog + `~/.claude/` (also refreshes this project) |
30
- | `catalog` | `haus update` | global | Fetch latest catalog (same command as update) |
31
- | `install` (`global`) | `haus install` | global | Seed `~/.claude/` with haus-owned files |
32
- | `uninstall` | `haus uninstall` | global | Remove all haus global files from `~/.claude/` |
24
+ | Task name (legacy aliases) | Command | Scope | What it does |
25
+ | ----------------------------------------------------------------- | ----------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
26
+ | `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs |
27
+ | `project:clone [name]` (`clone`) | _Clone procedure below_ | project | No name: clone a **workspace**'s repos from `repos.manifest.json`. With a `name`: find & clone one repo by name from GitHub |
28
+ | `project:refresh` (`apply`, `refresh`, `claude-md`, `regenerate`) | `haus apply --write` | project | Re-run setup / refresh `.claude/` context + regenerate root `CLAUDE.md` import block |
29
+ | `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
30
+ | `update` (`upgrade`) | `haus update` | global | Update npm package + catalog + `~/.claude/` (also refreshes this project) |
31
+ | `catalog` | `haus update` | global | Fetch latest catalog (same command as update) |
32
+ | `install` (`global`) | `haus install` | global | Seed `~/.claude/` with haus-owned files |
33
+ | `uninstall` | `haus uninstall` | global | Remove all haus global files from `~/.claude/` |
33
34
 
34
35
  ## Step 1 — Determine the task
35
36
 
@@ -48,6 +49,8 @@ Options:
48
49
  (haus update — checks npm for new version, fetches catalog, refreshes ~/.claude/)
49
50
  4. [global] catalog — fetch catalog updates only
50
51
  (haus update — same command; pulls latest workflow templates and lockfile)
52
+ 5. [project] project:clone [name] — clone repos
53
+ (no name: clone a workspace from repos.manifest.json; with a name: find & clone one repo by name from GitHub)
51
54
  ```
52
55
 
53
56
  Map the user's selection to the command from the alias table, then continue to Step 2.
@@ -58,6 +61,8 @@ Run the mapped command via Bash. Quote the exact command you are running before
58
61
 
59
62
  **Exception — `project:init` (`setup` / `init`):** this maps to a multi-step procedure, not a single command. Do not run a bare `haus init`. Skip to **Setup (`project:init`)** under Step 3 and follow it.
60
63
 
64
+ **Exception — `project:clone` (`clone`):** this asks the user a question before running, so it is a short procedure too. Skip to **Clone (`project:clone`)** under Step 3 and follow it.
65
+
61
66
  ## Step 3 — Post-run steps
62
67
 
63
68
  After the command completes, follow the relevant post-run steps below.
@@ -67,6 +72,10 @@ After the command completes, follow the relevant post-run steps below.
67
72
  1. Open and follow `~/.claude/commands/haus-setup.md` — the installed `haus-setup` command (in some projects also `.claude/commands/haus-setup.md`). Run every step in order. It detects the stack, asks the guided questions, runs `haus apply --write` (scaffolding, skills, commands, rules, docs skill), writes the **project docs** (`CLAUDE.md` body + `docs/`) and `.haus-workflow/deep-context.json`, runs `haus recommend`, applies the newly-matched helpers, and confirms.
68
73
  2. Then fill `.haus-workflow/workflow-config.md` — replace every placeholder (`TODO`, `n/a`, empty): test/lint/typecheck/build commands (check `package.json`), docs paths, validation library, pre-commit tool, highest-stakes logic (ask if unclear). Leave none.
69
74
 
75
+ ### Clone (`project:clone`)
76
+
77
+ 1. Open and follow `~/.claude/commands/haus-clone.md` — the installed `haus-clone` command. With a `name` argument it finds and clones one matching repo from GitHub; with no argument it clones a workspace's repos from `repos.manifest.json`. Per-repo setup (install, Docker, env) is a separate step that isn't wired yet, so just get the repos in place for now.
78
+
70
79
  ### After `haus apply --write`
71
80
 
72
81
  Verify that the root `CLAUDE.md` imports all three haus files:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {