@united-robotics/cli 0.4.5 → 0.4.7
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 +83 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
5
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, appendFileSync } from "fs";
|
|
6
6
|
import { homedir, tmpdir } from "os";
|
|
7
7
|
import { basename, dirname, join, parse } from "path";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
@@ -26,6 +26,17 @@ function save(config, opts = {}) {
|
|
|
26
26
|
mkdirSync(dirname(path), { recursive: true });
|
|
27
27
|
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
28
28
|
}
|
|
29
|
+
function rememberDefaultProject(projectId, cwd) {
|
|
30
|
+
const currentCwd = process.cwd();
|
|
31
|
+
try {
|
|
32
|
+
process.chdir(cwd);
|
|
33
|
+
const cfg = load();
|
|
34
|
+
save({ ...cfg, defaultProjectId: projectId });
|
|
35
|
+
ignoreWorkspaceConfigInGit(cwd);
|
|
36
|
+
} finally {
|
|
37
|
+
process.chdir(currentCwd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
29
40
|
async function api(path, init = {}) {
|
|
30
41
|
const cfg = load();
|
|
31
42
|
const res = await fetch(`${cfg.apiUrl}${path}`, { ...init, headers: { authorization: `Bearer ${cfg.token ?? ""}`, "content-type": "application/json", ...init.headers ?? {} } });
|
|
@@ -58,16 +69,22 @@ project.command("clone").argument("<projectId>").option("--dest <path>").option(
|
|
|
58
69
|
const dest = opts.inPlace ? "." : opts.dest;
|
|
59
70
|
const preservedConfig = opts.inPlace ? preserveInPlaceWorkspaceConfig(process.cwd()) : null;
|
|
60
71
|
try {
|
|
61
|
-
if (opts.inPlace
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
if (opts.inPlace && isEmptyGitWorktree(process.cwd())) {
|
|
73
|
+
checkoutPrimaryIntoExistingGitWorktree(primary, opts.name, opts.email, credentials.env);
|
|
74
|
+
} else {
|
|
75
|
+
if (opts.inPlace) assertInPlaceCloneTarget(process.cwd());
|
|
76
|
+
const cloneArgs = ["clone", primary.url];
|
|
77
|
+
if (dest) cloneArgs.push(dest);
|
|
78
|
+
runGit(cloneArgs, { cwd: process.cwd(), env: credentials.env, authHint: true });
|
|
79
|
+
}
|
|
65
80
|
} finally {
|
|
66
81
|
preservedConfig?.restore();
|
|
67
82
|
}
|
|
68
83
|
const worktree = dest ?? repoDirName(primary.url);
|
|
84
|
+
rememberDefaultProject(projectId, worktree);
|
|
69
85
|
configureGitIdentity(worktree, opts.name, opts.email, credentials.env);
|
|
70
|
-
if (opts.submodules) initProjectWorktree(worktree, opts.name, opts.email, credentials.env);
|
|
86
|
+
if (opts.submodules) initProjectWorktree(worktree, projectId, opts.name, opts.email, credentials.env);
|
|
87
|
+
else configureGitCredentialHelper(worktree, projectId, credentials.env);
|
|
71
88
|
printCloneSuccess(projectId, worktree, token);
|
|
72
89
|
} finally {
|
|
73
90
|
credentials.cleanup();
|
|
@@ -77,7 +94,8 @@ project.command("init").argument("<projectId>").option("--cwd <path>", "workspac
|
|
|
77
94
|
const token = await projectGithubToken(projectId);
|
|
78
95
|
const credentials = createGitAskpass(token.token);
|
|
79
96
|
try {
|
|
80
|
-
|
|
97
|
+
rememberDefaultProject(projectId, opts.cwd);
|
|
98
|
+
initProjectWorktree(opts.cwd, projectId, opts.name, opts.email, credentials.env);
|
|
81
99
|
console.log(`Initialized ${projectId} workspace at ${opts.cwd}`);
|
|
82
100
|
printDetachedHeadNotice();
|
|
83
101
|
} finally {
|
|
@@ -99,6 +117,16 @@ git.command("identity").option("--name <name>", "Git user.name", "exis[ai]").opt
|
|
|
99
117
|
}
|
|
100
118
|
console.log(`Configured git committer identity: ${opts.name} <${opts.email}>`);
|
|
101
119
|
});
|
|
120
|
+
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) => {
|
|
121
|
+
if (operation && operation !== "get") return;
|
|
122
|
+
const projectId = opts.project ?? load().defaultProjectId;
|
|
123
|
+
if (!projectId) throw new Error("Missing project id for UR git credential helper. Run `ur project init <projectId>` from the workspace root.");
|
|
124
|
+
const result = await projectGithubToken(projectId);
|
|
125
|
+
process.stdout.write(`username=x-access-token
|
|
126
|
+
password=${result.token}
|
|
127
|
+
|
|
128
|
+
`);
|
|
129
|
+
});
|
|
102
130
|
var github = program.command("github");
|
|
103
131
|
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) => {
|
|
104
132
|
const result = await api(`/api/projects/${opts.project}/github-token`, {
|
|
@@ -154,21 +182,36 @@ esac
|
|
|
154
182
|
cleanup: () => rmSync(dir, { recursive: true, force: true })
|
|
155
183
|
};
|
|
156
184
|
}
|
|
157
|
-
function initProjectWorktree(worktree, name, email, env = process.env) {
|
|
185
|
+
function initProjectWorktree(worktree, projectId, name, email, env = process.env) {
|
|
158
186
|
configureGitIdentity(worktree, name, email, env);
|
|
159
187
|
runGit(["submodule", "sync", "--recursive"], { cwd: worktree, env, authHint: true, allowFailure: true });
|
|
160
188
|
runGit(["submodule", "update", "--init", "--recursive"], { cwd: worktree, env, authHint: true });
|
|
189
|
+
configureGitCredentialHelper(worktree, projectId, env);
|
|
161
190
|
configureSubmoduleIdentities(worktree, name, email, env);
|
|
191
|
+
configureSubmoduleCredentialHelpers(worktree, projectId, env);
|
|
162
192
|
printDetachedHeadNotice();
|
|
163
193
|
}
|
|
164
194
|
function configureGitIdentity(cwd, name, email, env = process.env) {
|
|
165
195
|
runGit(["config", "--local", "user.name", name], { cwd, env });
|
|
166
196
|
runGit(["config", "--local", "user.email", email], { cwd, env });
|
|
167
197
|
}
|
|
198
|
+
function configureGitCredentialHelper(cwd, projectId, env = process.env) {
|
|
199
|
+
const helper = `!ur git credential-helper --project ${shellSingleQuoteSafe(projectId)}`;
|
|
200
|
+
runGit(["config", "--local", "--unset-all", "credential.helper"], { cwd, env, allowFailure: true });
|
|
201
|
+
runGit(["config", "--local", "--add", "credential.helper", ""], { cwd, env });
|
|
202
|
+
runGit(["config", "--local", "--add", "credential.helper", helper], { cwd, env });
|
|
203
|
+
runGit(["config", "--local", "credential.useHttpPath", "true"], { cwd, env });
|
|
204
|
+
runGit(["config", "--local", "credential.interactive", "false"], { cwd, env, allowFailure: true });
|
|
205
|
+
}
|
|
168
206
|
function configureSubmoduleIdentities(worktree, name, email, env = process.env) {
|
|
169
207
|
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 });
|
|
170
208
|
if (result.status !== 0) console.warn("Warning: could not apply git identity to all submodules. Run `ur git identity --recursive` after submodules are initialized.");
|
|
171
209
|
}
|
|
210
|
+
function configureSubmoduleCredentialHelpers(worktree, projectId, env = process.env) {
|
|
211
|
+
const helper = `!ur git credential-helper --project ${shellSingleQuoteSafe(projectId)}`;
|
|
212
|
+
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 });
|
|
213
|
+
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.");
|
|
214
|
+
}
|
|
172
215
|
function runGit(args, options = { cwd: process.cwd() }) {
|
|
173
216
|
const gitArgs = options.authHint ? ["-c", "credential.helper=", "-c", "credential.useHttpPath=true", ...args] : args;
|
|
174
217
|
const result = spawnSync(gitBin(), gitArgs, { cwd: options.cwd, env: options.env ?? process.env, encoding: "utf8" });
|
|
@@ -185,11 +228,26 @@ ${result.stderr}`)) {
|
|
|
185
228
|
return { status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
186
229
|
}
|
|
187
230
|
function assertInPlaceCloneTarget(cwd) {
|
|
188
|
-
const entries =
|
|
231
|
+
const entries = inPlaceRelevantEntries(cwd);
|
|
189
232
|
if (entries.length > 0) {
|
|
190
|
-
throw new Error(`Cannot in-place clone into a non-empty directory (${cwd}). Start from an empty Codex project root, or use --dest <path> intentionally.`);
|
|
233
|
+
throw new Error(`Cannot in-place clone into a non-empty directory (${cwd}). Start from an empty Codex project root, use an empty Git repo root, or use --dest <path> intentionally.`);
|
|
191
234
|
}
|
|
192
235
|
}
|
|
236
|
+
function isEmptyGitWorktree(cwd) {
|
|
237
|
+
const entries = inPlaceRelevantEntries(cwd);
|
|
238
|
+
return entries.length === 1 && entries[0] === ".git";
|
|
239
|
+
}
|
|
240
|
+
function inPlaceRelevantEntries(cwd) {
|
|
241
|
+
return readdirSync(cwd).filter((entry) => ![".DS_Store", configDirName].includes(entry));
|
|
242
|
+
}
|
|
243
|
+
function checkoutPrimaryIntoExistingGitWorktree(repo, name, email, env) {
|
|
244
|
+
const branch = repo.defaultBranch || "main";
|
|
245
|
+
configureGitIdentity(process.cwd(), name, email, env);
|
|
246
|
+
if (gitRemoteExists(process.cwd(), "origin")) runGit(["remote", "set-url", "origin", repo.url], { cwd: process.cwd(), env });
|
|
247
|
+
else runGit(["remote", "add", "origin", repo.url], { cwd: process.cwd(), env });
|
|
248
|
+
runGit(["fetch", "origin", branch], { cwd: process.cwd(), env, authHint: true });
|
|
249
|
+
runGit(["checkout", "-B", branch, `origin/${branch}`], { cwd: process.cwd(), env });
|
|
250
|
+
}
|
|
193
251
|
function localConfigPath(cwd) {
|
|
194
252
|
return join(cwd, configDirName, configFileName);
|
|
195
253
|
}
|
|
@@ -213,9 +271,24 @@ function preserveInPlaceWorkspaceConfig(cwd) {
|
|
|
213
271
|
restore: () => {
|
|
214
272
|
mkdirSync(configDir, { recursive: true });
|
|
215
273
|
writeFileSync(configPath, content);
|
|
274
|
+
ignoreWorkspaceConfigInGit(cwd);
|
|
216
275
|
}
|
|
217
276
|
};
|
|
218
277
|
}
|
|
278
|
+
function gitRemoteExists(cwd, remote) {
|
|
279
|
+
const result = spawnSync(gitBin(), ["remote", "get-url", remote], { cwd, encoding: "utf8", stdio: "ignore" });
|
|
280
|
+
return result.status === 0;
|
|
281
|
+
}
|
|
282
|
+
function ignoreWorkspaceConfigInGit(cwd) {
|
|
283
|
+
const gitDir = join(cwd, ".git");
|
|
284
|
+
if (!existsSync(gitDir)) return;
|
|
285
|
+
const excludePath = join(gitDir, "info", "exclude");
|
|
286
|
+
mkdirSync(dirname(excludePath), { recursive: true });
|
|
287
|
+
const existing = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
|
|
288
|
+
if (!existing.split(/\r?\n/).includes(`${configDirName}/`)) appendFileSync(excludePath, `
|
|
289
|
+
${configDirName}/
|
|
290
|
+
`);
|
|
291
|
+
}
|
|
219
292
|
function gitBin() {
|
|
220
293
|
return process.env.UR_GIT_BIN || (process.platform === "darwin" ? "/usr/bin/git" : "git");
|
|
221
294
|
}
|