@osfactory/har 0.63.0 → 0.64.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 (2) hide show
  1. package/dist/index.js +218 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -29230,9 +29230,6 @@ function resolvePortalTargetsForRepo(options) {
29230
29230
  const aliases = getRepoPortalTargetAliases(options.repoPath);
29231
29231
  const attached = aliases.map((alias) => getPortalTargetRecord(alias)).filter((entry) => entry !== null).map(recordToPortalTarget);
29232
29232
  if (attached.length > 0) return { source: "repo", targets: attached };
29233
- if (store.targets.length === 1) {
29234
- return { source: "single", targets: [recordToPortalTarget(store.targets[0])] };
29235
- }
29236
29233
  return null;
29237
29234
  }
29238
29235
  if (store.defaultTarget) {
@@ -74842,7 +74839,30 @@ function openBrowser(url) {
74842
74839
  } catch {
74843
74840
  }
74844
74841
  }
74845
- async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
74842
+ var REPOS_QUERY_BUDGET = 1800;
74843
+ function encodeCliReposQuery(repos) {
74844
+ const unique = [...new Set(repos.filter((repo) => repo.trim().length > 0))];
74845
+ if (unique.length === 0) return null;
74846
+ const encoded = encodeURIComponent(JSON.stringify(unique));
74847
+ if (encoded.length > REPOS_QUERY_BUDGET) return null;
74848
+ return encoded;
74849
+ }
74850
+ function parseCliTargetsParam(raw) {
74851
+ if (!raw) return null;
74852
+ try {
74853
+ const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
74854
+ if (!Array.isArray(parsed.targets)) return null;
74855
+ const targets = parsed.targets.filter(
74856
+ (entry) => typeof entry?.workspaceId === "string" && typeof entry?.token === "string" && Array.isArray(entry.repos)
74857
+ );
74858
+ return targets.length > 0 ? targets : null;
74859
+ } catch {
74860
+ return null;
74861
+ }
74862
+ }
74863
+ async function loginViaBrowser(portalUrl, openUrlOrOptions = openBrowser) {
74864
+ const options = typeof openUrlOrOptions === "function" ? { openUrl: openUrlOrOptions } : openUrlOrOptions;
74865
+ const openUrl = options.openUrl ?? openBrowser;
74846
74866
  const state = (0, import_crypto.randomUUID)();
74847
74867
  return new Promise((resolve53, reject) => {
74848
74868
  let timer;
@@ -74871,6 +74891,7 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
74871
74891
  res.writeHead(200, { "Content-Type": "text/html" });
74872
74892
  res.end("<h1>Logged in</h1><p>You can close this tab and return to the terminal.</p>");
74873
74893
  cleanup();
74894
+ const targets = parseCliTargetsParam(requestUrl.searchParams.get("targets"));
74874
74895
  resolve53({
74875
74896
  portalUrl,
74876
74897
  token,
@@ -74881,7 +74902,8 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
74881
74902
  email: email2,
74882
74903
  refreshToken,
74883
74904
  expiresAt,
74884
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
74905
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
74906
+ ...targets ? { targets } : {}
74885
74907
  });
74886
74908
  });
74887
74909
  const cleanup = () => {
@@ -74895,7 +74917,8 @@ async function loginViaBrowser(portalUrl, openUrl = openBrowser) {
74895
74917
  server.listen(0, "127.0.0.1", () => {
74896
74918
  const { port } = server.address();
74897
74919
  const callback = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
74898
- const loginUrl = `${portalUrl}/cli/login?callback=${encodeURIComponent(callback)}&state=${state}`;
74920
+ const reposQuery = encodeCliReposQuery(options.repos ?? []);
74921
+ const loginUrl = `${portalUrl}/cli/login?callback=${encodeURIComponent(callback)}&state=${state}${reposQuery ? `&repos=${reposQuery}` : ""}`;
74899
74922
  openUrl(loginUrl);
74900
74923
  process.stderr.write(`Opening ${loginUrl}
74901
74924
  If your browser did not open, paste that URL.
@@ -74908,6 +74931,59 @@ If your browser did not open, paste that URL.
74908
74931
  });
74909
74932
  }
74910
74933
 
74934
+ // src/core/portal-repo-map.ts
74935
+ init_control_repo_path();
74936
+ init_control_registry();
74937
+ init_portal_targets();
74938
+ function planRepoWorkspaceMap(opts) {
74939
+ const registered = (opts.registeredRepos ?? listRegisteredRepos()).map(
74940
+ (repoPath) => canonicalizeControlRepoPath(repoPath)
74941
+ );
74942
+ const current = opts.currentRepo ? canonicalizeControlRepoPath(opts.currentRepo) : null;
74943
+ const others = registered.filter((repoPath) => repoPath !== current);
74944
+ const connections = opts.connections ?? listPortalTargetRecords();
74945
+ if (others.length === 0 || connections.length === 0) return { kind: "none" };
74946
+ if (connections.length === 1) {
74947
+ const only = connections[0];
74948
+ return {
74949
+ kind: "attach-more",
74950
+ workspaceAlias: only.alias,
74951
+ workspaceLabel: displayPortalTargetLabel(only),
74952
+ repos: others
74953
+ };
74954
+ }
74955
+ return {
74956
+ kind: "map-orgs",
74957
+ currentAlias: opts.currentAlias,
74958
+ repos: others,
74959
+ connections: connections.map((entry) => ({
74960
+ alias: entry.alias,
74961
+ label: displayPortalTargetLabel(entry)
74962
+ }))
74963
+ };
74964
+ }
74965
+ function applyRepoWorkspaceMap(assignments) {
74966
+ const attached = [];
74967
+ for (const row of assignments) {
74968
+ if (!row.alias) continue;
74969
+ const canonical = canonicalizeControlRepoPath(row.repoPath);
74970
+ attachRepoPortalTarget(canonical, row.alias);
74971
+ recordRepoForControlSync(canonical);
74972
+ attached.push(canonical);
74973
+ }
74974
+ return attached;
74975
+ }
74976
+ function remainingUnassignedRepos(repos, assigned) {
74977
+ const taken = new Set(
74978
+ [...assigned].map((repoPath) => canonicalizeControlRepoPath(repoPath))
74979
+ );
74980
+ return repos.filter((repoPath) => !taken.has(canonicalizeControlRepoPath(repoPath)));
74981
+ }
74982
+ function remainingConnections(connections, usedAliases) {
74983
+ const used = /* @__PURE__ */ new Set([...usedAliases]);
74984
+ return connections.filter((entry) => !used.has(entry.alias));
74985
+ }
74986
+
74911
74987
  // src/core/portal-connect.ts
74912
74988
  init_portal_targets();
74913
74989
  function attachRepoIfPresent(alias, repoPath) {
@@ -74930,9 +75006,16 @@ async function runPortalConnect(input) {
74930
75006
  token: input.apiKey,
74931
75007
  setAsDefault: true
74932
75008
  });
74933
- return { record: record3, attachedRepo: attachRepoIfPresent(record3.alias, input.repoPath) };
75009
+ return {
75010
+ record: record3,
75011
+ attachedRepo: attachRepoIfPresent(record3.alias, input.repoPath),
75012
+ extraAttached: [],
75013
+ mappedInBrowser: false
75014
+ };
74934
75015
  }
74935
- const creds = await loginViaBrowser(input.portalUrl);
75016
+ const creds = await loginViaBrowser(input.portalUrl, {
75017
+ repos: listRegisteredReposForConnect(input.repoPath)
75018
+ });
74936
75019
  const record2 = upsertPortalTarget({
74937
75020
  alias: input.alias,
74938
75021
  portalUrl: input.portalUrl,
@@ -74946,7 +75029,35 @@ async function runPortalConnect(input) {
74946
75029
  email: creds.email,
74947
75030
  setAsDefault: true
74948
75031
  });
74949
- return { record: record2, attachedRepo: attachRepoIfPresent(record2.alias, input.repoPath) };
75032
+ const attachedRepo = attachRepoIfPresent(record2.alias, input.repoPath);
75033
+ if (!creds.targets || creds.targets.length === 0) {
75034
+ return { record: record2, attachedRepo, extraAttached: [], mappedInBrowser: false };
75035
+ }
75036
+ const extraAttached = [];
75037
+ for (const target of creds.targets) {
75038
+ const next = upsertPortalTarget({
75039
+ portalUrl: input.portalUrl,
75040
+ workspaceId: target.workspaceId,
75041
+ workspace: target.workspace,
75042
+ workspaceSlug: target.workspace,
75043
+ workspaceName: target.workspaceName,
75044
+ token: target.token,
75045
+ refreshToken: target.refreshToken,
75046
+ expiresAt: target.expiresAt,
75047
+ email: creds.email,
75048
+ setAsDefault: false
75049
+ });
75050
+ const attached = applyRepoWorkspaceMap(
75051
+ target.repos.map((repoPath) => ({ repoPath, alias: next.alias }))
75052
+ );
75053
+ extraAttached.push(...attached.filter((repoPath) => repoPath !== attachedRepo));
75054
+ }
75055
+ return { record: record2, attachedRepo, extraAttached, mappedInBrowser: true };
75056
+ }
75057
+ function listRegisteredReposForConnect(repoPath) {
75058
+ const resolved = repoPath ? path59.resolve(repoPath) : null;
75059
+ const current = resolved && fs49.existsSync(resolved) ? canonicalizeControlRepoPath(resolved) : null;
75060
+ return [.../* @__PURE__ */ new Set([...current ? [current] : [], ...listRegisteredRepos()])];
74950
75061
  }
74951
75062
 
74952
75063
  // src/cli/commands/hq.ts
@@ -74978,7 +75089,7 @@ var hqCommand = {
74978
75089
  alias: "y",
74979
75090
  type: "boolean",
74980
75091
  default: false,
74981
- describe: "Skip the destination prompt and use the resolved portal URL"
75092
+ describe: "Skip interactive prompts (portal URL and extra-repo mapping)"
74982
75093
  }),
74983
75094
  (argv) => handleHqConnect(
74984
75095
  argv
@@ -75036,6 +75147,90 @@ async function resolveConnectPortalUrl(explicit, skipPrompt) {
75036
75147
  ]);
75037
75148
  return resolvePortalUrl(url);
75038
75149
  }
75150
+ async function promptExtraRepoAttachments(opts) {
75151
+ const plan = planRepoWorkspaceMap(opts);
75152
+ if (plan.kind === "none") return [];
75153
+ const noun = plan.repos.length === 1 ? "repository" : "repositories";
75154
+ info(`${plan.repos.length} other registered ${noun}:`);
75155
+ for (const repoPath of plan.repos) info(` ${repoPath}`);
75156
+ if (plan.kind === "attach-more") {
75157
+ const { selected } = await esm_default13.prompt([
75158
+ {
75159
+ type: "checkbox",
75160
+ name: "selected",
75161
+ message: `Attach which of these ${plan.repos.length} ${noun} to ${plan.workspaceLabel}?`,
75162
+ choices: plan.repos.map((repoPath) => ({
75163
+ name: repoPath,
75164
+ value: repoPath,
75165
+ checked: true
75166
+ }))
75167
+ }
75168
+ ]);
75169
+ return applyRepoWorkspaceMap(
75170
+ selected.map((repoPath) => ({ repoPath, alias: plan.workspaceAlias }))
75171
+ );
75172
+ }
75173
+ const { sendAll } = await esm_default13.prompt([
75174
+ {
75175
+ type: "confirm",
75176
+ name: "sendAll",
75177
+ message: `Send all ${plan.repos.length} ${noun} to the same workspace?`,
75178
+ default: true
75179
+ }
75180
+ ]);
75181
+ if (sendAll) {
75182
+ const { alias } = await esm_default13.prompt([
75183
+ {
75184
+ type: "list",
75185
+ name: "alias",
75186
+ message: "Which workspace?",
75187
+ default: plan.currentAlias,
75188
+ choices: plan.connections.map((entry) => ({
75189
+ name: entry.label,
75190
+ value: entry.alias
75191
+ }))
75192
+ }
75193
+ ]);
75194
+ return applyRepoWorkspaceMap(plan.repos.map((repoPath) => ({ repoPath, alias })));
75195
+ }
75196
+ const attached = [];
75197
+ let remainingRepos = [...plan.repos];
75198
+ let remainingOrgs = [...plan.connections];
75199
+ while (remainingRepos.length > 0 && remainingOrgs.length > 0) {
75200
+ const left2 = remainingRepos.length === 1 ? "repository" : "repositories";
75201
+ info(`${remainingRepos.length} ${left2} left to assign.`);
75202
+ const { alias } = await esm_default13.prompt([
75203
+ {
75204
+ type: "list",
75205
+ name: "alias",
75206
+ message: "Workspace to assign repositories to",
75207
+ default: remainingOrgs.some((entry) => entry.alias === plan.currentAlias) ? plan.currentAlias : remainingOrgs[0]?.alias,
75208
+ choices: [
75209
+ ...remainingOrgs.map((entry) => ({
75210
+ name: entry.label,
75211
+ value: entry.alias
75212
+ })),
75213
+ { name: "Done \u2014 leave the rest unattached", value: "" }
75214
+ ]
75215
+ }
75216
+ ]);
75217
+ if (!alias) break;
75218
+ const { selected } = await esm_default13.prompt([
75219
+ {
75220
+ type: "checkbox",
75221
+ name: "selected",
75222
+ message: `Repositories for ${remainingOrgs.find((entry) => entry.alias === alias)?.label ?? alias}`,
75223
+ choices: remainingRepos.map((repoPath) => ({ name: repoPath, value: repoPath }))
75224
+ }
75225
+ ]);
75226
+ if (selected.length > 0) {
75227
+ attached.push(...applyRepoWorkspaceMap(selected.map((repoPath) => ({ repoPath, alias }))));
75228
+ }
75229
+ remainingRepos = remainingUnassignedRepos(remainingRepos, selected);
75230
+ remainingOrgs = remainingConnections(remainingOrgs, [alias]);
75231
+ }
75232
+ return attached;
75233
+ }
75039
75234
  async function handleHqConnect(argv) {
75040
75235
  if (argv.deprecated) {
75041
75236
  warn("har control login is deprecated \u2014 use har hq connect");
@@ -75046,7 +75241,7 @@ async function handleHqConnect(argv) {
75046
75241
  info(`Portal: ${portalUrl} (${PORTAL_SOURCE_LABEL[source]})`);
75047
75242
  }
75048
75243
  try {
75049
- const { record: record2, attachedRepo } = await runPortalConnect({
75244
+ const { record: record2, attachedRepo, extraAttached, mappedInBrowser } = await runPortalConnect({
75050
75245
  portalUrl,
75051
75246
  apiKey: argv.apiKey,
75052
75247
  repoPath: argv.repo ?? ".",
@@ -75074,6 +75269,18 @@ async function handleHqConnect(argv) {
75074
75269
  } else {
75075
75270
  info("No local repository attached (run this from a repo, or pass --repo).");
75076
75271
  }
75272
+ for (const repoPath of extraAttached) {
75273
+ success(`Attached ${repoPath}`);
75274
+ }
75275
+ if (!argv.json && !argv.yes && !mappedInBrowser && isInteractive()) {
75276
+ const extra = await promptExtraRepoAttachments({
75277
+ currentRepo: attachedRepo,
75278
+ currentAlias: record2.alias
75279
+ });
75280
+ for (const repoPath of extra) {
75281
+ success(`Attached ${repoPath}`);
75282
+ }
75283
+ }
75077
75284
  info("Sync: har control sync");
75078
75285
  } catch (err) {
75079
75286
  error(`Connect failed: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osfactory/har",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "description": "HAR: open agent harness CLI and MCP server for coding agents, isolated worktrees, and software-factory workflows",
5
5
  "keywords": [
6
6
  "har",