@merittdev/horus 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.cjs +118 -27
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -57239,7 +57239,7 @@ init_cjs_shims();
57239
57239
 
57240
57240
  // ../../packages/core/src/version.ts
57241
57241
  init_cjs_shims();
57242
- var HORUS_VERSION = true ? "0.2.0" : "dev";
57242
+ var HORUS_VERSION = true ? "0.2.1" : "dev";
57243
57243
  var PINNED_AXON_VERSION = "1.0.7";
57244
57244
  var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
57245
57245
 
@@ -92205,6 +92205,18 @@ var CloudClient = class {
92205
92205
  context() {
92206
92206
  return this.request("GET", "/v1/context");
92207
92207
  }
92208
+ /** Create a workspace in an org (HOR-307 — used by `cloud link` create-from-repo). */
92209
+ createWorkspace(organizationId, body) {
92210
+ return this.request("POST", `/v1/organizations/${organizationId}/workspaces`, body);
92211
+ }
92212
+ /** Create a project in a workspace, optionally with its repo identity (HOR-307). */
92213
+ createProject(organizationId, workspaceId, body) {
92214
+ return this.request(
92215
+ "POST",
92216
+ `/v1/organizations/${organizationId}/workspaces/${workspaceId}/projects`,
92217
+ body
92218
+ );
92219
+ }
92208
92220
  listTokens() {
92209
92221
  return this.request("GET", "/v1/cli/tokens");
92210
92222
  }
@@ -98420,6 +98432,22 @@ function parseRepoName(remoteUrl) {
98420
98432
  const lastSegment = cleaned.split(/[/:]/).filter(Boolean).pop() ?? "";
98421
98433
  return lastSegment;
98422
98434
  }
98435
+ function normalizeRemote(remoteUrl) {
98436
+ let s = remoteUrl.trim().replace(/\.git$/, "");
98437
+ s = s.replace(/^[a-z]+:\/\//i, "");
98438
+ s = s.replace(/^[^@/]+@/, "");
98439
+ s = s.replace(":", "/");
98440
+ return s.replace(/\/+$/, "").toLowerCase();
98441
+ }
98442
+ function remotesMatch(a, b2) {
98443
+ return normalizeRemote(a) === normalizeRemote(b2);
98444
+ }
98445
+ function inferProvider(remoteUrl) {
98446
+ const host = normalizeRemote(remoteUrl).split("/")[0] ?? "";
98447
+ if (host.includes("github")) return "github";
98448
+ if (host.includes("gitlab")) return "gitlab";
98449
+ return "other";
98450
+ }
98423
98451
  function detectGitRemote(root) {
98424
98452
  try {
98425
98453
  const remoteUrl = (0, import_node_child_process10.execFileSync)("git", ["-C", root, "remote", "get-url", "origin"], {
@@ -98449,6 +98477,7 @@ async function runCloudLink(opts = {}) {
98449
98477
  }
98450
98478
  const remote = detectGitRemote(root);
98451
98479
  if (remote) console.log(import_picocolors42.default.dim(`Detected git remote: ${remote.remoteUrl}`));
98480
+ let resolved = null;
98452
98481
  let target = opts.project;
98453
98482
  if (!target) {
98454
98483
  const existing = readCloudConfig(root);
@@ -98456,24 +98485,40 @@ async function runCloudLink(opts = {}) {
98456
98485
  target = `${existing.organization.slug}/${existing.workspace.slug}/${existing.project.slug}`;
98457
98486
  }
98458
98487
  }
98459
- if (!target) {
98460
- target = await pickProjectInteractively(ctx);
98488
+ if (!target && remote) {
98489
+ const match = ctx.projects.find((p) => p.remoteUrl && remotesMatch(p.remoteUrl, remote.remoteUrl));
98490
+ if (match) {
98491
+ const ws = ctx.workspaces.find((w) => w.id === match.workspaceId);
98492
+ const org = ctx.organizations.find((o) => o.id === match.organizationId);
98493
+ if (ws && org) {
98494
+ target = `${org.slug}/${ws.slug}/${match.slug}`;
98495
+ console.log(import_picocolors42.default.dim(`Matched existing project by remote: ${import_picocolors42.default.bold(target)}`));
98496
+ }
98497
+ }
98461
98498
  }
98462
98499
  if (!target) {
98463
- console.error(
98464
- import_picocolors42.default.red("No project specified.") + `
98465
- Pass ${import_picocolors42.default.bold("--project <org/workspace/project>")}, or run ${import_picocolors42.default.bold("horus context list")} to see options.`
98466
- );
98467
- return 1;
98500
+ const created = await createProjectFromRepo(session.client, ctx, remote, opts.yes);
98501
+ if (created === "aborted") return 1;
98502
+ if (!created) {
98503
+ console.error(
98504
+ import_picocolors42.default.red("No project to link.") + `
98505
+ Run ${import_picocolors42.default.bold("horus cloud link")} in a TTY to create one, pass ${import_picocolors42.default.bold("--project <org/workspace/project>")}, or create a project at ${import_picocolors42.default.bold("https://cloud.horus.sh")}.`
98506
+ );
98507
+ return 1;
98508
+ }
98509
+ resolved = created;
98468
98510
  }
98469
- const resolved = resolveTriple(ctx, target);
98470
98511
  if (!resolved) {
98471
- console.error(
98472
- import_picocolors42.default.red(`You don't have access to ${import_picocolors42.default.bold(target)}.`) + `
98512
+ resolved = resolveTriple(ctx, target);
98513
+ if (!resolved) {
98514
+ console.error(
98515
+ import_picocolors42.default.red(`You don't have access to ${import_picocolors42.default.bold(target)}.`) + `
98473
98516
  Run ${import_picocolors42.default.bold("horus context list")} to see available projects.`
98474
- );
98475
- return 1;
98517
+ );
98518
+ return 1;
98519
+ }
98476
98520
  }
98521
+ const label = `${resolved.organization.slug}/${resolved.workspace.slug}/${resolved.project.slug}`;
98477
98522
  writeCloudConfig(root, {
98478
98523
  context: "cloud",
98479
98524
  organization: resolved.organization,
@@ -98481,7 +98526,7 @@ async function runCloudLink(opts = {}) {
98481
98526
  project: resolved.project
98482
98527
  });
98483
98528
  console.log(
98484
- `${import_picocolors42.default.green("\u2713")} Linked. Context for this repo is now ${import_picocolors42.default.bold(target)} ${import_picocolors42.default.dim("(cloud)")}.`
98529
+ `${import_picocolors42.default.green("\u2713")} Linked. Context for this repo is now ${import_picocolors42.default.bold(label)} ${import_picocolors42.default.dim("(cloud)")}.`
98485
98530
  );
98486
98531
  console.log(
98487
98532
  import_picocolors42.default.dim(` Investigations here save to Horus Cloud. ${import_picocolors42.default.bold("horus context use local")} to switch back.`)
@@ -98622,21 +98667,67 @@ async function runCloudSync(opts = {}) {
98622
98667
  );
98623
98668
  return failed > 0 ? 1 : 0;
98624
98669
  }
98625
- async function pickProjectInteractively(ctx) {
98626
- if (!process.stdin.isTTY) return void 0;
98627
- const labels = ctx.projects.map((p) => {
98628
- const ws = ctx.workspaces.find((w) => w.id === p.workspaceId);
98629
- const org = ctx.organizations.find((o) => o.id === p.organizationId);
98630
- return ws && org ? `${org.slug}/${ws.slug}/${p.slug}` : null;
98631
- }).filter((x) => !!x);
98632
- if (labels.length === 0) return void 0;
98633
- console.log("Select a project to link:");
98634
- labels.forEach((label, i) => console.log(` ${i + 1}) ${label}`));
98670
+ async function createProjectFromRepo(client, ctx, remote, yes) {
98671
+ if (!process.stdin.isTTY && !yes) return null;
98672
+ if (ctx.organizations.length === 0) {
98673
+ console.error(
98674
+ import_picocolors42.default.red("You're not a member of any organization yet.") + `
98675
+ Create one at ${import_picocolors42.default.bold("https://cloud.horus.sh")} first.`
98676
+ );
98677
+ return null;
98678
+ }
98635
98679
  const rl = (0, import_promises2.createInterface)({ input: process.stdin, output: process.stdout });
98680
+ const ask2 = async (q, def) => {
98681
+ if (yes && def !== void 0) return def;
98682
+ const a = (await rl.question(def ? `${q} (${def}): ` : `${q}: `)).trim();
98683
+ return a || def || "";
98684
+ };
98636
98685
  try {
98637
- const answer = (await rl.question("Number: ")).trim();
98638
- const idx = Number(answer) - 1;
98639
- return labels[idx];
98686
+ let org = ctx.organizations[0];
98687
+ if (ctx.organizations.length > 1) {
98688
+ console.log("Organizations:");
98689
+ ctx.organizations.forEach((o, i) => console.log(` ${i + 1}) ${o.slug}`));
98690
+ const idx = Number(await ask2("Org number", "1")) - 1;
98691
+ org = ctx.organizations[idx] ?? org;
98692
+ }
98693
+ const orgWorkspaces = ctx.workspaces.filter((w) => w.organizationId === org.id);
98694
+ let workspace;
98695
+ const repoName = remote ? parseRepoName(remote.remoteUrl) : "";
98696
+ if (orgWorkspaces.length === 0) {
98697
+ const wsName = await ask2("New workspace name", repoName || "default");
98698
+ const ws = await client.createWorkspace(org.id, { name: wsName });
98699
+ workspace = { id: ws.id, slug: ws.slug };
98700
+ console.log(import_picocolors42.default.dim(`Created workspace ${import_picocolors42.default.bold(ws.slug)}.`));
98701
+ } else {
98702
+ console.log(`Workspaces in ${import_picocolors42.default.bold(org.slug)}:`);
98703
+ orgWorkspaces.forEach((w, i) => console.log(` ${i + 1}) ${w.slug}`));
98704
+ console.log(` ${orgWorkspaces.length + 1}) + create a new workspace`);
98705
+ const pick2 = Number(await ask2("Workspace number", "1"));
98706
+ if (pick2 === orgWorkspaces.length + 1) {
98707
+ const wsName = await ask2("New workspace name", repoName || "default");
98708
+ const ws = await client.createWorkspace(org.id, { name: wsName });
98709
+ workspace = { id: ws.id, slug: ws.slug };
98710
+ console.log(import_picocolors42.default.dim(`Created workspace ${import_picocolors42.default.bold(ws.slug)}.`));
98711
+ } else {
98712
+ const chosen = orgWorkspaces[pick2 - 1] ?? orgWorkspaces[0];
98713
+ workspace = { id: chosen.id, slug: chosen.slug };
98714
+ }
98715
+ }
98716
+ const projName = await ask2("Project name", repoName || "project");
98717
+ const project = await client.createProject(org.id, workspace.id, {
98718
+ name: projName,
98719
+ remoteUrl: remote?.remoteUrl,
98720
+ provider: remote ? inferProvider(remote.remoteUrl) : void 0
98721
+ });
98722
+ console.log(import_picocolors42.default.dim(`Created project ${import_picocolors42.default.bold(project.slug)}.`));
98723
+ return {
98724
+ organization: { id: org.id, slug: org.slug },
98725
+ workspace,
98726
+ project: { id: project.id, slug: project.slug }
98727
+ };
98728
+ } catch (err) {
98729
+ reportCloudError(err);
98730
+ return "aborted";
98640
98731
  } finally {
98641
98732
  rl.close();
98642
98733
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {