@rness/cli 0.5.2 → 0.6.0

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.
@@ -725,7 +725,7 @@ function classifyProbe(url, exitCode, stderr) {
725
725
  var STDERR_FLUSH_MS = 200;
726
726
  async function probeRemote(url, timeoutMs = 6e4, credentials) {
727
727
  positional(url, "repository url");
728
- return new Promise((resolve4) => {
728
+ return new Promise((resolve5) => {
729
729
  const auth = withCredentials(credentials);
730
730
  const child = spawn(
731
731
  "git",
@@ -744,7 +744,7 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
744
744
  settled = true;
745
745
  clearTimeout(timer);
746
746
  child.stderr?.destroy();
747
- resolve4(probe);
747
+ resolve5(probe);
748
748
  };
749
749
  const timer = setTimeout(() => {
750
750
  child.kill("SIGKILL");
@@ -814,13 +814,8 @@ function readOrg(value) {
814
814
  }
815
815
  return value;
816
816
  }
817
- function resolveOrg(manifest, root) {
818
- if (manifest.org !== null) return { org: manifest.org, warning: null };
819
- const name = basename2(root);
820
- return {
821
- org: name,
822
- warning: `rness.json: no "org"; using the directory name "${name}"`
823
- };
817
+ function workspaceName(manifest, root) {
818
+ return manifest.org ?? basename2(root);
824
819
  }
825
820
  function readRepos(value) {
826
821
  const raw = value ?? {};
@@ -915,8 +910,13 @@ function parseRepoSpec(spec, org, host) {
915
910
  url = spec;
916
911
  } else {
917
912
  const parts = spec.split("/");
918
- if (parts.length === 1) url = repoUrl(host, org, spec);
919
- else if (parts.length === 2 && parts[0] !== void 0 && parts[1] !== void 0)
913
+ if (parts.length === 1) {
914
+ if (org === null)
915
+ throw new Error(
916
+ "this workspace has no GitHub organization: give <owner>/<repo> or a git URL"
917
+ );
918
+ url = repoUrl(host, org, spec);
919
+ } else if (parts.length === 2 && parts[0] !== void 0 && parts[1] !== void 0)
920
920
  url = repoUrl(host, parts[0], parts[1]);
921
921
  else
922
922
  throw new Error(
@@ -1162,7 +1162,7 @@ async function detectGithubSsh(opts) {
1162
1162
  ...opts.interactive ? [] : ["-o", "BatchMode=yes"],
1163
1163
  "git@github.com"
1164
1164
  ];
1165
- return new Promise((resolve4) => {
1165
+ return new Promise((resolve5) => {
1166
1166
  const child = spawn2(opts.command ?? "ssh", args, {
1167
1167
  stdio: [opts.interactive ? "inherit" : "ignore", "ignore", "pipe"]
1168
1168
  });
@@ -1175,7 +1175,7 @@ async function detectGithubSsh(opts) {
1175
1175
  settled = true;
1176
1176
  clearTimeout(timer);
1177
1177
  child.stderr?.destroy();
1178
- resolve4(access2);
1178
+ resolve5(access2);
1179
1179
  };
1180
1180
  const timer = setTimeout(() => {
1181
1181
  child.kill("SIGKILL");
@@ -1828,9 +1828,8 @@ async function syncCommand(opts, deps = {}) {
1828
1828
  try {
1829
1829
  const ws = await findWorkspace(cwd);
1830
1830
  const manifest = await loadManifest(ws.rnessDir);
1831
- const { org, warning } = resolveOrg(manifest, ws.root);
1831
+ const org = workspaceName(manifest, ws.root);
1832
1832
  if (ownUi) ui.intro(`Sync workspace ${org}`);
1833
- if (warning !== null) ui.warn(warning);
1834
1833
  if (opts.scope !== void 0 && !Object.hasOwn(manifest.scopes, opts.scope)) {
1835
1834
  process.stderr.write(`unknown scope: ${opts.scope}
1836
1835
  `);
@@ -2035,9 +2034,7 @@ async function addCommand(spec, opts, deps = {}) {
2035
2034
  try {
2036
2035
  const ws = await findWorkspace(cwd);
2037
2036
  const manifest = await loadManifest(ws.rnessDir);
2038
- const { org, warning } = resolveOrg(manifest, ws.root);
2039
- if (warning !== null) process.stderr.write(`${warn(warning)}
2040
- `);
2037
+ const org = manifest.org;
2041
2038
  let parsed;
2042
2039
  try {
2043
2040
  parsed = parseRepoSpec(spec, org, transport.hosts.https);
@@ -2062,7 +2059,10 @@ async function addCommand(spec, opts, deps = {}) {
2062
2059
  return 2;
2063
2060
  }
2064
2061
  const ui = deps.ui ?? (interactive ? await makeUi(terminal) : { ...plainUi });
2065
- if (ownUi) ui.intro(`Add ${parsed.name} to workspace ${org}`);
2062
+ if (ownUi)
2063
+ ui.intro(
2064
+ `Add ${parsed.name} to workspace ${workspaceName(manifest, ws.root)}`
2065
+ );
2066
2066
  if (opts.host !== void 0 || opts.https === true) ui.gitIsSilent = true;
2067
2067
  let host = opts.host;
2068
2068
  if (host === void 0 && opts.ssh === true) host = transport.hosts.ssh;
@@ -2229,6 +2229,9 @@ async function contextCommand(opts) {
2229
2229
  }
2230
2230
  }
2231
2231
 
2232
+ // src/version.ts
2233
+ var VERSION = "0.6.0";
2234
+
2232
2235
  // src/core/delegate.ts
2233
2236
  import { readFile as readFile6 } from "fs/promises";
2234
2237
  import { join as join11 } from "path";
@@ -2304,6 +2307,17 @@ async function packageManagerVersion(pm) {
2304
2307
  throw new Error(`${pm} is not available on PATH`, { cause: e });
2305
2308
  }
2306
2309
  }
2310
+ function rnessRunner(pm, version) {
2311
+ if (pm === "pnpm") return "pnpm dlx @rness/cli";
2312
+ if (pm === "bun") return "bunx @rness/cli";
2313
+ if (pm === "yarn" && Number(version?.split(".")[0]) >= 2)
2314
+ return "yarn dlx @rness/cli";
2315
+ return "npx @rness/cli";
2316
+ }
2317
+ async function rnessRunnerFor(pm) {
2318
+ const version = pm === "yarn" ? await packageManagerVersion(pm).catch(() => null) : null;
2319
+ return rnessRunner(pm, version);
2320
+ }
2307
2321
  var BARE_NPM_CODE = /^npm (error|ERR!) code \S+$/;
2308
2322
  function installErrorLine(stderr) {
2309
2323
  const lines = stderr.split("\n").map((l) => l.trim()).filter((l) => l !== "");
@@ -2331,12 +2345,228 @@ async function installDependencies(pm, dir, opts = {}) {
2331
2345
  }
2332
2346
  }
2333
2347
 
2334
- // src/core/pinned.ts
2348
+ // src/core/scaffold.ts
2349
+ import { statSync } from "fs";
2350
+ import { dirname as dirname4, join as join12 } from "path";
2351
+ import { fileURLToPath } from "url";
2352
+ var SCAFFOLD_FILES = [
2353
+ "AGENTS.md",
2354
+ "CONVENTIONS.md",
2355
+ "README.md",
2356
+ "WORKSPACE.md",
2357
+ "rness.json",
2358
+ "package.json",
2359
+ "_gitignore",
2360
+ ".githooks/pre-commit",
2361
+ ".github/workflows/validate.yml",
2362
+ ".github/dependabot.yml",
2363
+ "pnpm-workspace.yaml",
2364
+ "adr/0000-template.md",
2365
+ "standards/.gitkeep",
2366
+ "specs/.gitkeep",
2367
+ "plans/.gitkeep",
2368
+ "skills/.gitkeep",
2369
+ "docs/.gitkeep"
2370
+ ];
2371
+ function scaffoldDir() {
2372
+ let dir = dirname4(fileURLToPath(import.meta.url));
2373
+ for (let i = 0; i < 4; i += 1) {
2374
+ const candidate = join12(dir, "scaffold");
2375
+ try {
2376
+ if (statSync(join12(candidate, "rness.json")).isFile()) return candidate;
2377
+ } catch {
2378
+ }
2379
+ dir = dirname4(dir);
2380
+ }
2381
+ throw new Error("scaffold directory not found next to the @rness/cli package");
2382
+ }
2383
+
2384
+ // src/core/scaffold-copy.ts
2385
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile7, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2386
+ import { dirname as dirname5, join as join13 } from "path";
2387
+ var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
2388
+ async function copyScaffold(dest, tokens) {
2389
+ const source = scaffoldDir();
2390
+ for (const rel of SCAFFOLD_FILES) {
2391
+ if (rel === "rness.json") continue;
2392
+ const from = join13(source, ...rel.split("/"));
2393
+ const targetRel = rel === "_gitignore" ? ".gitignore" : rel;
2394
+ const to = join13(dest, ...targetRel.split("/"));
2395
+ await mkdir2(dirname5(to), { recursive: true });
2396
+ let content = await readFile7(from, "utf8");
2397
+ if (TOKENISED.has(rel)) {
2398
+ content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
2399
+ }
2400
+ await writeFile2(to, content, "utf8");
2401
+ const mode = (await stat2(from)).mode & 511;
2402
+ if (mode & 73) await chmod2(to, mode);
2403
+ }
2404
+ }
2405
+
2406
+ // src/commands/login.ts
2407
+ import { spawn as spawn3 } from "child_process";
2408
+ import { realpath } from "fs/promises";
2409
+
2410
+ // src/core/setup-git.ts
2335
2411
  import { execFile as execFile3 } from "child_process";
2336
- import { readFile as readFile7 } from "fs/promises";
2337
- import { basename as basename4, join as join12 } from "path";
2338
2412
  import { promisify as promisify3 } from "util";
2339
2413
  var execFileP3 = promisify3(execFile3);
2414
+ var KEY = "credential.https://github.com.helper";
2415
+ function helperValue(node, bin) {
2416
+ return `!'${node}' '${bin}' git-credential`;
2417
+ }
2418
+ var isOurs = (value) => /^!'[^']+' '[^']*rness[^']*' git-credential$/.test(value);
2419
+ async function config(args) {
2420
+ const { stdout } = await execFileP3("git", ["config", "--global", ...args]);
2421
+ return stdout;
2422
+ }
2423
+ async function values() {
2424
+ try {
2425
+ const out = await config(["--get-all", KEY]);
2426
+ return out.replace(/\n$/, "").split("\n");
2427
+ } catch {
2428
+ return [];
2429
+ }
2430
+ }
2431
+ function isStableInstall(bin) {
2432
+ return !/[\\/]_npx[\\/]/.test(bin);
2433
+ }
2434
+ async function setupGit(node, bin) {
2435
+ const ours = helperValue(node, bin);
2436
+ if ((await values()).includes(ours)) return false;
2437
+ await removeGitSetup();
2438
+ await config(["--add", KEY, ""]);
2439
+ await config(["--add", KEY, ours]);
2440
+ return true;
2441
+ }
2442
+ async function removeGitSetup() {
2443
+ const current = await values();
2444
+ if (!current.some(isOurs)) return false;
2445
+ const kept = current.filter(
2446
+ (value, i) => !isOurs(value) && !(value === "" && isOurs(current[i + 1] ?? ""))
2447
+ );
2448
+ await config(["--unset-all", KEY]);
2449
+ for (const value of kept) await config(["--add", KEY, value]);
2450
+ return true;
2451
+ }
2452
+
2453
+ // src/commands/login.ts
2454
+ function openInBrowser(url) {
2455
+ const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
2456
+ try {
2457
+ const child = spawn3(command, args, { stdio: "ignore", detached: true });
2458
+ child.on("error", () => void 0);
2459
+ child.unref();
2460
+ } catch {
2461
+ }
2462
+ }
2463
+ async function offerSetupGit(opts, terminal, bin, ui) {
2464
+ if (opts.setupGit === false) return;
2465
+ if (!isStableInstall(bin)) {
2466
+ ui.hint(
2467
+ "install rness globally to let git use your login: npm i -g @rness/cli"
2468
+ );
2469
+ return;
2470
+ }
2471
+ if (opts.setupGit !== true) {
2472
+ if (!terminal.isTty()) return;
2473
+ const p = await terminal.prompts();
2474
+ const ok = await p.confirm({
2475
+ message: "Use rness to authenticate git over HTTPS for github.com?",
2476
+ initialValue: true
2477
+ });
2478
+ if (p.isCancel(ok) || ok !== true) return;
2479
+ }
2480
+ const changed = await setupGit(process.execPath, bin);
2481
+ ui.line(
2482
+ changed ? "updated" : "unchanged",
2483
+ "git: rness answers for https://github.com (rness logout undoes it)",
2484
+ "git now asks rness for your github.com login over HTTPS (rness logout undoes it)"
2485
+ );
2486
+ }
2487
+ async function loginCommand(opts, deps = {}) {
2488
+ const terminal = deps.terminal ?? defaultTerminal;
2489
+ const own = deps.ui === void 0;
2490
+ const ui = deps.ui ?? await makeUi(terminal);
2491
+ if (own) ui.intro("Log in to GitHub");
2492
+ try {
2493
+ const bin = deps.bin ?? await realpath(process.argv[1] ?? "").catch(() => "");
2494
+ const env = envToken();
2495
+ if (env !== null)
2496
+ ui.hint(`${env.source} is set and wins over a stored login`);
2497
+ const stored = await readAuth();
2498
+ if (stored !== null && await resolveToken() !== null) {
2499
+ ui.line(
2500
+ "logged in",
2501
+ `as ${stored.login} (github.com); rness logout to switch`
2502
+ );
2503
+ await offerSetupGit(opts, terminal, bin, ui);
2504
+ if (own) ui.outro("Nothing to do", "idle");
2505
+ return 0;
2506
+ }
2507
+ const code = await requestDeviceCode();
2508
+ if (ui.session)
2509
+ ui.line(
2510
+ "open",
2511
+ code.verificationUri,
2512
+ `Open ${code.verificationUri} and enter the code ${code.userCode}`
2513
+ );
2514
+ else {
2515
+ ui.line("open", code.verificationUri);
2516
+ ui.line("code", code.userCode);
2517
+ }
2518
+ if (terminal.isTty() && process.env["SSH_CONNECTION"] === void 0)
2519
+ (deps.open ?? openInBrowser)(code.verificationUri);
2520
+ if (!ui.session)
2521
+ ui.line("waiting", "for you to approve rness on github.com\u2026");
2522
+ const login = await ui.step(
2523
+ {
2524
+ doing: "waiting for you to approve rness on github.com",
2525
+ sentence: "Waiting for your approval on github.com",
2526
+ quiet: true
2527
+ },
2528
+ async () => {
2529
+ const tokens = await pollForToken(code, deps);
2530
+ const user = await getUser({
2531
+ token: tokens.accessToken,
2532
+ apiBase: opts.githubApi ?? DEFAULT_GITHUB_API
2533
+ });
2534
+ await writeAuth({ login: user.login, tokens });
2535
+ return user.login;
2536
+ },
2537
+ (name) => ["logged in", `as ${name} (github.com)`]
2538
+ );
2539
+ if (!ui.session) ui.line("logged in", `as ${login} (github.com)`);
2540
+ await offerSetupGit(opts, terminal, bin, ui);
2541
+ if (own) ui.outro(`You are logged in as ${login}`);
2542
+ return 0;
2543
+ } catch (e) {
2544
+ if (e instanceof LoginError) {
2545
+ ui.error(e.message);
2546
+ return 1;
2547
+ }
2548
+ return reportError(e);
2549
+ }
2550
+ }
2551
+ function revokeUrl() {
2552
+ return `https://github.com/settings/connections/applications/${clientId()}`;
2553
+ }
2554
+
2555
+ // src/commands/create.ts
2556
+ import { mkdir as mkdir5, mkdtemp, rename as rename2, rm as rm5 } from "fs/promises";
2557
+ import { tmpdir } from "os";
2558
+ import { join as join17, relative as relative4, resolve as resolve4 } from "path";
2559
+
2560
+ // src/core/new-workspace.ts
2561
+ import { mkdir as mkdir3, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
2562
+ import { dirname as dirname6, join as join15, relative as relative2 } from "path";
2563
+
2564
+ // src/core/pinned.ts
2565
+ import { execFile as execFile4 } from "child_process";
2566
+ import { readFile as readFile8 } from "fs/promises";
2567
+ import { basename as basename4, join as join14 } from "path";
2568
+ import { promisify as promisify4 } from "util";
2569
+ var execFileP4 = promisify4(execFile4);
2340
2570
  var PACKAGE = "@rness/cli";
2341
2571
  var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
2342
2572
  function compareVersions(a, b) {
@@ -2359,7 +2589,7 @@ function compareVersions(a, b) {
2359
2589
  async function readPackageJson(rnessDir) {
2360
2590
  try {
2361
2591
  const parsed = JSON.parse(
2362
- await readFile7(join12(rnessDir, "package.json"), "utf8")
2592
+ await readFile8(join14(rnessDir, "package.json"), "utf8")
2363
2593
  );
2364
2594
  return parsed !== null && typeof parsed === "object" ? parsed : null;
2365
2595
  } catch {
@@ -2378,8 +2608,8 @@ async function readPin(rnessDir) {
2378
2608
  return null;
2379
2609
  }
2380
2610
  async function writePin(rnessDir, from, to) {
2381
- const file = join12(rnessDir, "package.json");
2382
- const text = await readFile7(file, "utf8");
2611
+ const file = join14(rnessDir, "package.json");
2612
+ const text = await readFile8(file, "utf8");
2383
2613
  const entry = new RegExp(
2384
2614
  `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
2385
2615
  );
@@ -2401,12 +2631,12 @@ async function workspacePackageManager(rnessDir) {
2401
2631
  if (isPackageManager(name)) return name;
2402
2632
  }
2403
2633
  for (const [lockfile, pm] of LOCKFILES)
2404
- if (await exists(join12(rnessDir, lockfile))) return pm;
2634
+ if (await exists(join14(rnessDir, lockfile))) return pm;
2405
2635
  return "npm";
2406
2636
  }
2407
2637
  async function hasLockfile(rnessDir, pm) {
2408
2638
  for (const [lockfile, owner] of LOCKFILES)
2409
- if (owner === pm && await exists(join12(rnessDir, lockfile))) return true;
2639
+ if (owner === pm && await exists(join14(rnessDir, lockfile))) return true;
2410
2640
  return false;
2411
2641
  }
2412
2642
  async function pinDrift(rnessDir) {
@@ -2462,9 +2692,9 @@ async function ensureInstalled(rnessDir, opts = {}) {
2462
2692
  }
2463
2693
  }
2464
2694
  async function runPinnedSync(root) {
2465
- const installed = await readPinnedCli(join12(root, ".rness"));
2695
+ const installed = await readPinnedCli(join14(root, ".rness"));
2466
2696
  if (installed === null) return null;
2467
- const pinned = join12(installed.dir, ...installed.bin.split("/"));
2697
+ const pinned = join14(installed.dir, ...installed.bin.split("/"));
2468
2698
  if (!await exists(pinned))
2469
2699
  return {
2470
2700
  code: 1,
@@ -2473,7 +2703,7 @@ async function runPinnedSync(root) {
2473
2703
  `
2474
2704
  };
2475
2705
  try {
2476
- const { stdout, stderr } = await execFileP3(
2706
+ const { stdout, stderr } = await execFileP4(
2477
2707
  process.execPath,
2478
2708
  [pinned, "sync", "--yes"],
2479
2709
  {
@@ -2526,7 +2756,7 @@ function syncOutcome(result, opts = {}) {
2526
2756
  };
2527
2757
  }
2528
2758
  async function syncPinned(root, shown, fallback) {
2529
- const rnessDir = join12(root, ".rness");
2759
+ const rnessDir = join14(root, ".rness");
2530
2760
  const installed = await readPinnedCli(rnessDir);
2531
2761
  if (installed === null) {
2532
2762
  if (fallback !== void 0) return fallback();
@@ -2536,7 +2766,7 @@ async function syncPinned(root, shown, fallback) {
2536
2766
  );
2537
2767
  return 1;
2538
2768
  }
2539
- const pinned = join12(installed.dir, ...installed.bin.split("/"));
2769
+ const pinned = join14(installed.dir, ...installed.bin.split("/"));
2540
2770
  if (!await exists(pinned)) {
2541
2771
  process.stderr.write(
2542
2772
  `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
@@ -2545,7 +2775,7 @@ async function syncPinned(root, shown, fallback) {
2545
2775
  return 1;
2546
2776
  }
2547
2777
  try {
2548
- const { stdout, stderr } = await execFileP3(
2778
+ const { stdout, stderr } = await execFileP4(
2549
2779
  process.execPath,
2550
2780
  [pinned, "sync", "--yes"],
2551
2781
  {
@@ -2569,220 +2799,83 @@ async function syncPinned(root, shown, fallback) {
2569
2799
  }
2570
2800
  }
2571
2801
 
2572
- // src/core/scaffold.ts
2573
- import { statSync } from "fs";
2574
- import { dirname as dirname4, join as join13 } from "path";
2575
- import { fileURLToPath } from "url";
2576
- var SCAFFOLD_FILES = [
2577
- "AGENTS.md",
2578
- "CONVENTIONS.md",
2579
- "README.md",
2580
- "WORKSPACE.md",
2581
- "rness.json",
2582
- "package.json",
2583
- "_gitignore",
2584
- ".githooks/pre-commit",
2585
- ".github/workflows/validate.yml",
2586
- ".github/dependabot.yml",
2587
- "pnpm-workspace.yaml",
2588
- "adr/0000-template.md",
2589
- "standards/.gitkeep",
2590
- "specs/.gitkeep",
2591
- "plans/.gitkeep",
2592
- "skills/.gitkeep",
2593
- "docs/.gitkeep"
2594
- ];
2595
- function scaffoldDir() {
2596
- let dir = dirname4(fileURLToPath(import.meta.url));
2597
- for (let i = 0; i < 4; i += 1) {
2598
- const candidate = join13(dir, "scaffold");
2599
- try {
2600
- if (statSync(join13(candidate, "rness.json")).isFile()) return candidate;
2601
- } catch {
2602
- }
2603
- dir = dirname4(dir);
2802
+ // src/core/new-workspace.ts
2803
+ async function isEmptyDir(dir) {
2804
+ return (await readdir2(dir)).length === 0;
2805
+ }
2806
+ async function insideWorkspace(dir) {
2807
+ try {
2808
+ await findWorkspace(dir);
2809
+ return true;
2810
+ } catch {
2811
+ return false;
2604
2812
  }
2605
- throw new Error("scaffold directory not found next to the @rness/cli package");
2606
2813
  }
2607
-
2608
- // src/core/scaffold-copy.ts
2609
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile8, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2610
- import { dirname as dirname5, join as join14 } from "path";
2611
- var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
2612
- async function copyScaffold(dest, tokens) {
2613
- const source = scaffoldDir();
2614
- for (const rel of SCAFFOLD_FILES) {
2615
- if (rel === "rness.json") continue;
2616
- const from = join14(source, ...rel.split("/"));
2617
- const targetRel = rel === "_gitignore" ? ".gitignore" : rel;
2618
- const to = join14(dest, ...targetRel.split("/"));
2619
- await mkdir2(dirname5(to), { recursive: true });
2620
- let content = await readFile8(from, "utf8");
2621
- if (TOKENISED.has(rel)) {
2622
- content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
2623
- }
2624
- await writeFile2(to, content, "utf8");
2625
- const mode = (await stat2(from)).mode & 511;
2626
- if (mode & 73) await chmod2(to, mode);
2814
+ async function targetProblem(cwd, root, pm) {
2815
+ const shown = relative2(cwd, root) || ".";
2816
+ if (await insideWorkspace(cwd) || await insideWorkspace(dirname6(root)))
2817
+ return "already inside an rness workspace; create runs from outside one";
2818
+ if (!await exists(root)) return null;
2819
+ if (await exists(join15(root, ".rness", "rness.json"))) {
2820
+ if (!await exists(join15(root, ".rness", "node_modules")))
2821
+ return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then rness sync`;
2822
+ return `${shown} already holds an rness workspace (.rness/); run rness add inside it to add repositories`;
2627
2823
  }
2824
+ if (!(await stat3(root)).isDirectory()) return `${shown} is not a directory`;
2825
+ if (!await isEmptyDir(root)) return `${shown} is not empty`;
2826
+ return null;
2628
2827
  }
2629
-
2630
- // src/version.ts
2631
- var VERSION = "0.5.2";
2632
-
2633
- // src/commands/login.ts
2634
- import { spawn as spawn3 } from "child_process";
2635
- import { realpath } from "fs/promises";
2636
-
2637
- // src/core/setup-git.ts
2638
- import { execFile as execFile4 } from "child_process";
2639
- import { promisify as promisify4 } from "util";
2640
- var execFileP4 = promisify4(execFile4);
2641
- var KEY = "credential.https://github.com.helper";
2642
- function helperValue(node, bin) {
2643
- return `!'${node}' '${bin}' git-credential`;
2644
- }
2645
- var isOurs = (value) => /^!'[^']+' '[^']*rness[^']*' git-credential$/.test(value);
2646
- async function config(args) {
2647
- const { stdout } = await execFileP4("git", ["config", "--global", ...args]);
2648
- return stdout;
2649
- }
2650
- async function values() {
2651
- try {
2652
- const out = await config(["--get-all", KEY]);
2653
- return out.replace(/\n$/, "").split("\n");
2654
- } catch {
2655
- return [];
2828
+ async function installContext(input) {
2829
+ const { ui, pm, rnessDir } = input;
2830
+ if (input.skip) {
2831
+ ui.line("skipped", `install with ${pm} (--skip-install)`);
2832
+ return;
2656
2833
  }
2657
- }
2658
- function isStableInstall(bin) {
2659
- return !/[\\/]_npx[\\/]/.test(bin);
2660
- }
2661
- async function setupGit(node, bin) {
2662
- const ours = helperValue(node, bin);
2663
- if ((await values()).includes(ours)) return false;
2664
- await removeGitSetup();
2665
- await config(["--add", KEY, ""]);
2666
- await config(["--add", KEY, ours]);
2667
- return true;
2668
- }
2669
- async function removeGitSetup() {
2670
- const current = await values();
2671
- if (!current.some(isOurs)) return false;
2672
- const kept = current.filter(
2673
- (value, i) => !isOurs(value) && !(value === "" && isOurs(current[i + 1] ?? ""))
2834
+ const frozen = await hasLockfile(rnessDir, pm);
2835
+ await ui.step(
2836
+ { doing: `installing dependencies with ${pm}` },
2837
+ () => installDependencies(pm, rnessDir, { frozen }),
2838
+ () => ["installed", `dependencies with ${pm}`]
2674
2839
  );
2675
- await config(["--unset-all", KEY]);
2676
- for (const value of kept) await config(["--add", KEY, value]);
2677
- return true;
2678
2840
  }
2679
-
2680
- // src/commands/login.ts
2681
- function openInBrowser(url) {
2682
- const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
2841
+ async function buildContext(input) {
2842
+ const { ui, root, shown, pm } = input;
2843
+ const rnessDir = join15(root, ".rness");
2844
+ const pmVersion = await packageManagerVersion(pm);
2845
+ await mkdir3(root, { recursive: true });
2683
2846
  try {
2684
- const child = spawn3(command, args, { stdio: "ignore", detached: true });
2685
- child.on("error", () => void 0);
2686
- child.unref();
2687
- } catch {
2688
- }
2689
- }
2690
- async function offerSetupGit(opts, terminal, bin, ui) {
2691
- if (opts.setupGit === false) return;
2692
- if (!isStableInstall(bin)) {
2693
- ui.hint(
2694
- "install rness globally to let git use your login: npm i -g @rness/cli"
2695
- );
2696
- return;
2697
- }
2698
- if (opts.setupGit !== true) {
2699
- if (!terminal.isTty()) return;
2700
- const p = await terminal.prompts();
2701
- const ok = await p.confirm({
2702
- message: "Use rness to authenticate git over HTTPS for github.com?",
2703
- initialValue: true
2847
+ await copyScaffold(rnessDir, {
2848
+ version: VERSION,
2849
+ packageManager: `${pm}@${pmVersion}`
2704
2850
  });
2705
- if (p.isCancel(ok) || ok !== true) return;
2851
+ await writeManifest(rnessDir, input.manifest);
2852
+ ui.line("created", `${shown}/.rness (${input.kind})`);
2853
+ await installContext({ ui, pm, rnessDir, skip: input.skipInstall });
2854
+ return true;
2855
+ } catch (e) {
2856
+ reportError(e);
2857
+ await rm4(rnessDir, { recursive: true, force: true }).catch(() => void 0);
2858
+ if (await isEmptyDir(root).catch(() => false))
2859
+ await rm4(root, { recursive: true, force: true }).catch(() => void 0);
2860
+ process.stderr.write(
2861
+ `removed ${shown}/.rness after the failure; fix it and retry
2862
+ `
2863
+ );
2864
+ return false;
2706
2865
  }
2707
- const changed = await setupGit(process.execPath, bin);
2708
- ui.line(
2709
- changed ? "updated" : "unchanged",
2710
- "git: rness answers for https://github.com (rness logout undoes it)",
2711
- "git now asks rness for your github.com login over HTTPS (rness logout undoes it)"
2712
- );
2713
2866
  }
2714
- async function loginCommand(opts, deps = {}) {
2715
- const terminal = deps.terminal ?? defaultTerminal;
2716
- const own = deps.ui === void 0;
2717
- const ui = deps.ui ?? await makeUi(terminal);
2718
- if (own) ui.intro("Log in to GitHub");
2867
+ async function commitContext(input) {
2868
+ const { ui, rnessDir, shown } = input;
2869
+ await init(rnessDir);
2719
2870
  try {
2720
- const bin = deps.bin ?? await realpath(process.argv[1] ?? "").catch(() => "");
2721
- const env = envToken();
2722
- if (env !== null)
2723
- ui.hint(`${env.source} is set and wins over a stored login`);
2724
- const stored = await readAuth();
2725
- if (stored !== null && await resolveToken() !== null) {
2726
- ui.line(
2727
- "logged in",
2728
- `as ${stored.login} (github.com); rness logout to switch`
2729
- );
2730
- await offerSetupGit(opts, terminal, bin, ui);
2731
- if (own) ui.outro("Nothing to do", "idle");
2732
- return 0;
2733
- }
2734
- const code = await requestDeviceCode();
2735
- if (ui.session)
2736
- ui.line(
2737
- "open",
2738
- code.verificationUri,
2739
- `Open ${code.verificationUri} and enter the code ${code.userCode}`
2740
- );
2741
- else {
2742
- ui.line("open", code.verificationUri);
2743
- ui.line("code", code.userCode);
2744
- }
2745
- if (terminal.isTty() && process.env["SSH_CONNECTION"] === void 0)
2746
- (deps.open ?? openInBrowser)(code.verificationUri);
2747
- if (!ui.session)
2748
- ui.line("waiting", "for you to approve rness on github.com\u2026");
2749
- const login = await ui.step(
2750
- {
2751
- doing: "waiting for you to approve rness on github.com",
2752
- sentence: "Waiting for your approval on github.com",
2753
- quiet: true
2754
- },
2755
- async () => {
2756
- const tokens = await pollForToken(code, deps);
2757
- const user = await getUser({
2758
- token: tokens.accessToken,
2759
- apiBase: opts.githubApi ?? DEFAULT_GITHUB_API
2760
- });
2761
- await writeAuth({ login: user.login, tokens });
2762
- return user.login;
2763
- },
2764
- (name) => ["logged in", `as ${name} (github.com)`]
2765
- );
2766
- if (!ui.session) ui.line("logged in", `as ${login} (github.com)`);
2767
- await offerSetupGit(opts, terminal, bin, ui);
2768
- if (own) ui.outro(`You are logged in as ${login}`);
2769
- return 0;
2871
+ await commitAll(rnessDir, "chore: rness workspace context");
2872
+ ui.line("committed", `${shown}/.rness`);
2770
2873
  } catch (e) {
2771
- if (e instanceof LoginError) {
2772
- ui.error(e.message);
2773
- return 1;
2774
- }
2775
- return reportError(e);
2874
+ ui.warn(
2875
+ `${e instanceof Error ? e.message : String(e)} \u2014 commit ${shown}/.rness yourself`
2876
+ );
2776
2877
  }
2777
2878
  }
2778
- function revokeUrl() {
2779
- return `https://github.com/settings/connections/applications/${clientId()}`;
2780
- }
2781
-
2782
- // src/commands/create.ts
2783
- import { mkdir as mkdir3, mkdtemp, readdir as readdir2, rename as rename2, rm as rm4, stat as stat3 } from "fs/promises";
2784
- import { tmpdir } from "os";
2785
- import { dirname as dirname6, join as join15, relative as relative2, resolve as resolve3 } from "path";
2786
2879
 
2787
2880
  // src/core/sync-blocks.ts
2788
2881
  async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
@@ -2812,38 +2905,113 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
2812
2905
  return outcome.code;
2813
2906
  }
2814
2907
 
2815
- // src/commands/create.ts
2816
- async function isEmptyDir(dir) {
2817
- return (await readdir2(dir)).length === 0;
2818
- }
2819
- async function insideWorkspace(dir) {
2820
- try {
2821
- await findWorkspace(dir);
2822
- return true;
2823
- } catch {
2824
- return false;
2908
+ // src/commands/create-blank.ts
2909
+ import { mkdir as mkdir4 } from "fs/promises";
2910
+ import { join as join16, relative as relative3, resolve as resolve3 } from "path";
2911
+ var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
2912
+ function workspaceNameError(name) {
2913
+ return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
2914
+ }
2915
+ async function createBlank(input) {
2916
+ const { cwd, pm, ui } = input;
2917
+ let name = input.name;
2918
+ let prompted = false;
2919
+ if (name === void 0) {
2920
+ if (!input.interactive) {
2921
+ process.stderr.write(
2922
+ "rness create --blank needs a <name> without a prompt\n"
2923
+ );
2924
+ return 2;
2925
+ }
2926
+ const p = await input.prompts();
2927
+ const answer = await p.text({
2928
+ message: "What should the workspace be called?",
2929
+ placeholder: "my-workspace",
2930
+ validate: (value) => {
2931
+ const v = value?.trim() ?? "";
2932
+ return WORKSPACE_NAME.test(v) ? void 0 : workspaceNameError(v);
2933
+ }
2934
+ });
2935
+ if (p.isCancel(answer) || typeof answer !== "string") {
2936
+ ui.cancelled();
2937
+ return 0;
2938
+ }
2939
+ name = answer.trim();
2940
+ prompted = true;
2941
+ }
2942
+ const root = resolve3(cwd, name);
2943
+ const shown = relative3(cwd, root) || ".";
2944
+ const problem = await targetProblem(cwd, root, pm);
2945
+ if (problem !== null) {
2946
+ process.stderr.write(`${problem}
2947
+ `);
2948
+ return 1;
2949
+ }
2950
+ if (!input.mayWrite) {
2951
+ process.stderr.write(
2952
+ "rness create writes files; pass --yes to run without a prompt\n"
2953
+ );
2954
+ return 2;
2825
2955
  }
2956
+ if (input.interactive && !prompted) {
2957
+ const p = await input.prompts();
2958
+ const ok = await p.confirm({
2959
+ message: `Create blank workspace ${name} in ./${shown}?`
2960
+ });
2961
+ if (p.isCancel(ok) || ok !== true) {
2962
+ ui.cancelled();
2963
+ return 0;
2964
+ }
2965
+ }
2966
+ const built = await buildContext({
2967
+ ui,
2968
+ root,
2969
+ shown,
2970
+ manifest: { contract: 1, org: null, repos: {}, scopes: {} },
2971
+ pm,
2972
+ skipInstall: input.skipInstall,
2973
+ kind: "blank workspace"
2974
+ });
2975
+ if (!built) return 1;
2976
+ await mkdir4(join16(root, "org"), { recursive: true });
2977
+ await commitContext({ ui, rnessDir: join16(root, ".rness"), shown });
2978
+ const code = await syncBlocks(
2979
+ ui,
2980
+ root,
2981
+ shown,
2982
+ () => syncCommand({ yes: true, cwd: root })
2983
+ );
2984
+ if (code !== 0) return code;
2985
+ const next = [
2986
+ `cd ${shown}`,
2987
+ "# bring a repository in: <owner>/<repo> on GitHub, or any git URL",
2988
+ `${await rnessRunnerFor(pm)} add <owner>/<repo>`,
2989
+ '# to share it: set "org" in .rness/rness.json, then push .rness to github.com/<org>/.rness'
2990
+ ];
2991
+ if (ui.session) {
2992
+ ui.note("Next steps", next);
2993
+ ui.outro(`Workspace ${name} is ready in ${shown}/`);
2994
+ } else
2995
+ process.stdout.write(
2996
+ [
2997
+ "",
2998
+ `Workspace \`${name}\` is ready in ${shown}/.`,
2999
+ "",
3000
+ "Next:",
3001
+ ...next.map((line) => ` ${line}`),
3002
+ ""
3003
+ ].join("\n")
3004
+ );
3005
+ return 0;
2826
3006
  }
3007
+
3008
+ // src/commands/create.ts
2827
3009
  function firstLine4(text) {
2828
3010
  return text.split("\n").find((l) => l.trim() !== "")?.trim() ?? "unknown error";
2829
3011
  }
2830
3012
  function splitSpecs(list) {
2831
3013
  return list.split(",").map((s) => s.trim()).filter((s) => s !== "");
2832
3014
  }
2833
- async function targetProblem(cwd, root, pm) {
2834
- const shown = relative2(cwd, root) || ".";
2835
- if (await insideWorkspace(cwd) || await insideWorkspace(dirname6(root)))
2836
- return "already inside an rness workspace; create runs from outside one";
2837
- if (!await exists(root)) return null;
2838
- if (await exists(join15(root, ".rness", "rness.json"))) {
2839
- if (!await exists(join15(root, ".rness", "node_modules")))
2840
- return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then rness sync`;
2841
- return `${shown} already holds an rness workspace (.rness/); run rness add inside it to add repositories`;
2842
- }
2843
- if (!(await stat3(root)).isDirectory()) return `${shown} is not a directory`;
2844
- if (!await isEmptyDir(root)) return `${shown} is not empty`;
2845
- return null;
2846
- }
2847
3015
  function cancelled(ui) {
2848
3016
  ui.cancelled();
2849
3017
  return 0;
@@ -2966,7 +3134,7 @@ async function cloneSelection(input) {
2966
3134
  { doing: `cloning org/${repo}`, git: true },
2967
3135
  () => clone(
2968
3136
  entry.url,
2969
- join15(input.root, "org", repo),
3137
+ join17(input.root, "org", repo),
2970
3138
  input.provider.credentialsFor(entry.url)
2971
3139
  ),
2972
3140
  () => ["cloned", `org/${repo}`]
@@ -3006,6 +3174,25 @@ async function placeContext(staged, rnessDir, url, credentials) {
3006
3174
  await clone(url, rnessDir, credentials);
3007
3175
  }
3008
3176
  }
3177
+ function blankConflict(opts) {
3178
+ if (opts.org !== void 0) return "--org";
3179
+ if (opts.repos !== void 0) return "--repos";
3180
+ if (opts.ssh === true) return "--ssh";
3181
+ if (opts.https === true) return "--https";
3182
+ if (opts.host !== void 0) return "--host";
3183
+ return null;
3184
+ }
3185
+ async function chooseStart(prompts) {
3186
+ const answer = await prompts.select({
3187
+ message: "How do you want to start?",
3188
+ options: [
3189
+ { value: "github", label: "From a GitHub organization" },
3190
+ { value: "blank", label: "Blank local workspace" }
3191
+ ]
3192
+ });
3193
+ if (prompts.isCancel(answer)) return null;
3194
+ return answer === "blank" ? "blank" : "github";
3195
+ }
3009
3196
  var OTHER = /* @__PURE__ */ Symbol("another organization");
3010
3197
  async function chooseOrganization(provider, prompts) {
3011
3198
  if (!provider.authenticated) return OTHER;
@@ -3082,6 +3269,19 @@ async function createCommand(opts, deps = {}) {
3082
3269
  process.stderr.write("--ssh and --https cannot be combined\n");
3083
3270
  return 2;
3084
3271
  }
3272
+ if (opts.blank === true) {
3273
+ const clash = blankConflict(opts);
3274
+ if (clash !== null) {
3275
+ process.stderr.write(`--blank cannot be combined with ${clash}
3276
+ `);
3277
+ return 2;
3278
+ }
3279
+ if (opts.name !== void 0 && !WORKSPACE_NAME.test(opts.name)) {
3280
+ process.stderr.write(`${workspaceNameError(opts.name)}
3281
+ `);
3282
+ return 2;
3283
+ }
3284
+ }
3085
3285
  if (opts.org !== void 0 && !ORG_NAME.test(opts.org)) {
3086
3286
  process.stderr.write(
3087
3287
  `organization name "${opts.org}" is not a valid GitHub organization name
@@ -3090,6 +3290,7 @@ async function createCommand(opts, deps = {}) {
3090
3290
  return 2;
3091
3291
  }
3092
3292
  let pm = opts.pm !== void 0 && isPackageManager(opts.pm) ? opts.pm : detectPackageManager();
3293
+ const launchedWith = pm;
3093
3294
  const interactive = opts.yes !== true && terminal.isTty();
3094
3295
  const ui = interactive ? await makeUi(terminal) : { ...plainUi };
3095
3296
  let sshTest;
@@ -3129,6 +3330,23 @@ async function createCommand(opts, deps = {}) {
3129
3330
  );
3130
3331
  return 1;
3131
3332
  }
3333
+ let blank = opts.blank === true;
3334
+ if (!blank && interactive && opts.org === void 0 && opts.repos === void 0) {
3335
+ const start = await chooseStart(await prompts());
3336
+ if (start === null) return cancelled(ui);
3337
+ blank = start === "blank";
3338
+ }
3339
+ if (blank)
3340
+ return await createBlank({
3341
+ name: opts.name,
3342
+ cwd,
3343
+ pm,
3344
+ interactive,
3345
+ mayWrite: opts.yes === true || terminal.isTty(),
3346
+ skipInstall: opts.skipInstall === true,
3347
+ ui,
3348
+ prompts
3349
+ });
3132
3350
  let prompted = false;
3133
3351
  let org = opts.org;
3134
3352
  if (org === void 0 && !interactive) {
@@ -3176,8 +3394,8 @@ async function createCommand(opts, deps = {}) {
3176
3394
  org = answer.trim();
3177
3395
  prompted = true;
3178
3396
  }
3179
- const root = resolve3(cwd, org);
3180
- const shown = relative2(cwd, root) || ".";
3397
+ const root = resolve4(cwd, org);
3398
+ const shown = relative4(cwd, root) || ".";
3181
3399
  const problem = await targetProblem(cwd, root, pm);
3182
3400
  if (problem !== null) {
3183
3401
  process.stderr.write(`${problem}
@@ -3218,8 +3436,8 @@ async function createCommand(opts, deps = {}) {
3218
3436
  );
3219
3437
  let catalogue = { contract: 1, org, repos: {}, scopes: {} };
3220
3438
  const staged = async () => {
3221
- staging ??= await mkdtemp(join15(tmpdir(), "rness-join-"));
3222
- return join15(staging, ".rness");
3439
+ staging ??= await mkdtemp(join17(tmpdir(), "rness-join-"));
3440
+ return join17(staging, ".rness");
3223
3441
  };
3224
3442
  if (joining) {
3225
3443
  const stagedDir = await staged();
@@ -3269,21 +3487,10 @@ async function createCommand(opts, deps = {}) {
3269
3487
  });
3270
3488
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
3271
3489
  }
3272
- const rnessDir = join15(root, ".rness");
3273
- const install = async () => {
3274
- if (opts.skipInstall === true) {
3275
- ui.line("skipped", `install with ${pm} (--skip-install)`);
3276
- return;
3277
- }
3278
- const frozen = await hasLockfile(rnessDir, pm);
3279
- await ui.step(
3280
- { doing: `installing dependencies with ${pm}` },
3281
- () => installDependencies(pm, rnessDir, { frozen }),
3282
- () => ["installed", `dependencies with ${pm}`]
3283
- );
3284
- };
3490
+ const rnessDir = join17(root, ".rness");
3491
+ const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
3285
3492
  if (joining) {
3286
- await mkdir3(root, { recursive: true });
3493
+ await mkdir5(root, { recursive: true });
3287
3494
  await placeContext(
3288
3495
  await staged(),
3289
3496
  rnessDir,
@@ -3293,7 +3500,7 @@ async function createCommand(opts, deps = {}) {
3293
3500
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
3294
3501
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
3295
3502
  await install();
3296
- await mkdir3(join15(root, "org"), { recursive: true });
3503
+ await mkdir5(join17(root, "org"), { recursive: true });
3297
3504
  const failures2 = await cloneSelection({
3298
3505
  root,
3299
3506
  rnessDir,
@@ -3316,15 +3523,18 @@ async function createCommand(opts, deps = {}) {
3316
3523
  const remaining = Object.keys(catalogue.repos).filter(
3317
3524
  (name2) => !cloned.has(name2)
3318
3525
  );
3526
+ const rness2 = await rnessRunnerFor(launchedWith);
3319
3527
  const next2 = [
3320
3528
  `cd ${shown}`,
3321
3529
  // The comment above its command, as the new-workspace note does: a
3322
3530
  // line that wraps in a narrow terminal is a line nobody reads.
3323
3531
  ...remaining.length > 0 ? [
3324
3532
  `# clone the catalogue repositories you did not pick: ${remaining.join(", ")}`,
3325
- "rness sync --all"
3533
+ `${rness2} sync --all`
3326
3534
  ] : [],
3327
- ...failures2.map((f) => `rness add ${f.spec} # failed: ${f.message}`)
3535
+ ...failures2.map(
3536
+ (f) => `${rness2} add ${f.spec} # failed: ${f.message}`
3537
+ )
3328
3538
  ];
3329
3539
  if (ui.session) ui.note("Next steps", next2);
3330
3540
  else process.stdout.write(`
@@ -3333,30 +3543,17 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3333
3543
  ui.outro(`You joined ${org} \u2014 your workspace is in ${shown}/`);
3334
3544
  return failures2.length > 0 ? 1 : 0;
3335
3545
  }
3336
- const pmVersion = await packageManagerVersion(pm);
3337
- await mkdir3(root, { recursive: true });
3338
- try {
3339
- await copyScaffold(rnessDir, {
3340
- version: VERSION,
3341
- packageManager: `${pm}@${pmVersion}`
3342
- });
3343
- await writeManifest(rnessDir, catalogue);
3344
- ui.line("created", `${shown}/.rness (new workspace)`);
3345
- await install();
3346
- } catch (e) {
3347
- reportError(e);
3348
- await rm4(rnessDir, { recursive: true, force: true }).catch(
3349
- () => void 0
3350
- );
3351
- if (await isEmptyDir(root).catch(() => false))
3352
- await rm4(root, { recursive: true, force: true }).catch(() => void 0);
3353
- process.stderr.write(
3354
- `removed ${shown}/.rness after the failure; fix it and retry
3355
- `
3356
- );
3357
- return 1;
3358
- }
3359
- await mkdir3(join15(root, "org"), { recursive: true });
3546
+ const built = await buildContext({
3547
+ ui,
3548
+ root,
3549
+ shown,
3550
+ manifest: catalogue,
3551
+ pm,
3552
+ skipInstall: opts.skipInstall === true,
3553
+ kind: "new workspace"
3554
+ });
3555
+ if (!built) return 1;
3556
+ await mkdir5(join17(root, "org"), { recursive: true });
3360
3557
  const failures = await cloneSelection({
3361
3558
  root,
3362
3559
  rnessDir,
@@ -3367,15 +3564,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3367
3564
  provider: gitProvider,
3368
3565
  ui
3369
3566
  });
3370
- await init(rnessDir);
3371
- try {
3372
- await commitAll(rnessDir, "chore: rness workspace context");
3373
- ui.line("committed", `${shown}/.rness`);
3374
- } catch (e) {
3375
- ui.warn(
3376
- `${e instanceof Error ? e.message : String(e)} \u2014 commit ${shown}/.rness yourself`
3377
- );
3378
- }
3567
+ await commitContext({ ui, rnessDir, shown });
3379
3568
  const code = await syncBlocks(
3380
3569
  ui,
3381
3570
  root,
@@ -3394,11 +3583,12 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3394
3583
  url: contextUrl
3395
3584
  }) : false;
3396
3585
  if (published === null) return cancelled(ui);
3586
+ const rness = await rnessRunnerFor(launchedWith);
3397
3587
  const next = [
3398
3588
  `cd ${shown}/.rness`,
3399
3589
  // What was asked for and did not happen, as the command that retries
3400
3590
  // it — the workspace is complete otherwise.
3401
- ...failures.map((f) => `rness add ${f.spec} # failed: ${f.message}`),
3591
+ ...failures.map((f) => `${rness} add ${f.spec} # failed: ${f.message}`),
3402
3592
  ...published ? [] : [
3403
3593
  `# create the empty private repository ${org}/.rness on github.com, then:`,
3404
3594
  `git remote add origin ${contextUrl} && git push -u origin main`,
@@ -3407,7 +3597,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3407
3597
  "# or, with the GitHub CLI (gh) installed, in one step:",
3408
3598
  `gh repo create ${org}/.rness --private --source . --push`
3409
3599
  ],
3410
- `# then, for every teammate: npm create rness ${org}`
3600
+ `# then, for every teammate: ${launchedWith} create rness ${org}`
3411
3601
  ];
3412
3602
  if (ui.session) {
3413
3603
  ui.note("Next steps", next);
@@ -3428,7 +3618,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3428
3618
  return reportError(e);
3429
3619
  } finally {
3430
3620
  if (staging !== void 0)
3431
- await rm4(staging, { recursive: true, force: true }).catch(() => void 0);
3621
+ await rm5(staging, { recursive: true, force: true }).catch(() => void 0);
3432
3622
  }
3433
3623
  }
3434
3624
 
@@ -3462,8 +3652,8 @@ async function logoutCommand() {
3462
3652
 
3463
3653
  // src/commands/upgrade.ts
3464
3654
  import { execFile as execFile5 } from "child_process";
3465
- import { mkdir as mkdir4, readFile as readFile9, readdir as readdir3 } from "fs/promises";
3466
- import { basename as basename5, dirname as dirname7, join as join16, relative as relative3 } from "path";
3655
+ import { mkdir as mkdir6, readFile as readFile9, readdir as readdir3 } from "fs/promises";
3656
+ import { basename as basename5, dirname as dirname7, join as join18, relative as relative5 } from "path";
3467
3657
  import { promisify as promisify5 } from "util";
3468
3658
  var execFileP5 = promisify5(execFile5);
3469
3659
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -3560,15 +3750,15 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3560
3750
  return 1;
3561
3751
  }
3562
3752
  }
3563
- const packageFile = join16(ws.rnessDir, "package.json");
3753
+ const packageFile = join18(ws.rnessDir, "package.json");
3564
3754
  const restore = /* @__PURE__ */ new Map([
3565
3755
  [packageFile, await readFile9(packageFile, "utf8")]
3566
3756
  ]);
3567
3757
  if (pin.spec !== target) await writePin(ws.rnessDir, pin.spec, target);
3568
- const workflows = join16(ws.rnessDir, ".github", "workflows");
3758
+ const workflows = join18(ws.rnessDir, ".github", "workflows");
3569
3759
  for (const name of await readdir3(workflows).catch(() => [])) {
3570
3760
  if (!/\.ya?ml$/.test(name)) continue;
3571
- const file = join16(workflows, name);
3761
+ const file = join18(workflows, name);
3572
3762
  const text = await readOrNull(file);
3573
3763
  if (text === null) continue;
3574
3764
  const shown = `.github/workflows/${name}`;
@@ -3586,14 +3776,14 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3586
3776
  ui.warn(`${shown} names @rness/cli@${pin.spec}; update it by hand`);
3587
3777
  }
3588
3778
  }
3589
- const dependabot = join16(ws.rnessDir, ".github", "dependabot.yml");
3779
+ const dependabot = join18(ws.rnessDir, ".github", "dependabot.yml");
3590
3780
  if (await exists(dependabot)) {
3591
3781
  ui.hint(".github/dependabot.yml is already there \u2014 left as it is");
3592
3782
  } else {
3593
- await mkdir4(dirname7(dependabot), { recursive: true });
3783
+ await mkdir6(dirname7(dependabot), { recursive: true });
3594
3784
  await writeFileAtomic(
3595
3785
  dependabot,
3596
- await readFile9(join16(scaffoldDir(), ".github", "dependabot.yml"), "utf8")
3786
+ await readFile9(join18(scaffoldDir(), ".github", "dependabot.yml"), "utf8")
3597
3787
  );
3598
3788
  ui.line(
3599
3789
  "wrote",
@@ -3610,7 +3800,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3610
3800
  for (const [file, text] of restore) await writeFileAtomic(file, text);
3611
3801
  const message = e instanceof Error ? e.message : String(e);
3612
3802
  ui.error(message);
3613
- ui.hint(`restored ${relative3(cwd, packageFile) || packageFile}`, "stderr");
3803
+ ui.hint(`restored ${relative5(cwd, packageFile) || packageFile}`, "stderr");
3614
3804
  if (/minimum.?release.?age|NO_MATURE/i.test(message))
3615
3805
  ui.hint(
3616
3806
  "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
@@ -3629,7 +3819,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3629
3819
  ui.gitIsSilent = true;
3630
3820
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
3631
3821
  if (code !== 0) return code;
3632
- const rel = relative3(cwd, ws.rnessDir) || ".";
3822
+ const rel = relative5(cwd, ws.rnessDir) || ".";
3633
3823
  const next = [
3634
3824
  `git -C ${rel} add -A && git -C ${rel} commit -m "chore: rness ${target}"`,
3635
3825
  "# teammates: git pull \u2014 the next rness command installs it"
@@ -3655,7 +3845,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3655
3845
  }
3656
3846
 
3657
3847
  // src/core/blocks.ts
3658
- import { join as join17 } from "path";
3848
+ import { join as join19 } from "path";
3659
3849
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
3660
3850
  async function checkBlocks({
3661
3851
  root,
@@ -3672,13 +3862,13 @@ async function checkBlocks({
3672
3862
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
3673
3863
  checks
3674
3864
  });
3675
- if (!await exists(join17(root, "org"))) return derive();
3865
+ if (!await exists(join19(root, "org"))) return derive();
3676
3866
  const targets = [{ scope: null, dir: root, depth: 0, label: "AGENTS.md" }];
3677
3867
  for (const [name, entry] of Object.entries(manifest.scopes)) {
3678
3868
  const segments = entry.path.split("/");
3679
3869
  targets.push({
3680
3870
  scope: name,
3681
- dir: join17(root, ...segments),
3871
+ dir: join19(root, ...segments),
3682
3872
  depth: segments.length,
3683
3873
  label: `${entry.path}/AGENTS.md`
3684
3874
  });
@@ -3692,7 +3882,7 @@ async function checkBlocks({
3692
3882
  );
3693
3883
  continue;
3694
3884
  }
3695
- const text = await readOrNull(join17(t.dir, "AGENTS.md"));
3885
+ const text = await readOrNull(join19(t.dir, "AGENTS.md"));
3696
3886
  const lines = text === null ? [] : text.split(/\r?\n/);
3697
3887
  const found = findBlock(lines);
3698
3888
  if (found.kind === "none") {
@@ -3727,7 +3917,7 @@ async function checkBlocks({
3727
3917
  }
3728
3918
 
3729
3919
  // src/core/contract.ts
3730
- import { join as join18 } from "path";
3920
+ import { join as join20 } from "path";
3731
3921
  var STATUSES = {
3732
3922
  adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
3733
3923
  specs: [
@@ -3746,7 +3936,7 @@ async function checkContract(rnessDir) {
3746
3936
  const problems = [];
3747
3937
  for (const name of COLLECTIONS) {
3748
3938
  const allowed = STATUSES[name];
3749
- const items = await collectMarkdown(join18(rnessDir, name));
3939
+ const items = await collectMarkdown(join20(rnessDir, name));
3750
3940
  for (const item of items) {
3751
3941
  const where = `${name}/${item.rel}`;
3752
3942
  if (SKIP.has(where)) continue;
@@ -3789,8 +3979,7 @@ async function validateCommand(opts, deps = {}) {
3789
3979
  const ws = await findWorkspace(cwd);
3790
3980
  const manifest = await loadManifest(ws.rnessDir);
3791
3981
  for (const s of Object.keys(manifest.scopes)) scopeChain(manifest, s);
3792
- const { org, warning } = resolveOrg(manifest, ws.root);
3793
- const warnings = warning === null ? [] : [warning];
3982
+ const org = workspaceName(manifest, ws.root);
3794
3983
  const problems = await checkContract(ws.rnessDir);
3795
3984
  const blocks = await checkBlocks({
3796
3985
  root: ws.root,
@@ -3799,12 +3988,11 @@ async function validateCommand(opts, deps = {}) {
3799
3988
  org
3800
3989
  });
3801
3990
  problems.push(...blocks.problems);
3802
- warnings.push(...blocks.warnings);
3803
3991
  const ui = await makeUi(deps.terminal ?? defaultTerminal);
3804
3992
  if (!ui.session) {
3805
3993
  for (const p of problems) process.stderr.write(`${p}
3806
3994
  `);
3807
- for (const w of warnings) process.stderr.write(`${warn(w)}
3995
+ for (const w of blocks.warnings) process.stderr.write(`${warn(w)}
3808
3996
  `);
3809
3997
  if (problems.length > 0) return 1;
3810
3998
  process.stdout.write("context ok\n");
@@ -3817,7 +4005,6 @@ async function validateCommand(opts, deps = {}) {
3817
4005
  ui.error(c.message);
3818
4006
  else ui.warn(c.message);
3819
4007
  for (const p of problems) if (!blocks.problems.includes(p)) ui.error(p);
3820
- for (const w of warnings) if (!blocks.warnings.includes(w)) ui.warn(w);
3821
4008
  ui.outro(
3822
4009
  problems.length > 0 ? "Context mismatch" : "Context OK",
3823
4010
  problems.length > 0 ? "error" : "done"
@@ -3920,8 +4107,11 @@ function buildProgram(state) {
3920
4107
  state.code = await upgradeCommand(version, opts);
3921
4108
  });
3922
4109
  program.command("create").description(
3923
- "Create a workspace for a GitHub organization, or join its existing .rness"
3924
- ).argument("[org]", "the GitHub organization, same as --org").option(
4110
+ "Create or join a GitHub organization's workspace, or a blank one with --blank"
4111
+ ).argument(
4112
+ "[name]",
4113
+ "the GitHub organization, same as --org; with --blank, the workspace directory"
4114
+ ).option(
3925
4115
  "--org <name>",
3926
4116
  "the GitHub organization, exact name (github.com/<name>)"
3927
4117
  ).option(
@@ -3933,7 +4123,16 @@ function buildProgram(state) {
3933
4123
  ).option(
3934
4124
  "--ssh",
3935
4125
  "force git@github.com: (default: SSH when it works, else HTTPS)"
3936
- ).option("--https", "force https://github.com/").option("--skip-install", "do not install .rness/ dependencies").option("-y, --yes", "do not ask for confirmation").option("--template <name>", "reserved").addOption(new Option("--host <base>").hideHelp()).addOption(new Option("--github-api <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (orgArg, opts) => {
4126
+ ).option("--https", "force https://github.com/").option(
4127
+ "--blank",
4128
+ "a local workspace with no GitHub organization: .rness/, an empty org/, the root files"
4129
+ ).option("--skip-install", "do not install .rness/ dependencies").option("-y, --yes", "do not ask for confirmation").option("--template <name>", "reserved").addOption(new Option("--host <base>").hideHelp()).addOption(new Option("--github-api <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (orgArg, opts) => {
4130
+ if (opts.blank === true) {
4131
+ state.code = await createCommand(
4132
+ orgArg === void 0 ? opts : { ...opts, name: orgArg }
4133
+ );
4134
+ return;
4135
+ }
3937
4136
  if (orgArg !== void 0 && opts.org !== void 0 && orgArg !== opts.org) {
3938
4137
  process.stderr.write(
3939
4138
  `organization given twice: "${orgArg}" and --org "${opts.org}"
@@ -3967,6 +4166,51 @@ async function run(argv) {
3967
4166
  return state.code;
3968
4167
  }
3969
4168
 
4169
+ // src/core/catch-up.ts
4170
+ import { spawnSync } from "child_process";
4171
+ var OWN_COMMANDS = [
4172
+ "create",
4173
+ "upgrade",
4174
+ "update",
4175
+ "login",
4176
+ "logout",
4177
+ "git-credential"
4178
+ ];
4179
+ var CATCH_UP_DONE = "RNESS_CATCH_UP_DONE";
4180
+ async function catchUp(argv, deps = {}) {
4181
+ const env = deps.env ?? process.env;
4182
+ const cwd = deps.cwd ?? process.cwd();
4183
+ const command = argv[0];
4184
+ if (command === void 0 || command.startsWith("-") || OWN_COMMANDS.includes(command) || env["RNESS_NO_DELEGATE"] === "1" || env[CATCH_UP_DONE] === "1")
4185
+ return null;
4186
+ const ws = await findWorkspace(cwd).catch(() => null);
4187
+ if (ws === null) return null;
4188
+ env[CATCH_UP_DONE] = "1";
4189
+ const stderr = deps.stderr ?? ((text) => void process.stderr.write(text));
4190
+ const outcome = await ensureInstalled(ws.rnessDir, {
4191
+ env,
4192
+ announce: (pin) => stderr(`installing @rness/cli ${pin} in .rness\u2026
4193
+ `),
4194
+ ...deps.install === void 0 ? {} : { install: deps.install }
4195
+ });
4196
+ if (outcome === null) return null;
4197
+ if (!outcome.installed) {
4198
+ const warning = await driftWarning(cwd);
4199
+ if (warning !== null) stderr(`${warning}
4200
+ `);
4201
+ return null;
4202
+ }
4203
+ if (await pinDrift(ws.rnessDir) !== null) return null;
4204
+ const bin = await pinnedBinPath(ws.rnessDir);
4205
+ if (bin === null) return null;
4206
+ const spawn4 = deps.spawn ?? ((file, args, dir) => spawnSync(process.execPath, [file, ...args], {
4207
+ cwd: dir,
4208
+ env,
4209
+ stdio: "inherit"
4210
+ }).status ?? 1);
4211
+ return spawn4(bin, argv, cwd);
4212
+ }
4213
+
3970
4214
  export {
3971
4215
  exists,
3972
4216
  readOrNull,
@@ -4013,20 +4257,16 @@ export {
4013
4257
  addCommand,
4014
4258
  renderMarkdown,
4015
4259
  contextCommand,
4016
- pinnedBinPath,
4260
+ VERSION,
4017
4261
  findDelegate,
4018
4262
  PACKAGE_MANAGERS,
4019
4263
  isPackageManager,
4020
4264
  detectPackageManager,
4021
4265
  packageManagerVersion,
4022
4266
  installDependencies,
4023
- pinDrift,
4024
- driftWarning,
4025
- ensureInstalled,
4026
4267
  SCAFFOLD_FILES,
4027
4268
  scaffoldDir,
4028
4269
  copyScaffold,
4029
- VERSION,
4030
4270
  loginCommand,
4031
4271
  createCommand,
4032
4272
  logoutCommand,
@@ -4035,6 +4275,8 @@ export {
4035
4275
  STATUSES,
4036
4276
  checkContract,
4037
4277
  validateCommand,
4038
- run
4278
+ run,
4279
+ OWN_COMMANDS,
4280
+ catchUp
4039
4281
  };
4040
- //# sourceMappingURL=chunk-P4YG6P7I.js.map
4282
+ //# sourceMappingURL=chunk-NY3WV7X5.js.map