@united-robotics/cli 0.4.4 → 0.4.6

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 +91 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,23 +2,29 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import { chmodSync, 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
- import { basename, join } from "path";
7
+ import { basename, dirname, join, parse } from "path";
8
8
  import { spawnSync } from "child_process";
9
- var configDir = join(homedir(), ".united-robotics");
10
- var configPath = join(configDir, "config.json");
9
+ var configFileName = "config.json";
10
+ var configDirName = ".united-robotics";
11
+ var globalConfigDir = join(homedir(), configDirName);
12
+ var globalConfigPath = join(globalConfigDir, configFileName);
11
13
  var defaultApiUrl = process.env.UR_API_URL ?? "https://united-robotics.rollersoft.com.au";
12
14
  function load() {
13
- try {
14
- return JSON.parse(readFileSync(configPath, "utf8"));
15
- } catch {
16
- return { apiUrl: defaultApiUrl };
15
+ const localPath = findLocalConfigPath(process.cwd());
16
+ for (const path of [process.env.UR_CONFIG, localPath, globalConfigPath].filter(Boolean)) {
17
+ try {
18
+ return JSON.parse(readFileSync(path, "utf8"));
19
+ } catch {
20
+ }
17
21
  }
22
+ return { apiUrl: defaultApiUrl };
18
23
  }
19
- function save(config) {
20
- mkdirSync(configDir, { recursive: true });
21
- writeFileSync(configPath, JSON.stringify(config, null, 2));
24
+ function save(config, opts = {}) {
25
+ const path = opts.global ? globalConfigPath : localConfigPath(process.cwd());
26
+ mkdirSync(dirname(path), { recursive: true });
27
+ writeFileSync(path, JSON.stringify(config, null, 2));
22
28
  }
23
29
  async function api(path, init = {}) {
24
30
  const cfg = load();
@@ -28,10 +34,10 @@ async function api(path, init = {}) {
28
34
  }
29
35
  var program = new Command();
30
36
  program.name("ur").description("United Robotics customer CLI").version("0.1.0");
31
- program.command("login").requiredOption("--token <token>").option("--api <url>").action((opts) => {
37
+ program.command("login").requiredOption("--token <token>").option("--api <url>").option("--global", "write ~/.united-robotics/config.json instead of the current workspace").action((opts) => {
32
38
  const cfg = load();
33
- save({ apiUrl: opts.api ?? cfg.apiUrl ?? defaultApiUrl, token: opts.token });
34
- console.log("Logged in to United Robotics");
39
+ save({ apiUrl: opts.api ?? cfg.apiUrl ?? defaultApiUrl, token: opts.token }, { global: opts.global });
40
+ console.log(`Logged in to United Robotics (${opts.global ? "global" : "workspace"} config)`);
35
41
  });
36
42
  program.command("whoami").action(async () => console.log(JSON.stringify(await api("/api/me"), null, 2)));
37
43
  program.command("team").argument("<cmd>").action(async (cmd) => {
@@ -50,10 +56,19 @@ project.command("clone").argument("<projectId>").option("--dest <path>").option(
50
56
  try {
51
57
  if (opts.inPlace && opts.dest) throw new Error("Use either --in-place or --dest, not both.");
52
58
  const dest = opts.inPlace ? "." : opts.dest;
53
- if (opts.inPlace) assertInPlaceCloneTarget(process.cwd());
54
- const cloneArgs = ["clone", primary.url];
55
- if (dest) cloneArgs.push(dest);
56
- runGit(cloneArgs, { cwd: process.cwd(), env: credentials.env, authHint: true });
59
+ const preservedConfig = opts.inPlace ? preserveInPlaceWorkspaceConfig(process.cwd()) : null;
60
+ try {
61
+ if (opts.inPlace && isEmptyGitWorktree(process.cwd())) {
62
+ checkoutPrimaryIntoExistingGitWorktree(primary, opts.name, opts.email, credentials.env);
63
+ } else {
64
+ if (opts.inPlace) assertInPlaceCloneTarget(process.cwd());
65
+ const cloneArgs = ["clone", primary.url];
66
+ if (dest) cloneArgs.push(dest);
67
+ runGit(cloneArgs, { cwd: process.cwd(), env: credentials.env, authHint: true });
68
+ }
69
+ } finally {
70
+ preservedConfig?.restore();
71
+ }
57
72
  const worktree = dest ?? repoDirName(primary.url);
58
73
  configureGitIdentity(worktree, opts.name, opts.email, credentials.env);
59
74
  if (opts.submodules) initProjectWorktree(worktree, opts.name, opts.email, credentials.env);
@@ -174,11 +189,67 @@ ${result.stderr}`)) {
174
189
  return { status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
175
190
  }
176
191
  function assertInPlaceCloneTarget(cwd) {
177
- const entries = readdirSync(cwd).filter((entry) => ![".DS_Store"].includes(entry));
192
+ const entries = inPlaceRelevantEntries(cwd);
178
193
  if (entries.length > 0) {
179
- 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.`);
194
+ 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.`);
180
195
  }
181
196
  }
197
+ function isEmptyGitWorktree(cwd) {
198
+ const entries = inPlaceRelevantEntries(cwd);
199
+ return entries.length === 1 && entries[0] === ".git";
200
+ }
201
+ function inPlaceRelevantEntries(cwd) {
202
+ return readdirSync(cwd).filter((entry) => ![".DS_Store", configDirName].includes(entry));
203
+ }
204
+ function checkoutPrimaryIntoExistingGitWorktree(repo, name, email, env) {
205
+ const branch = repo.defaultBranch || "main";
206
+ configureGitIdentity(process.cwd(), name, email, env);
207
+ if (gitRemoteExists(process.cwd(), "origin")) runGit(["remote", "set-url", "origin", repo.url], { cwd: process.cwd(), env });
208
+ else runGit(["remote", "add", "origin", repo.url], { cwd: process.cwd(), env });
209
+ runGit(["fetch", "origin", branch], { cwd: process.cwd(), env, authHint: true });
210
+ runGit(["checkout", "-B", branch, `origin/${branch}`], { cwd: process.cwd(), env });
211
+ }
212
+ function localConfigPath(cwd) {
213
+ return join(cwd, configDirName, configFileName);
214
+ }
215
+ function findLocalConfigPath(start) {
216
+ let dir = start;
217
+ const root = parse(start).root;
218
+ while (true) {
219
+ const candidate = localConfigPath(dir);
220
+ if (existsSync(candidate)) return candidate;
221
+ if (dir === root) return null;
222
+ dir = dirname(dir);
223
+ }
224
+ }
225
+ function preserveInPlaceWorkspaceConfig(cwd) {
226
+ const configDir = join(cwd, configDirName);
227
+ const configPath = join(configDir, configFileName);
228
+ if (!existsSync(configPath)) return null;
229
+ const content = readFileSync(configPath, "utf8");
230
+ rmSync(configDir, { recursive: true, force: true });
231
+ return {
232
+ restore: () => {
233
+ mkdirSync(configDir, { recursive: true });
234
+ writeFileSync(configPath, content);
235
+ ignoreWorkspaceConfigInGit(cwd);
236
+ }
237
+ };
238
+ }
239
+ function gitRemoteExists(cwd, remote) {
240
+ const result = spawnSync(gitBin(), ["remote", "get-url", remote], { cwd, encoding: "utf8", stdio: "ignore" });
241
+ return result.status === 0;
242
+ }
243
+ function ignoreWorkspaceConfigInGit(cwd) {
244
+ const gitDir = join(cwd, ".git");
245
+ if (!existsSync(gitDir)) return;
246
+ const excludePath = join(gitDir, "info", "exclude");
247
+ mkdirSync(dirname(excludePath), { recursive: true });
248
+ const existing = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
249
+ if (!existing.split(/\r?\n/).includes(`${configDirName}/`)) appendFileSync(excludePath, `
250
+ ${configDirName}/
251
+ `);
252
+ }
182
253
  function gitBin() {
183
254
  return process.env.UR_GIT_BIN || (process.platform === "darwin" ? "/usr/bin/git" : "git");
184
255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@united-robotics/cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ur": "dist/index.js"