@united-robotics/cli 0.4.6 → 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.
- package/dist/index.js +109 -6
- 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)) {
|
|
@@ -26,6 +28,17 @@ function save(config, opts = {}) {
|
|
|
26
28
|
mkdirSync(dirname(path), { recursive: true });
|
|
27
29
|
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
28
30
|
}
|
|
31
|
+
function rememberDefaultProject(projectId, cwd) {
|
|
32
|
+
const currentCwd = process.cwd();
|
|
33
|
+
try {
|
|
34
|
+
process.chdir(cwd);
|
|
35
|
+
const cfg = load();
|
|
36
|
+
save({ ...cfg, defaultProjectId: projectId });
|
|
37
|
+
ignoreWorkspaceConfigInGit(cwd);
|
|
38
|
+
} finally {
|
|
39
|
+
process.chdir(currentCwd);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
29
42
|
async function api(path, init = {}) {
|
|
30
43
|
const cfg = load();
|
|
31
44
|
const res = await fetch(`${cfg.apiUrl}${path}`, { ...init, headers: { authorization: `Bearer ${cfg.token ?? ""}`, "content-type": "application/json", ...init.headers ?? {} } });
|
|
@@ -47,7 +60,7 @@ program.command("team").argument("<cmd>").action(async (cmd) => {
|
|
|
47
60
|
var project = program.command("project");
|
|
48
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)));
|
|
49
62
|
project.command("show").argument("<projectId>").action(async (projectId) => console.log(JSON.stringify({ projectId, repos: await projectRepos(projectId) }, null, 2)));
|
|
50
|
-
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",
|
|
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) => {
|
|
51
64
|
const repos = await projectRepos(projectId);
|
|
52
65
|
const primary = repos.find((r) => r.kind === "primary") ?? repos[0];
|
|
53
66
|
if (!primary) throw new Error("No repo found");
|
|
@@ -70,18 +83,21 @@ project.command("clone").argument("<projectId>").option("--dest <path>").option(
|
|
|
70
83
|
preservedConfig?.restore();
|
|
71
84
|
}
|
|
72
85
|
const worktree = dest ?? repoDirName(primary.url);
|
|
86
|
+
rememberDefaultProject(projectId, worktree);
|
|
73
87
|
configureGitIdentity(worktree, opts.name, opts.email, credentials.env);
|
|
74
|
-
if (opts.submodules) initProjectWorktree(worktree, opts.name, opts.email, credentials.env);
|
|
88
|
+
if (opts.submodules) initProjectWorktree(worktree, projectId, opts.name, opts.email, credentials.env);
|
|
89
|
+
else configureGitCredentialHelper(worktree, projectId, credentials.env);
|
|
75
90
|
printCloneSuccess(projectId, worktree, token);
|
|
76
91
|
} finally {
|
|
77
92
|
credentials.cleanup();
|
|
78
93
|
}
|
|
79
94
|
});
|
|
80
|
-
project.command("init").argument("<projectId>").option("--cwd <path>", "workspace path", process.cwd()).option("--name <name>", "Git user.name for Vercel-compatible commits",
|
|
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) => {
|
|
81
96
|
const token = await projectGithubToken(projectId);
|
|
82
97
|
const credentials = createGitAskpass(token.token);
|
|
83
98
|
try {
|
|
84
|
-
|
|
99
|
+
rememberDefaultProject(projectId, opts.cwd);
|
|
100
|
+
initProjectWorktree(opts.cwd, projectId, opts.name, opts.email, credentials.env);
|
|
85
101
|
console.log(`Initialized ${projectId} workspace at ${opts.cwd}`);
|
|
86
102
|
printDetachedHeadNotice();
|
|
87
103
|
} finally {
|
|
@@ -93,7 +109,7 @@ program.command("repo").argument("<cmd>").argument("<projectId>").action(async (
|
|
|
93
109
|
console.log(JSON.stringify(await projectRepos(projectId), null, 2));
|
|
94
110
|
});
|
|
95
111
|
var git = program.command("git").description("Git helpers for tenant workspaces");
|
|
96
|
-
git.command("identity").option("--name <name>", "Git user.name",
|
|
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) => {
|
|
97
113
|
if (opts.global) {
|
|
98
114
|
runGit(["config", "--global", "user.name", opts.name], { cwd: process.cwd() });
|
|
99
115
|
runGit(["config", "--global", "user.email", opts.email], { cwd: process.cwd() });
|
|
@@ -103,6 +119,16 @@ git.command("identity").option("--name <name>", "Git user.name", "exis[ai]").opt
|
|
|
103
119
|
}
|
|
104
120
|
console.log(`Configured git committer identity: ${opts.name} <${opts.email}>`);
|
|
105
121
|
});
|
|
122
|
+
git.command("credential-helper").description("Git credential helper used by United Robotics workspaces").argument("[operation]", "git credential helper operation").option("--project <projectId>", "project id; defaults to local config defaultProjectId").action(async (operation, opts) => {
|
|
123
|
+
if (operation && operation !== "get") return;
|
|
124
|
+
const projectId = opts.project ?? load().defaultProjectId;
|
|
125
|
+
if (!projectId) throw new Error("Missing project id for UR git credential helper. Run `ur project init <projectId>` from the workspace root.");
|
|
126
|
+
const result = await projectGithubToken(projectId);
|
|
127
|
+
process.stdout.write(`username=x-access-token
|
|
128
|
+
password=${result.token}
|
|
129
|
+
|
|
130
|
+
`);
|
|
131
|
+
});
|
|
106
132
|
var github = program.command("github");
|
|
107
133
|
github.command("token").option("--project <projectId>", "project id", "skinspirit-main").option("--repo <repo>", "repo id, name, full name, or URL within the project").option("--json", "emit full JSON").option("--redact", "redact token in JSON output").action(async (opts) => {
|
|
108
134
|
const result = await api(`/api/projects/${opts.project}/github-token`, {
|
|
@@ -158,20 +184,97 @@ esac
|
|
|
158
184
|
cleanup: () => rmSync(dir, { recursive: true, force: true })
|
|
159
185
|
};
|
|
160
186
|
}
|
|
161
|
-
function initProjectWorktree(worktree, name, email, env = process.env) {
|
|
187
|
+
function initProjectWorktree(worktree, projectId, name, email, env = process.env) {
|
|
162
188
|
configureGitIdentity(worktree, name, email, env);
|
|
163
189
|
runGit(["submodule", "sync", "--recursive"], { cwd: worktree, env, authHint: true, allowFailure: true });
|
|
164
190
|
runGit(["submodule", "update", "--init", "--recursive"], { cwd: worktree, env, authHint: true });
|
|
191
|
+
configureGitCredentialHelper(worktree, projectId, env);
|
|
165
192
|
configureSubmoduleIdentities(worktree, name, email, env);
|
|
193
|
+
configureSubmoduleCredentialHelpers(worktree, projectId, env);
|
|
166
194
|
printDetachedHeadNotice();
|
|
167
195
|
}
|
|
168
196
|
function configureGitIdentity(cwd, name, email, env = process.env) {
|
|
169
197
|
runGit(["config", "--local", "user.name", name], { cwd, env });
|
|
170
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));
|
|
260
|
+
}
|
|
261
|
+
function configureGitCredentialHelper(cwd, projectId, env = process.env) {
|
|
262
|
+
const helper = `!ur git credential-helper --project ${shellSingleQuoteSafe(projectId)}`;
|
|
263
|
+
runGit(["config", "--local", "--unset-all", "credential.helper"], { cwd, env, allowFailure: true });
|
|
264
|
+
runGit(["config", "--local", "--add", "credential.helper", ""], { cwd, env });
|
|
265
|
+
runGit(["config", "--local", "--add", "credential.helper", helper], { cwd, env });
|
|
266
|
+
runGit(["config", "--local", "credential.useHttpPath", "true"], { cwd, env });
|
|
267
|
+
runGit(["config", "--local", "credential.interactive", "false"], { cwd, env, allowFailure: true });
|
|
171
268
|
}
|
|
172
269
|
function configureSubmoduleIdentities(worktree, name, email, env = process.env) {
|
|
173
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 });
|
|
174
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);
|
|
273
|
+
}
|
|
274
|
+
function configureSubmoduleCredentialHelpers(worktree, projectId, env = process.env) {
|
|
275
|
+
const helper = `!ur git credential-helper --project ${shellSingleQuoteSafe(projectId)}`;
|
|
276
|
+
const result = runGit(["submodule", "foreach", "--recursive", `git config --local --unset-all credential.helper || true; git config --local --add credential.helper ''; git config --local --add credential.helper '${shellSingleQuoteSafe(helper)}'; git config --local credential.useHttpPath true; git config --local credential.interactive false || true`], { cwd: worktree, env, allowFailure: true });
|
|
277
|
+
if (result.status !== 0) console.warn("Warning: could not apply UR git credential helper to all submodules. Run `ur project init <projectId>` after submodules are initialized.");
|
|
175
278
|
}
|
|
176
279
|
function runGit(args, options = { cwd: process.cwd() }) {
|
|
177
280
|
const gitArgs = options.authHint ? ["-c", "credential.helper=", "-c", "credential.useHttpPath=true", ...args] : args;
|