@united-robotics/cli 0.4.7 → 0.4.8

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 +67 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ var configDirName = ".united-robotics";
11
11
  var globalConfigDir = join(homedir(), configDirName);
12
12
  var globalConfigPath = join(globalConfigDir, configFileName);
13
13
  var defaultApiUrl = process.env.UR_API_URL ?? "https://united-robotics.rollersoft.com.au";
14
+ var defaultGitName = "exis[ai]";
15
+ var defaultGitEmail = "gotexis@gmail.com";
14
16
  function load() {
15
17
  const localPath = findLocalConfigPath(process.cwd());
16
18
  for (const path of [process.env.UR_CONFIG, localPath, globalConfigPath].filter(Boolean)) {
@@ -58,7 +60,7 @@ program.command("team").argument("<cmd>").action(async (cmd) => {
58
60
  var project = program.command("project");
59
61
  project.command("ls").option("--team <teamId>").action(async (opts) => console.log(JSON.stringify(await api(`/api/projects${opts.team ? `?team=${encodeURIComponent(opts.team)}` : ""}`), null, 2)));
60
62
  project.command("show").argument("<projectId>").action(async (projectId) => console.log(JSON.stringify({ projectId, repos: await projectRepos(projectId) }, null, 2)));
61
- project.command("clone").argument("<projectId>").option("--dest <path>").option("--in-place", "clone into the current directory so AGENTS.md and workspace files land at the project root").option("--no-submodules", "clone primary repo only").option("--name <name>", "Git user.name for Vercel-compatible commits", "exis[ai]").option("--email <email>", "Git user.email for Vercel-compatible commits", "gotexis@gmail.com").action(async (projectId, opts) => {
63
+ project.command("clone").argument("<projectId>").option("--dest <path>").option("--in-place", "clone into the current directory so AGENTS.md and workspace files land at the project root").option("--no-submodules", "clone primary repo only").option("--name <name>", "Git user.name for Vercel-compatible commits", defaultGitName).option("--email <email>", "Git user.email for Vercel-compatible commits", defaultGitEmail).action(async (projectId, opts) => {
62
64
  const repos = await projectRepos(projectId);
63
65
  const primary = repos.find((r) => r.kind === "primary") ?? repos[0];
64
66
  if (!primary) throw new Error("No repo found");
@@ -90,7 +92,7 @@ project.command("clone").argument("<projectId>").option("--dest <path>").option(
90
92
  credentials.cleanup();
91
93
  }
92
94
  });
93
- project.command("init").argument("<projectId>").option("--cwd <path>", "workspace path", process.cwd()).option("--name <name>", "Git user.name for Vercel-compatible commits", "exis[ai]").option("--email <email>", "Git user.email for Vercel-compatible commits", "gotexis@gmail.com").action(async (projectId, opts) => {
95
+ project.command("init").argument("<projectId>").option("--cwd <path>", "workspace path", process.cwd()).option("--name <name>", "Git user.name for Vercel-compatible commits", defaultGitName).option("--email <email>", "Git user.email for Vercel-compatible commits", defaultGitEmail).action(async (projectId, opts) => {
94
96
  const token = await projectGithubToken(projectId);
95
97
  const credentials = createGitAskpass(token.token);
96
98
  try {
@@ -107,7 +109,7 @@ program.command("repo").argument("<cmd>").argument("<projectId>").action(async (
107
109
  console.log(JSON.stringify(await projectRepos(projectId), null, 2));
108
110
  });
109
111
  var git = program.command("git").description("Git helpers for tenant workspaces");
110
- git.command("identity").option("--name <name>", "Git user.name", "exis[ai]").option("--email <email>", "Git user.email", "gotexis@gmail.com").option("--global", "write global git config instead of current repo").option("--recursive", "also apply to initialized submodules").action((opts) => {
112
+ git.command("identity").option("--name <name>", "Git user.name", defaultGitName).option("--email <email>", "Git user.email", defaultGitEmail).option("--global", "write global git config instead of current repo").option("--recursive", "also apply to initialized submodules").action((opts) => {
111
113
  if (opts.global) {
112
114
  runGit(["config", "--global", "user.name", opts.name], { cwd: process.cwd() });
113
115
  runGit(["config", "--global", "user.email", opts.email], { cwd: process.cwd() });
@@ -194,6 +196,67 @@ function initProjectWorktree(worktree, projectId, name, email, env = process.env
194
196
  function configureGitIdentity(cwd, name, email, env = process.env) {
195
197
  runGit(["config", "--local", "user.name", name], { cwd, env });
196
198
  runGit(["config", "--local", "user.email", email], { cwd, env });
199
+ installIdentityHooks(cwd, name, email);
200
+ }
201
+ function installIdentityHooks(cwd, name, email) {
202
+ const gitDir = resolveGitDir(cwd);
203
+ if (!gitDir) return;
204
+ const hooksDir = join(gitDir, "hooks");
205
+ mkdirSync(hooksDir, { recursive: true });
206
+ const expected = `${name} <${email}>`;
207
+ const preCommit = `#!/bin/sh
208
+ set -eu
209
+ expected_name='${shellSingleQuoteSafe(name)}'
210
+ expected_email='${shellSingleQuoteSafe(email)}'
211
+ actual_name="\${GIT_AUTHOR_NAME:-$(git config --get user.name || true)}"
212
+ actual_email="\${GIT_AUTHOR_EMAIL:-$(git config --get user.email || true)}"
213
+ committer_name="\${GIT_COMMITTER_NAME:-$(git config --get user.name || true)}"
214
+ committer_email="\${GIT_COMMITTER_EMAIL:-$(git config --get user.email || true)}"
215
+ if [ "$actual_name" != "$expected_name" ] || [ "$actual_email" != "$expected_email" ] || [ "$committer_name" != "$expected_name" ] || [ "$committer_email" != "$expected_email" ]; then
216
+ echo "United Robotics identity guard: commits must use ${expected}." >&2
217
+ echo "Run: ur git identity --recursive" >&2
218
+ exit 1
219
+ fi
220
+ `;
221
+ const prePush = `#!/bin/sh
222
+ set -eu
223
+ expected='${shellSingleQuoteSafe(expected)}'
224
+ while read local_ref local_sha remote_ref remote_sha; do
225
+ case "$local_sha" in
226
+ 0000000000000000000000000000000000000000) continue ;;
227
+ esac
228
+ if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
229
+ range="$local_sha"
230
+ else
231
+ range="$remote_sha..$local_sha"
232
+ fi
233
+ bad=$(git log --format='%H%x09%an <%ae>%x09%cn <%ce>' "$range" -- 2>/dev/null | awk -F ' ' -v e="$expected" '$2 != e || $3 != e { print; exit }')
234
+ if [ -n "$bad" ]; then
235
+ echo "United Robotics identity guard: refusing push with non-standard author/committer." >&2
236
+ echo "$bad" >&2
237
+ echo "Expected author and committer: $expected" >&2
238
+ echo "Fix by recommitting/amending after: ur git identity --recursive" >&2
239
+ exit 1
240
+ fi
241
+ done
242
+ `;
243
+ writeExecutableHook(join(hooksDir, "pre-commit"), preCommit);
244
+ writeExecutableHook(join(hooksDir, "pre-push"), prePush);
245
+ }
246
+ function resolveGitDir(cwd) {
247
+ const result = runGit(["rev-parse", "--git-dir"], { cwd, allowFailure: true });
248
+ if (result.status !== 0) return null;
249
+ const value = result.stdout.trim();
250
+ if (!value) return null;
251
+ return value.startsWith("/") ? value : join(cwd, value);
252
+ }
253
+ function writeExecutableHook(path, content) {
254
+ writeFileSync(path, content, { mode: 493 });
255
+ chmodSync(path, 493);
256
+ }
257
+ function listInitializedSubmodules(worktree, env = process.env) {
258
+ const result = runGit(["submodule", "status", "--recursive"], { cwd: worktree, env, allowFailure: true });
259
+ return result.stdout.split(/\r?\n/).map((line) => line.trim().split(/\s+/)[1]).filter((value) => Boolean(value));
197
260
  }
198
261
  function configureGitCredentialHelper(cwd, projectId, env = process.env) {
199
262
  const helper = `!ur git credential-helper --project ${shellSingleQuoteSafe(projectId)}`;
@@ -206,6 +269,7 @@ function configureGitCredentialHelper(cwd, projectId, env = process.env) {
206
269
  function configureSubmoduleIdentities(worktree, name, email, env = process.env) {
207
270
  const result = runGit(["submodule", "foreach", "--recursive", `git config --local user.name '${shellSingleQuoteSafe(name)}' && git config --local user.email '${shellSingleQuoteSafe(email)}'`], { cwd: worktree, env, allowFailure: true });
208
271
  if (result.status !== 0) console.warn("Warning: could not apply git identity to all submodules. Run `ur git identity --recursive` after submodules are initialized.");
272
+ for (const submodule of listInitializedSubmodules(worktree, env)) installIdentityHooks(join(worktree, submodule), name, email);
209
273
  }
210
274
  function configureSubmoduleCredentialHelpers(worktree, projectId, env = process.env) {
211
275
  const helper = `!ur git credential-helper --project ${shellSingleQuoteSafe(projectId)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@united-robotics/cli",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ur": "dist/index.js"