@united-robotics/cli 0.4.0 → 0.4.2
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 +129 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
6
|
-
import { homedir } from "os";
|
|
7
|
-
import { join } from "path";
|
|
5
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6
|
+
import { homedir, tmpdir } from "os";
|
|
7
|
+
import { basename, join } from "path";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
9
|
var configDir = join(homedir(), ".united-robotics");
|
|
10
10
|
var configPath = join(configDir, "config.json");
|
|
@@ -40,27 +40,59 @@ program.command("team").argument("<cmd>").action(async (cmd) => {
|
|
|
40
40
|
});
|
|
41
41
|
var project = program.command("project");
|
|
42
42
|
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)));
|
|
43
|
-
project.command("show").argument("<projectId>").action(async (projectId) => console.log(JSON.stringify({ projectId, repos: await
|
|
44
|
-
project.command("clone").argument("<projectId>").option("--dest <path>").action(async (projectId, opts) => {
|
|
45
|
-
const repos = await
|
|
43
|
+
project.command("show").argument("<projectId>").action(async (projectId) => console.log(JSON.stringify({ projectId, repos: await projectRepos(projectId) }, null, 2)));
|
|
44
|
+
project.command("clone").argument("<projectId>").option("--dest <path>").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) => {
|
|
45
|
+
const repos = await projectRepos(projectId);
|
|
46
46
|
const primary = repos.find((r) => r.kind === "primary") ?? repos[0];
|
|
47
47
|
if (!primary) throw new Error("No repo found");
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const token = await projectGithubToken(projectId);
|
|
49
|
+
const credentials = createGitAskpass(token.token);
|
|
50
|
+
try {
|
|
51
|
+
const dest = opts.dest;
|
|
52
|
+
const cloneArgs = ["clone", primary.url];
|
|
53
|
+
if (dest) cloneArgs.push(dest);
|
|
54
|
+
runGit(cloneArgs, { cwd: process.cwd(), env: credentials.env, authHint: true });
|
|
55
|
+
const worktree = dest ?? repoDirName(primary.url);
|
|
56
|
+
configureGitIdentity(worktree, opts.name, opts.email, credentials.env);
|
|
57
|
+
if (opts.submodules) initProjectWorktree(worktree, opts.name, opts.email, credentials.env);
|
|
58
|
+
printCloneSuccess(projectId, worktree, token);
|
|
59
|
+
} finally {
|
|
60
|
+
credentials.cleanup();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
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) => {
|
|
64
|
+
const token = await projectGithubToken(projectId);
|
|
65
|
+
const credentials = createGitAskpass(token.token);
|
|
66
|
+
try {
|
|
67
|
+
initProjectWorktree(opts.cwd, opts.name, opts.email, credentials.env);
|
|
68
|
+
console.log(`Initialized ${projectId} workspace at ${opts.cwd}`);
|
|
69
|
+
printDetachedHeadNotice();
|
|
70
|
+
} finally {
|
|
71
|
+
credentials.cleanup();
|
|
72
|
+
}
|
|
52
73
|
});
|
|
53
74
|
program.command("repo").argument("<cmd>").argument("<projectId>").action(async (cmd, projectId) => {
|
|
54
75
|
if (cmd !== "ls") throw new Error("supported: ur repo ls <projectId>");
|
|
55
|
-
console.log(JSON.stringify(await
|
|
76
|
+
console.log(JSON.stringify(await projectRepos(projectId), null, 2));
|
|
77
|
+
});
|
|
78
|
+
var git = program.command("git").description("Git helpers for tenant workspaces");
|
|
79
|
+
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) => {
|
|
80
|
+
if (opts.global) {
|
|
81
|
+
runGit(["config", "--global", "user.name", opts.name], { cwd: process.cwd() });
|
|
82
|
+
runGit(["config", "--global", "user.email", opts.email], { cwd: process.cwd() });
|
|
83
|
+
} else {
|
|
84
|
+
configureGitIdentity(process.cwd(), opts.name, opts.email);
|
|
85
|
+
if (opts.recursive) configureSubmoduleIdentities(process.cwd(), opts.name, opts.email);
|
|
86
|
+
}
|
|
87
|
+
console.log(`Configured git committer identity: ${opts.name} <${opts.email}>`);
|
|
56
88
|
});
|
|
57
89
|
var github = program.command("github");
|
|
58
|
-
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").action(async (opts) => {
|
|
90
|
+
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) => {
|
|
59
91
|
const result = await api(`/api/projects/${opts.project}/github-token`, {
|
|
60
92
|
method: "POST",
|
|
61
93
|
body: JSON.stringify(opts.repo ? { repo: opts.repo } : {})
|
|
62
94
|
});
|
|
63
|
-
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
95
|
+
if (opts.json) console.log(JSON.stringify(opts.redact ? redactTokenResponse(result) : result, null, 2));
|
|
64
96
|
else console.log(result.token);
|
|
65
97
|
});
|
|
66
98
|
var vercel = program.command("vercel").description("Tenant-scoped Vercel CI/CD status via United Robotics broker");
|
|
@@ -77,9 +109,91 @@ program.command("cicd").description("Alias for tenant-scoped CI/CD deployment st
|
|
|
77
109
|
const params = new URLSearchParams({ limit: opts.limit });
|
|
78
110
|
console.log(JSON.stringify(await api(`/api/projects/${opts.project}/vercel/deployments?${params.toString()}`), null, 2));
|
|
79
111
|
});
|
|
80
|
-
program.command("codex").argument("<cmd>").option("--project <projectId>", "project id", "skinspirit-main").action((cmd, opts) => {
|
|
112
|
+
program.command("codex").argument("<cmd>").option("--project <projectId>", "project id", "skinspirit-main").option("--include-token", "include the currently stored PAT in the generated prompt").action((cmd, opts) => {
|
|
81
113
|
if (cmd !== "onboard") throw new Error("supported: ur codex onboard");
|
|
82
114
|
const cfg = load();
|
|
83
|
-
|
|
115
|
+
const loginToken = opts.includeToken ? cfg.token ?? "<TOKEN>" : "<TOKEN>";
|
|
116
|
+
console.log(`Establish new agent workspace by globally installing \`@united-robotics/cli\` module (ensuring there is a working node, nvm and git) and authenticate using token "ur login --token ${loginToken}". Then run \`ur project ls\` and \`ur project clone ${opts.project}\`; clone now internally obtains the short-lived project-wide GitHub token, initializes private submodules, and applies the Vercel-compatible git identity. When GitHub write access is needed manually, use \`ur github token --project ${opts.project}\` for a short-lived GitHub App token, but avoid printing that token in logs unless necessary. Use that project as codex project base from now on, and familiarize yourself.`);
|
|
84
117
|
});
|
|
85
118
|
program.parseAsync();
|
|
119
|
+
async function projectRepos(projectId) {
|
|
120
|
+
return await api(`/api/projects/${projectId}/repos`);
|
|
121
|
+
}
|
|
122
|
+
async function projectGithubToken(projectId) {
|
|
123
|
+
return await api(`/api/projects/${projectId}/github-token`, { method: "POST", body: JSON.stringify({}) });
|
|
124
|
+
}
|
|
125
|
+
function createGitAskpass(token) {
|
|
126
|
+
const dir = mkdtempSync(join(tmpdir(), "ur-git-credentials-"));
|
|
127
|
+
const askpass = join(dir, "askpass.sh");
|
|
128
|
+
writeFileSync(askpass, `#!/bin/sh
|
|
129
|
+
case "$1" in
|
|
130
|
+
*Username*) printf '%s\\n' 'x-access-token' ;;
|
|
131
|
+
*Password*) printf '%s\\n' '${shellSingleQuoteSafe(token)}' ;;
|
|
132
|
+
*) printf '\\n' ;;
|
|
133
|
+
esac
|
|
134
|
+
`, { mode: 448 });
|
|
135
|
+
chmodSync(askpass, 448);
|
|
136
|
+
return {
|
|
137
|
+
env: { ...process.env, GIT_ASKPASS: askpass, GIT_TERMINAL_PROMPT: "0" },
|
|
138
|
+
cleanup: () => rmSync(dir, { recursive: true, force: true })
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function initProjectWorktree(worktree, name, email, env = process.env) {
|
|
142
|
+
configureGitIdentity(worktree, name, email, env);
|
|
143
|
+
runGit(["submodule", "sync", "--recursive"], { cwd: worktree, env, authHint: true, allowFailure: true });
|
|
144
|
+
runGit(["submodule", "update", "--init", "--recursive"], { cwd: worktree, env, authHint: true });
|
|
145
|
+
configureSubmoduleIdentities(worktree, name, email, env);
|
|
146
|
+
printDetachedHeadNotice();
|
|
147
|
+
}
|
|
148
|
+
function configureGitIdentity(cwd, name, email, env = process.env) {
|
|
149
|
+
runGit(["config", "--local", "user.name", name], { cwd, env });
|
|
150
|
+
runGit(["config", "--local", "user.email", email], { cwd, env });
|
|
151
|
+
}
|
|
152
|
+
function configureSubmoduleIdentities(worktree, name, email, env = process.env) {
|
|
153
|
+
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 });
|
|
154
|
+
if (result.status !== 0) console.warn("Warning: could not apply git identity to all submodules. Run `ur git identity --recursive` after submodules are initialized.");
|
|
155
|
+
}
|
|
156
|
+
function runGit(args, options = { cwd: process.cwd() }) {
|
|
157
|
+
const gitArgs = options.authHint ? ["-c", "credential.helper=", "-c", "credential.useHttpPath=true", ...args] : args;
|
|
158
|
+
const result = spawnSync(gitBin(), gitArgs, { cwd: options.cwd, env: options.env ?? process.env, encoding: "utf8" });
|
|
159
|
+
if (result.stdout) process.stdout.write(redactSecrets(result.stdout));
|
|
160
|
+
if (result.stderr) process.stderr.write(redactSecrets(result.stderr));
|
|
161
|
+
const status = result.status ?? 1;
|
|
162
|
+
if (status !== 0 && !options.allowFailure) {
|
|
163
|
+
if (options.authHint && /repository not found|authentication failed|could not read username|terminal prompts disabled/i.test(`${result.stdout}
|
|
164
|
+
${result.stderr}`)) {
|
|
165
|
+
throw new Error("GitHub authentication failed while accessing a private repo. The repo may exist, but GitHub credentials were unavailable or rejected. Re-run `ur login`, then `ur project clone <projectId>` so the CLI can inject a short-lived GitHub App token internally.");
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`git ${safeArgs(args)} failed with exit ${status}`);
|
|
168
|
+
}
|
|
169
|
+
return { status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
170
|
+
}
|
|
171
|
+
function gitBin() {
|
|
172
|
+
return process.env.UR_GIT_BIN || (process.platform === "darwin" ? "/usr/bin/git" : "git");
|
|
173
|
+
}
|
|
174
|
+
function repoDirName(url) {
|
|
175
|
+
return basename(url.replace(/\.git$/, ""));
|
|
176
|
+
}
|
|
177
|
+
function printCloneSuccess(projectId, worktree, token) {
|
|
178
|
+
const repoList = token.repos?.length ? ` (${token.repos.join(", ")})` : "";
|
|
179
|
+
console.log(`Cloned ${projectId} to ${worktree} using an internal short-lived GitHub App token${repoList}.`);
|
|
180
|
+
}
|
|
181
|
+
function printDetachedHeadNotice() {
|
|
182
|
+
console.log("Note: initialized submodules may be in detached HEAD state. Before editing a submodule repo, run `git switch <branch>` inside that submodule.");
|
|
183
|
+
}
|
|
184
|
+
function redactTokenResponse(result) {
|
|
185
|
+
return { ...result, token: redactToken(result.token) };
|
|
186
|
+
}
|
|
187
|
+
function redactSecrets(text) {
|
|
188
|
+
return text.replace(/ghs_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]").replace(/pat_[A-Za-z0-9_-]+/g, "[REDACTED_PAT]");
|
|
189
|
+
}
|
|
190
|
+
function redactToken(token) {
|
|
191
|
+
if (!token) return "";
|
|
192
|
+
return `${token.slice(0, 8)}\u2026redacted`;
|
|
193
|
+
}
|
|
194
|
+
function safeArgs(args) {
|
|
195
|
+
return args.map((arg) => /token|password|authorization/i.test(arg) ? "***" : JSON.stringify(arg)).join(" ");
|
|
196
|
+
}
|
|
197
|
+
function shellSingleQuoteSafe(value) {
|
|
198
|
+
return String(value).replace(/'/g, `'"'"'`);
|
|
199
|
+
}
|