oh-my-space 0.14.0 → 0.14.1

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/README.md CHANGED
@@ -21,7 +21,7 @@ OMS automates routine, deterministic preparation and bounded recovery whenever i
21
21
 
22
22
  - [Node.js](https://nodejs.org) `>=20.19.0` to run `oms`.
23
23
  - git `>=2.40` for `git switch` and the submodule commands `oms` relies on.
24
- - Run `oms` from a Git repository. For a new workspace, run `git init` first, since sources are tracked as submodules of it.
24
+ - Submodule commands require `oms.yaml` at the root Git top-level. `oms init` can scaffold outside Git, but run `git init` at that same workspace root before syncing sources.
25
25
 
26
26
  ## Install
27
27
 
@@ -70,6 +70,14 @@ oms/
70
70
 
71
71
  `oms.yaml` is your declaration, `.gitmodules` and each `oms/<alias>` gitlink are tracked in your project history, and every directory under `oms/` is a normal checked-out repository you can branch, edit, and commit in.
72
72
 
73
+ ## Command locations and workspace discovery
74
+
75
+ Workspace-aware commands search from the current directory toward the filesystem root and use the nearest `oms.yaml`. The first `oms.yaml` entry encountered is authoritative. It must be a regular file, or a symbolic link whose target is a regular file. A directory, broken link, link to a non-file target, or invalid nearest manifest causes the command to fail; OMS does not skip it and fall back to an outer workspace.
76
+
77
+ You can run workspace-aware commands at the workspace root or below it. When the current directory is inside `oms/<alias>/` for an alias declared by the selected manifest, `oms status --json` reports that alias and commands such as `oms commit` and `oms record` can infer it. An alias supplied on the command line always takes precedence over the inferred current alias. Arbitrary descendants and undeclared `oms/<name>/` paths can discover the workspace but do not become a current alias.
78
+
79
+ Commands that inspect or change submodule state require the selected manifest directory to be the root Git top-level. A nested `oms.yaml` below an enclosing Git root is rejected before `.gitmodules`, the root index, `oms/`, or the manifest can change. Repair that layout by either moving `oms.yaml` to the enclosing Git root or initializing a separate Git repository at the intended nested workspace root. `oms sync --list` is manifest-only and remains available before Git initialization.
80
+
73
81
  ## Typical branch flow
74
82
 
75
83
  Start a branch, commit inside the submodule, push it, then record the pointer move in your project history. Each command stays in a single Git scope:
@@ -183,12 +191,12 @@ Skill firing is best-effort — an agent loads a skill only when it judges the s
183
191
 
184
192
  | Command | Runs in | Does | Notes |
185
193
  | --- | --- | --- | --- |
186
- | `oms init` | current directory | Writes a starter `oms.yaml`. | Refuses if `oms.yaml` exists; use `--force`. Does not gitignore `oms/`. |
187
- | `oms doctor` | project root or child path | Checks `oms.yaml`, git availability, that the workspace is a git repo, and each alias's submodule state. | Returns exit 2 if any warning is raised. |
194
+ | `oms init` | current directory | Writes a starter `oms.yaml`. | The directory must be outside Git or the root Git top-level. A nested Git-work-tree directory is rejected before writes, even with `--force`. Otherwise refuses an existing manifest unless `--force` is used. Does not gitignore `oms/`. |
195
+ | `oms doctor` | workspace root or child path | Checks the nearest `oms.yaml`, Git root identity, and each alias's submodule state. | Diagnoses a nested manifest directly instead of treating it as a valid root. Returns exit 2 if any warning is raised. |
188
196
  | `oms sync <alias>` / `--all` | workspace root | Registers missing repos with `git submodule add`, initializes registered-but-uninitialized ones, fetches, and attaches the baseline branch. | Reproduces the recorded pointer on a fresh clone. Topology changes (`.gitmodules`, `oms/<alias>`) are left unstaged by default; commit them via the prompt or `--commit` (`chore(oms): add ...`). |
189
197
  | `oms status [alias...]` / `--all` | anywhere under root | Prints branch, pointer state (`ok`/`moved`/`uninit`/`missing`/`conflict`), dirtiness, and ahead/behind for each submodule. | `moved` means the working commit differs from the recorded pointer — record it with `oms record`. `--json` prints one machine-readable object on stdout for tooling and agents. |
190
- | `oms commit [alias]` | workspace root or inside `oms/<alias>/` | Commits source changes inside the selected submodule only; never the root gitlink. | `-m <message>` is required (repeatable). Commits existing staged changes as-is, otherwise stages all with `git add -A`. Infers the alias from the current `oms/<alias>/` directory. |
191
- | `oms record [alias]` | workspace root or inside `oms/<alias>/` | Commits an existing root gitlink pointer update for one alias (`chore(oms): update <alias> submodule to <sha>`). | Root repo only, path-limited to `oms/<alias>`; refuses unrelated staged changes. Not for adds/removals — use `oms sync`/`oms unsync`. |
198
+ | `oms commit [alias]` | workspace root or inside `oms/<alias>/` | Commits source changes inside the selected submodule only; never the root gitlink. | `-m <message>` is required (repeatable). Commits existing staged changes as-is, otherwise stages all with `git add -A`. Infers a configured alias from the current `oms/<alias>/` directory; an explicit alias wins. |
199
+ | `oms record [alias]` | workspace root or inside `oms/<alias>/` | Commits an existing root gitlink pointer update for one alias (`chore(oms): update <alias> submodule to <sha>`). | Root repo only, path-limited to `oms/<alias>`; refuses unrelated staged changes. An explicit alias wins over current-path inference. Not for adds/removals — use `oms sync`/`oms unsync`. |
192
200
  | `oms branch switch [alias] [branch]` | workspace root | `git switch` to a LOCAL branch, creating it locally if it does not exist yet (no remote required). | `--from <ref>` sets the start point for a new branch. Omit alias/branch to pick interactively (or create a new branch). |
193
201
  | `oms branch checkout [alias] [branch]` | workspace root | `git fetch origin --prune`, then check out a REMOTE branch (`origin/*`) as a local tracking branch (or switch to an existing local counterpart). | Omit alias/branch to pick interactively. To create a brand-new local branch, use `oms branch switch`. |
194
202
  | `oms branch list [alias]` | workspace root | Safely prepares one declared submodule, refreshes all declared remotes, and lists local and remote-tracking branches. | Shows baseline certainty, current/baseline flags, exact upstream divergence, detached HEAD, and per-remote `fresh`/`stale`/`unavailable` state. Never switches a branch or changes a root gitlink outside an explicitly accepted `oms sync`. |
@@ -1 +1 @@
1
- {"commit":"a0692fa946bc1ae17a53ecdda5077dad9dbd894d"}
1
+ {"commit":"9ab3e6b650acac13d9fff4b8b091b07721d427ac"}
package/dist/oms.js CHANGED
@@ -14197,7 +14197,7 @@ import { join as join3 } from "node:path";
14197
14197
 
14198
14198
  // scripts/lib/git.ts
14199
14199
  import { spawnSync } from "node:child_process";
14200
- import { existsSync as existsSync2 } from "node:fs";
14200
+ import { existsSync as existsSync2, lstatSync, realpathSync, statSync } from "node:fs";
14201
14201
  import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
14202
14202
  function redactSensitiveUrls(value) {
14203
14203
  return value.replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+(?::[^\s/@]*)?@/gi, "$1[redacted]@").replace(/([?&](?:(?:access|auth|bearer|id|oauth|refresh)?[_-]?token|(?:api|private)?[_-]?key|auth(?:orization)?|(?:client|consumer)?[_-]?secret|credential|jwt|pass(?:word|wd)?|signature)=)[^&\s]+/gi, "$1[redacted]");
@@ -14241,15 +14241,54 @@ function isGitVersionSupported(v2) {
14241
14241
  return v2.minor >= MIN_GIT_MINOR;
14242
14242
  }
14243
14243
  __name(isGitVersionSupported, "isGitVersionSupported");
14244
- function findWorkspaceRoot(options = {}) {
14244
+ function resolveWorkspaceManifest(options = {}) {
14245
14245
  let current = resolve2(options.cwd ?? process.cwd());
14246
14246
  while (true) {
14247
- if (existsSync2(join2(current, MANIFEST_FILENAME))) return current;
14248
- const parent = dirname2(current);
14249
- if (parent === current) return null;
14250
- current = parent;
14247
+ const manifestPath = join2(current, MANIFEST_FILENAME);
14248
+ let entry;
14249
+ try {
14250
+ entry = lstatSync(manifestPath);
14251
+ } catch (error) {
14252
+ const code = error instanceof Error && "code" in error ? error.code : void 0;
14253
+ if (code === "ENOENT") {
14254
+ const parent = dirname2(current);
14255
+ if (parent === current) return { kind: "missing" };
14256
+ current = parent;
14257
+ continue;
14258
+ }
14259
+ return {
14260
+ kind: "invalid",
14261
+ manifestPath,
14262
+ reason: error instanceof Error ? error.message : String(error)
14263
+ };
14264
+ }
14265
+ try {
14266
+ if (entry.isFile() || entry.isSymbolicLink() && statSync(manifestPath).isFile()) {
14267
+ return { kind: "found", manifestPath, repoRoot: current };
14268
+ }
14269
+ } catch {
14270
+ return {
14271
+ kind: "invalid",
14272
+ manifestPath,
14273
+ reason: "it is a broken symbolic link or does not resolve to a regular file"
14274
+ };
14275
+ }
14276
+ return {
14277
+ kind: "invalid",
14278
+ manifestPath,
14279
+ reason: "it is not a regular file or a symbolic link to a regular file"
14280
+ };
14251
14281
  }
14252
14282
  }
14283
+ __name(resolveWorkspaceManifest, "resolveWorkspaceManifest");
14284
+ function findWorkspaceRoot(options = {}) {
14285
+ const resolution = resolveWorkspaceManifest(options);
14286
+ if (resolution.kind === "found") return resolution.repoRoot;
14287
+ if (resolution.kind === "missing") return null;
14288
+ throw new Error(
14289
+ `Found ${MANIFEST_FILENAME} at ${resolution.manifestPath}, but ${resolution.reason}. Replace that entry with a regular file; OMS will not fall back to an ancestor manifest.`
14290
+ );
14291
+ }
14253
14292
  __name(findWorkspaceRoot, "findWorkspaceRoot");
14254
14293
  function aliasDir(repoRoot, alias) {
14255
14294
  return join2(repoRoot, DATA_DIRNAME, alias);
@@ -14259,10 +14298,34 @@ function submodulePath(alias) {
14259
14298
  return `${DATA_DIRNAME}/${alias}`;
14260
14299
  }
14261
14300
  __name(submodulePath, "submodulePath");
14262
- function isGitRepo(repoRoot) {
14263
- return runGit(repoRoot, ["rev-parse", "--is-inside-work-tree"]).stdout.trim() === "true";
14301
+ function inspectWorkspaceGitIdentity(workspaceRoot) {
14302
+ const topLevel = runGit(workspaceRoot, ["rev-parse", "--show-toplevel"], false, {
14303
+ ...process.env,
14304
+ LANG: "C",
14305
+ LC_ALL: "C"
14306
+ });
14307
+ if (!topLevel.success) {
14308
+ const diagnostic = topLevel.stderr.trim();
14309
+ if (/not a git repository|not a git work tree/i.test(diagnostic)) return { kind: "no-work-tree" };
14310
+ return {
14311
+ kind: "indeterminate",
14312
+ reason: diagnostic || "Git did not return a repository top-level"
14313
+ };
14314
+ }
14315
+ const gitTopLevel = process.platform === "win32" ? topLevel.stdout.replace(/\r?\n$/, "") : topLevel.stdout.replace(/\n$/, "");
14316
+ if (!gitTopLevel) {
14317
+ return { kind: "indeterminate", reason: "Git returned an empty repository top-level" };
14318
+ }
14319
+ try {
14320
+ return realpathSync(workspaceRoot) === realpathSync(gitTopLevel) ? { kind: "match", gitTopLevel } : { kind: "mismatch", gitTopLevel };
14321
+ } catch (error) {
14322
+ return {
14323
+ kind: "indeterminate",
14324
+ reason: error instanceof Error ? error.message : String(error)
14325
+ };
14326
+ }
14264
14327
  }
14265
- __name(isGitRepo, "isGitRepo");
14328
+ __name(inspectWorkspaceGitIdentity, "inspectWorkspaceGitIdentity");
14266
14329
  function registeredSubmodulePaths(repoRoot) {
14267
14330
  if (!existsSync2(join2(repoRoot, ".gitmodules"))) return [];
14268
14331
  const result = runGit(repoRoot, [
@@ -14650,6 +14713,7 @@ function abortOnLegacyRenameAt(repoRoot) {
14650
14713
  }
14651
14714
  __name(abortOnLegacyRenameAt, "abortOnLegacyRenameAt");
14652
14715
  function emitLegacyRenameHintWalkUp(options = {}) {
14716
+ if (resolveWorkspaceManifest(options).kind !== "missing") return false;
14653
14717
  let current = resolve3(options.cwd ?? process.cwd());
14654
14718
  while (true) {
14655
14719
  if (existsSync4(join4(current, LEGACY_MANIFEST))) {
@@ -14676,22 +14740,51 @@ function abortOnLegacyWorktree(repoRoot, repos) {
14676
14740
  }
14677
14741
  __name(abortOnLegacyWorktree, "abortOnLegacyWorktree");
14678
14742
  function loadRepos(options = {}) {
14679
- const repoRoot = findWorkspaceRoot(options);
14680
- if (!repoRoot) {
14743
+ const resolution = resolveWorkspaceManifest(options);
14744
+ if (resolution.kind === "missing") {
14681
14745
  R2.error(
14682
14746
  `Could not find ${MANIFEST_FILENAME} in the current directory or its parents. Create a ${MANIFEST_FILENAME} in this project, then retry.`
14683
14747
  );
14684
14748
  return null;
14685
14749
  }
14750
+ if (resolution.kind === "invalid") {
14751
+ R2.error(
14752
+ `Found ${MANIFEST_FILENAME} at ${resolution.manifestPath}, but ${resolution.reason}. Replace that entry with a regular file; OMS will not fall back to an ancestor manifest.`
14753
+ );
14754
+ return null;
14755
+ }
14686
14756
  try {
14687
- const manifestPath = join4(repoRoot, MANIFEST_FILENAME);
14688
- return { repos: validateSources((0, import_yaml.parse)(readFileSync3(manifestPath, "utf8"))), repoRoot };
14757
+ return {
14758
+ repos: validateSources((0, import_yaml.parse)(readFileSync3(resolution.manifestPath, "utf8"))),
14759
+ repoRoot: resolution.repoRoot
14760
+ };
14689
14761
  } catch (e2) {
14690
14762
  R2.error(e2 instanceof Error ? e2.message : String(e2));
14691
14763
  return null;
14692
14764
  }
14693
14765
  }
14694
14766
  __name(loadRepos, "loadRepos");
14767
+ function emitWorkspaceGitIdentityError(repoRoot) {
14768
+ const identity = inspectWorkspaceGitIdentity(repoRoot);
14769
+ if (identity.kind === "match") return false;
14770
+ if (identity.kind === "no-work-tree") {
14771
+ R2.error(
14772
+ `${repoRoot} is not a git repository. oh-my-space manages sources as git submodules; run "git init" at the workspace root first.`
14773
+ );
14774
+ return true;
14775
+ }
14776
+ if (identity.kind === "mismatch") {
14777
+ R2.error(
14778
+ `Workspace manifest directory ${repoRoot} does not match the root Git top-level ${identity.gitTopLevel}. Move ${MANIFEST_FILENAME} to ${identity.gitTopLevel}, or initialize a separate Git repository at ${repoRoot}.`
14779
+ );
14780
+ return true;
14781
+ }
14782
+ R2.error(
14783
+ `Could not verify that workspace ${repoRoot} is the root Git top-level: ${identity.reason}. Retry after the workspace path and Git repository are accessible.`
14784
+ );
14785
+ return true;
14786
+ }
14787
+ __name(emitWorkspaceGitIdentityError, "emitWorkspaceGitIdentityError");
14695
14788
  function loadForSubmodules() {
14696
14789
  const loaded = loadRepos();
14697
14790
  if (!loaded) {
@@ -14700,19 +14793,14 @@ function loadForSubmodules() {
14700
14793
  }
14701
14794
  const { repoRoot, repos } = loaded;
14702
14795
  if (abortOnLegacyRenameAt(repoRoot)) return null;
14703
- if (!isGitRepo(repoRoot)) {
14704
- R2.error(
14705
- `${repoRoot} is not a git repository. oh-my-space 0.6.0 manages sources as git submodules; run "git init" at the workspace root first.`
14706
- );
14707
- return null;
14708
- }
14796
+ if (emitWorkspaceGitIdentityError(repoRoot)) return null;
14709
14797
  if (abortOnLegacyWorktree(repoRoot, repos)) return null;
14710
14798
  return loaded;
14711
14799
  }
14712
14800
  __name(loadForSubmodules, "loadForSubmodules");
14713
14801
 
14714
14802
  // scripts/lib/status.ts
14715
- import { existsSync as existsSync5, lstatSync, readdirSync } from "node:fs";
14803
+ import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync } from "node:fs";
14716
14804
  import { relative, resolve as resolve4 } from "node:path";
14717
14805
  function pinState(repoRoot, alias) {
14718
14806
  const r = runGit(repoRoot, ["submodule", "status", "--", submodulePath(alias)]);
@@ -14883,7 +14971,7 @@ __name(partialRemovalTopology, "partialRemovalTopology");
14883
14971
  function readAliasDirEntries(repoRoot, alias) {
14884
14972
  const dir = aliasDir(repoRoot, alias);
14885
14973
  try {
14886
- if (!lstatSync(dir).isDirectory()) return { exists: true, kind: "file" };
14974
+ if (!lstatSync2(dir).isDirectory()) return { exists: true, kind: "file" };
14887
14975
  return { exists: true, kind: "dir", entries: readdirSync(dir) };
14888
14976
  } catch (error) {
14889
14977
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
@@ -15305,7 +15393,7 @@ import {
15305
15393
  constants as fsConstants,
15306
15394
  copyFileSync,
15307
15395
  fsyncSync,
15308
- lstatSync as lstatSync2,
15396
+ lstatSync as lstatSync3,
15309
15397
  mkdirSync as mkdirSync2,
15310
15398
  openSync as openSync2,
15311
15399
  readFileSync as readFileSync6,
@@ -15318,7 +15406,7 @@ import {
15318
15406
  import { dirname as dirname4, join as join7 } from "node:path";
15319
15407
 
15320
15408
  // scripts/lib/gitmodules-reconcile.ts
15321
- import { chmodSync, closeSync, existsSync as existsSync7, openSync, readFileSync as readFileSync5, renameSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
15409
+ import { chmodSync, closeSync, existsSync as existsSync7, openSync, readFileSync as readFileSync5, renameSync, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
15322
15410
  import { join as join6 } from "node:path";
15323
15411
  function shq(arg) {
15324
15412
  return `'${arg.replace(/'/g, "'\\''")}'`;
@@ -15401,7 +15489,7 @@ function reconcileGitmodules(repoRoot, plans, snapshot) {
15401
15489
  if (changedFields.size === 0) return { ok: true, changedFields };
15402
15490
  let originalMode;
15403
15491
  try {
15404
- originalMode = statSync(gitmodules).mode & 511;
15492
+ originalMode = statSync2(gitmodules).mode & 511;
15405
15493
  } catch {
15406
15494
  return { ok: false, retained: "original", reason: "could not read .gitmodules file mode", unapplied: aliases };
15407
15495
  }
@@ -15447,7 +15535,7 @@ __name(gitDir, "gitDir");
15447
15535
  function omsStateDir(repoRoot) {
15448
15536
  const dir = join7(gitDir(repoRoot), "oms");
15449
15537
  if (pathExists(dir)) {
15450
- const stat = lstatSync2(dir);
15538
+ const stat = lstatSync3(dir);
15451
15539
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`unsafe OMS state directory: ${dir}`);
15452
15540
  } else {
15453
15541
  mkdirSync2(dir, { recursive: true, mode: 448 });
@@ -15461,7 +15549,7 @@ function markerPath(repoRoot) {
15461
15549
  __name(markerPath, "markerPath");
15462
15550
  function pathExists(path) {
15463
15551
  try {
15464
- lstatSync2(path);
15552
+ lstatSync3(path);
15465
15553
  return true;
15466
15554
  } catch (error) {
15467
15555
  if (error.code === "ENOENT") return false;
@@ -15471,7 +15559,7 @@ function pathExists(path) {
15471
15559
  __name(pathExists, "pathExists");
15472
15560
  function pathIsSymlink(path) {
15473
15561
  try {
15474
- return lstatSync2(path).isSymbolicLink();
15562
+ return lstatSync3(path).isSymbolicLink();
15475
15563
  } catch {
15476
15564
  return false;
15477
15565
  }
@@ -15837,7 +15925,7 @@ function buildRecoveryIndex(repoRoot, realIndex, recoveryIndex, newHead, committ
15837
15925
  __name(buildRecoveryIndex, "buildRecoveryIndex");
15838
15926
  function installRecoveryIndex(realIndex, recoveryIndex, expectedHash) {
15839
15927
  try {
15840
- if (!lstatSync2(recoveryIndex).isFile() || lstatSync2(recoveryIndex).isSymbolicLink()) return false;
15928
+ if (!lstatSync3(recoveryIndex).isFile() || lstatSync3(recoveryIndex).isSymbolicLink()) return false;
15841
15929
  } catch {
15842
15930
  return false;
15843
15931
  }
@@ -15900,7 +15988,7 @@ function recoveryPreflight(repoRoot) {
15900
15988
  const stateDir = join7(gitDir(repoRoot), "oms");
15901
15989
  let stateExists = false;
15902
15990
  try {
15903
- const stat = lstatSync2(stateDir);
15991
+ const stat = lstatSync3(stateDir);
15904
15992
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
15905
15993
  return { ok: false, reason: `unsafe OMS state directory: ${stateDir}` };
15906
15994
  }
@@ -16723,12 +16811,7 @@ async function runSync(aliases, options) {
16723
16811
  printList(repos);
16724
16812
  return 0;
16725
16813
  }
16726
- if (!isGitRepo(repoRoot)) {
16727
- R2.error(
16728
- `${repoRoot} is not a git repository. oh-my-space 0.6.0 manages sources as git submodules; run "git init" at the workspace root first.`
16729
- );
16730
- return 1;
16731
- }
16814
+ if (emitWorkspaceGitIdentityError(repoRoot)) return 1;
16732
16815
  if (abortOnLegacyWorktree(repoRoot, repos)) return 1;
16733
16816
  const picked = await selectRepos(repos, aliases, options, "sync");
16734
16817
  if (!picked || picked.length === 0) return 1;
@@ -17613,7 +17696,7 @@ async function runDoctor() {
17613
17696
  }
17614
17697
  const { repos, repoRoot } = loaded;
17615
17698
  if (abortOnLegacyRenameAt(repoRoot)) return 1;
17616
- R2.success(`Workspace root: ${repoRoot}`);
17699
+ R2.success(`Workspace manifest directory: ${repoRoot}`);
17617
17700
  R2.success(`${MANIFEST_FILENAME}: ${repos.length} repo(s) configured`);
17618
17701
  const git = spawnSync3("git", ["--version"], {
17619
17702
  encoding: "utf8",
@@ -17637,12 +17720,26 @@ async function runDoctor() {
17637
17720
  );
17638
17721
  warnings++;
17639
17722
  }
17640
- if (!isGitRepo(repoRoot)) {
17723
+ const identity = inspectWorkspaceGitIdentity(repoRoot);
17724
+ if (identity.kind === "no-work-tree") {
17641
17725
  R2.warn(
17642
17726
  `workspace is not a git repository. oms manages sources as submodules; run "git init" at ${repoRoot}.`
17643
17727
  );
17644
- warnings++;
17728
+ return 2;
17645
17729
  }
17730
+ if (identity.kind === "mismatch") {
17731
+ R2.error(
17732
+ `Workspace manifest directory ${repoRoot} does not match the root Git top-level ${identity.gitTopLevel}. Move ${MANIFEST_FILENAME} to ${identity.gitTopLevel}, or initialize a separate Git repository at ${repoRoot}.`
17733
+ );
17734
+ return 1;
17735
+ }
17736
+ if (identity.kind === "indeterminate") {
17737
+ R2.error(
17738
+ `Could not verify that workspace ${repoRoot} is the root Git top-level: ${identity.reason}. Retry after the workspace path and Git repository are accessible.`
17739
+ );
17740
+ return 1;
17741
+ }
17742
+ R2.success(`Workspace root: ${repoRoot}`);
17646
17743
  if (abortOnLegacyWorktree(repoRoot, repos)) return 1;
17647
17744
  if (gitignoreIgnoresOms(repoRoot)) {
17648
17745
  R2.warn(`.gitignore excludes ${GITIGNORE_ENTRY}, but submodules must be tracked. Run "oms sync" to remove it.`);
@@ -17676,6 +17773,16 @@ __name(runDoctor, "runDoctor");
17676
17773
 
17677
17774
  // scripts/lib/help.ts
17678
17775
  var exitHelp = "\nExit codes: 0 ok | 1 usage/config error | 2 one or more git operations failed.";
17776
+ var workspaceContextHelp = `
17777
+ Workspace discovery uses the nearest oms.yaml from the current directory upward. An invalid nearest
17778
+ candidate is an error; OMS never falls back to a more distant manifest. Commands that inspect or change
17779
+ submodules require that manifest directory to be the root Git top-level. Move a nested manifest to the
17780
+ Git root, or initialize a separate Git repository at the intended workspace root.
17781
+ `;
17782
+ var initHelp = `
17783
+ The current directory must be outside a Git work tree or be its root top-level. Init refuses a child of
17784
+ an existing Git work tree before changing any file, including with --force.
17785
+ `;
17679
17786
  var statusHelp = `
17680
17787
  Machine-readable mode prints exactly one JSON object on stdout. The schemaVersion 1 payload has seven
17681
17788
  top-level keys: schemaVersion, toolVersion, workspaceRoot, currentAlias, root, repos, and errors.
@@ -17690,6 +17797,7 @@ Examples:
17690
17797
  var commitHelp = `
17691
17798
  Scope: commits inside the selected oms/<alias>/ submodule only \u2014 never the root gitlink. Existing staged
17692
17799
  changes are committed as-is (staged-first); otherwise all changes are staged with git add -A.
17800
+ An explicit alias takes precedence over an alias inferred from the current configured oms/<alias>/ path.
17693
17801
  Examples:
17694
17802
  $ oms commit api -m "feat: add login" # commit submodule source changes
17695
17803
  $ oms commit -m "fix: typo" # infer the alias from the current oms/<alias>/ directory
@@ -17697,6 +17805,7 @@ Examples:
17697
17805
  var recordHelp = `
17698
17806
  Scope: commits an existing root gitlink pointer update for one alias in the ROOT repository only
17699
17807
  (chore(oms): update <alias> submodule to <sha>). It never adds or removes a submodule registration.
17808
+ An explicit alias takes precedence over an alias inferred from the current configured oms/<alias>/ path.
17700
17809
  Example:
17701
17810
  $ oms record api
17702
17811
  `;
@@ -17764,15 +17873,29 @@ repos:
17764
17873
  branch: main
17765
17874
  `;
17766
17875
  async function runInit(options) {
17767
- const target = join11(process.cwd(), MANIFEST_FILENAME);
17876
+ const cwd = process.cwd();
17877
+ const target = join11(cwd, MANIFEST_FILENAME);
17878
+ const identity = inspectWorkspaceGitIdentity(cwd);
17879
+ if (identity.kind === "mismatch") {
17880
+ R2.error(
17881
+ `Cannot initialize a workspace at ${cwd} because it is below the root Git top-level ${identity.gitTopLevel}. Run "oms init" at ${identity.gitTopLevel}, or initialize a separate Git repository at ${cwd}.`
17882
+ );
17883
+ return 1;
17884
+ }
17885
+ if (identity.kind === "indeterminate") {
17886
+ R2.error(
17887
+ `Could not verify the workspace target ${cwd}: ${identity.reason}. No files were changed; retry after the path and Git repository are accessible.`
17888
+ );
17889
+ return 1;
17890
+ }
17768
17891
  if (existsSync11(target) && !options.force) {
17769
17892
  R2.error(`${MANIFEST_FILENAME} already exists at ${target}. Use --force to overwrite.`);
17770
17893
  return 1;
17771
17894
  }
17772
17895
  writeFileSync6(target, INIT_TEMPLATE);
17773
17896
  R2.success(`created ${MANIFEST_FILENAME} at ${target}`);
17774
- ensureOmsNotIgnored(process.cwd());
17775
- if (!isGitRepo(process.cwd())) {
17897
+ ensureOmsNotIgnored(cwd);
17898
+ if (identity.kind === "no-work-tree") {
17776
17899
  R2.info(`oms manages sources as git submodules; run "git init" here if this is not a git repo yet.`);
17777
17900
  }
17778
17901
  R2.info(`edit alias/remotes/branch, then run "oms sync".`);
@@ -17936,7 +18059,7 @@ import { spawnSync as spawnSync5 } from "node:child_process";
17936
18059
  var import_semver = __toESM(require_semver2(), 1);
17937
18060
 
17938
18061
  // scripts/lib/install-context.ts
17939
- import { existsSync as existsSync12, realpathSync } from "node:fs";
18062
+ import { existsSync as existsSync12, realpathSync as realpathSync2 } from "node:fs";
17940
18063
  import { delimiter, dirname as dirname5, join as join12, resolve as resolve5 } from "node:path";
17941
18064
  function formatCommand(command) {
17942
18065
  return [command.executable, ...command.args].join(" ");
@@ -17951,7 +18074,7 @@ function globalUpdateCommand(manager) {
17951
18074
  __name(globalUpdateCommand, "globalUpdateCommand");
17952
18075
  function safeRealpath(path) {
17953
18076
  try {
17954
- return realpathSync(path);
18077
+ return realpathSync2(path);
17955
18078
  } catch {
17956
18079
  return path;
17957
18080
  }
@@ -18456,40 +18579,40 @@ var collectRepeatable = /* @__PURE__ */ __name((value, acc) => [...acc, value],
18456
18579
  var program2 = new Command();
18457
18580
  program2.name("oms").description(
18458
18581
  `Manage source repositories listed in ${MANIFEST_FILENAME} as git submodules under ${DATA_DIRNAME}/<alias>/.`
18459
- ).version(readPackageVersion()).addHelpText("after", exitHelp);
18460
- program2.command("init").description(`Create a starter ${MANIFEST_FILENAME} in the current directory.`).option("--force", `overwrite an existing ${MANIFEST_FILENAME}`).addHelpText("after", exitHelp).action(async (options) => {
18582
+ ).version(readPackageVersion()).addHelpText("after", `${workspaceContextHelp}${exitHelp}`);
18583
+ program2.command("init").description(`Create a starter ${MANIFEST_FILENAME} in the current directory.`).option("--force", `overwrite an existing ${MANIFEST_FILENAME}`).addHelpText("after", `${initHelp}${exitHelp}`).action(async (options) => {
18461
18584
  await exitWith(runInit(options));
18462
18585
  });
18463
18586
  program2.command("doctor").description(
18464
18587
  `Check ${MANIFEST_FILENAME}, git availability, and the submodule state of each registered alias.`
18465
- ).addHelpText("after", exitHelp).action(async () => {
18588
+ ).addHelpText("after", `${workspaceContextHelp}${exitHelp}`).action(async () => {
18466
18589
  await exitWith(runDoctor());
18467
18590
  });
18468
18591
  program2.command("sync").description(
18469
18592
  `Register each repo as a submodule at ${DATA_DIRNAME}/<alias>/ (or initialize and refresh an existing one), checked out on its baseline branch.`
18470
- ).argument("[aliases...]", "repo aliases to sync (omit for interactive multi-select)").option("--all", "sync every registered source repo").option("--list", "print registered repos").option("--commit", "create the root topology commit (chore(oms): add ...) without prompting").addHelpText("after", `${syncHelp}${exitHelp}`).action(async (aliases, options) => {
18593
+ ).argument("[aliases...]", "repo aliases to sync (omit for interactive multi-select)").option("--all", "sync every registered source repo").option("--list", "print registered repos").option("--commit", "create the root topology commit (chore(oms): add ...) without prompting").addHelpText("after", `${syncHelp}${workspaceContextHelp}${exitHelp}`).action(async (aliases, options) => {
18471
18594
  await exitWith(runSync(aliases, options));
18472
18595
  });
18473
- program2.command("status").description("Show each submodule's branch, pointer state, dirtiness, and ahead/behind counts.").argument("[aliases...]", "repo aliases to inspect (omit for all)").option("--all", "inspect every registered source repo").option("--json", "print machine-readable workspace state (one JSON object on stdout)").addHelpText("after", `${statusHelp}${exitHelp}`).action(async (aliases, options) => {
18596
+ program2.command("status").description("Show each submodule's branch, pointer state, dirtiness, and ahead/behind counts.").argument("[aliases...]", "repo aliases to inspect (omit for all)").option("--all", "inspect every registered source repo").option("--json", "print machine-readable workspace state (one JSON object on stdout)").addHelpText("after", `${statusHelp}${workspaceContextHelp}${exitHelp}`).action(async (aliases, options) => {
18474
18597
  await exitWith(runStatus(aliases, options));
18475
18598
  });
18476
- program2.command("commit").description("Commit source changes inside the selected submodule only (never the root gitlink).").argument("[alias]", "registered source alias (omit to infer from the current oms/<alias>/ directory)").option("-m, --message <message>", "commit message (repeatable; required only to create a commit)", collectRepeatable, []).addHelpText("after", `${commitHelp}${exitHelp}`).action(async (alias, options) => {
18599
+ program2.command("commit").description("Commit source changes inside the selected submodule only (never the root gitlink).").argument("[alias]", "registered source alias (omit to infer from the current oms/<alias>/ directory)").option("-m, --message <message>", "commit message (repeatable; required only to create a commit)", collectRepeatable, []).addHelpText("after", `${commitHelp}${workspaceContextHelp}${exitHelp}`).action(async (alias, options) => {
18477
18600
  await exitWith(runCommit(alias, options));
18478
18601
  });
18479
- program2.command("record").description("Commit an existing root gitlink pointer update for the selected submodule (root repo only).").argument("[alias]", "registered source alias (omit to infer from the current oms/<alias>/ directory)").addHelpText("after", `${recordHelp}${exitHelp}`).action(async (alias) => {
18602
+ program2.command("record").description("Commit an existing root gitlink pointer update for the selected submodule (root repo only).").argument("[alias]", "registered source alias (omit to infer from the current oms/<alias>/ directory)").addHelpText("after", `${recordHelp}${workspaceContextHelp}${exitHelp}`).action(async (alias) => {
18480
18603
  await exitWith(runRecord(alias));
18481
18604
  });
18482
- var branchCommand = program2.command("branch").description("Inspect, switch, checkout, or delete submodule branches (interactive action selector).").addHelpText("after", exitHelp).action(async () => {
18605
+ var branchCommand = program2.command("branch").description("Inspect, switch, checkout, or delete submodule branches (interactive action selector).").addHelpText("after", `${workspaceContextHelp}${exitHelp}`).action(async () => {
18483
18606
  await exitWith(runBranch(branchCommand));
18484
18607
  });
18485
18608
  branchCommand.command("switch").description(
18486
18609
  "Switch a submodule to a LOCAL branch, creating it locally if it does not exist yet (no remote required)."
18487
- ).argument("[alias]", "registered source alias (omit to pick interactively)").argument("[branch]", "local branch name (omit to pick from local branches or create one)").option("--from <ref>", "start point for a new branch (default: current HEAD)").addHelpText("after", exitHelp).action(async (alias, branch, options) => {
18610
+ ).argument("[alias]", "registered source alias (omit to pick interactively)").argument("[branch]", "local branch name (omit to pick from local branches or create one)").option("--from <ref>", "start point for a new branch (default: current HEAD)").addHelpText("after", `${workspaceContextHelp}${exitHelp}`).action(async (alias, branch, options) => {
18488
18611
  await exitWith(runSwitch(alias, branch, options));
18489
18612
  });
18490
18613
  branchCommand.command("checkout").description(
18491
18614
  "Fetch origin, then check out a REMOTE branch (origin/*) as a local tracking branch."
18492
- ).argument("[alias]", "registered source alias (omit to pick interactively)").argument("[branch]", "remote branch name (omit to pick from origin/* branches)").addHelpText("after", exitHelp).action(async (alias, branch) => {
18615
+ ).argument("[alias]", "registered source alias (omit to pick interactively)").argument("[branch]", "remote branch name (omit to pick from origin/* branches)").addHelpText("after", `${workspaceContextHelp}${exitHelp}`).action(async (alias, branch) => {
18493
18616
  await exitWith(runCheckout(alias, branch));
18494
18617
  });
18495
18618
  branchCommand.command("list").description("Prepare one submodule, refresh declared remotes, and list local and remote branches.").argument("[alias]", "declared source alias (the sole alias is selected automatically)").addHelpText(
@@ -18504,31 +18627,31 @@ Behavior:
18504
18627
  Examples:
18505
18628
  $ oms branch list api
18506
18629
  $ oms branch list
18507
- ${exitHelp}`
18630
+ ${workspaceContextHelp}${exitHelp}`
18508
18631
  ).action(async (alias) => {
18509
18632
  await exitWith(runBranchList(alias));
18510
18633
  });
18511
18634
  branchCommand.command("delete").description(
18512
18635
  "Delete a LOCAL branch inside one initialized submodule (never a remote branch or the root gitlink)."
18513
- ).argument("[alias]", "registered source alias (omit to pick interactively)").argument("[branch]", "local branch name (omit to pick from deletable local branches)").option("-f, --force", "force-delete with git branch -D (still respects protected branches)").addHelpText("after", exitHelp).action(async (alias, branch, options) => {
18636
+ ).argument("[alias]", "registered source alias (omit to pick interactively)").argument("[branch]", "local branch name (omit to pick from deletable local branches)").option("-f, --force", "force-delete with git branch -D (still respects protected branches)").addHelpText("after", `${workspaceContextHelp}${exitHelp}`).action(async (alias, branch, options) => {
18514
18637
  await exitWith(runBranchDelete(alias, branch, options));
18515
18638
  });
18516
- program2.command("fetch").description("Run git fetch <remote> --prune in each submodule (defaults to origin).").argument("[aliases...]", "repo aliases to fetch (omit for interactive multi-select)").option("--all", "fetch every registered source repo").option("--remote <name>", "remote to fetch (repeatable; omit to choose interactively)", collectRepeatable, []).addHelpText("after", exitHelp).action(async (aliases, options) => {
18639
+ program2.command("fetch").description("Run git fetch <remote> --prune in each submodule (defaults to origin).").argument("[aliases...]", "repo aliases to fetch (omit for interactive multi-select)").option("--all", "fetch every registered source repo").option("--remote <name>", "remote to fetch (repeatable; omit to choose interactively)", collectRepeatable, []).addHelpText("after", `${workspaceContextHelp}${exitHelp}`).action(async (aliases, options) => {
18517
18640
  await exitWith(runManage("fetch", aliases, options));
18518
18641
  });
18519
18642
  program2.command("pull").description(
18520
18643
  "Pull the submodule branch only (git pull --ff-only <remote>); never stages or commits the root gitlink (defaults to origin)."
18521
- ).argument("[aliases...]", "repo aliases to pull (omit for interactive multi-select)").option("--all", "pull every registered source repo").option("--remote <name>", "remote to pull from (single; omit to choose interactively)", collectRepeatable, []).addHelpText("after", `${pullHelp}${exitHelp}`).action(async (aliases, options) => {
18644
+ ).argument("[aliases...]", "repo aliases to pull (omit for interactive multi-select)").option("--all", "pull every registered source repo").option("--remote <name>", "remote to pull from (single; omit to choose interactively)", collectRepeatable, []).addHelpText("after", `${pullHelp}${workspaceContextHelp}${exitHelp}`).action(async (aliases, options) => {
18522
18645
  await exitWith(runManage("pull", aliases, options));
18523
18646
  });
18524
18647
  program2.command("push").description(
18525
18648
  'Push the submodule branch only (creating the remote branch on first push); never stages or commits the root gitlink. Use "oms record <alias>" for root pointer commits (defaults to origin).'
18526
- ).argument("<aliases...>", "repo aliases to push").option("--commit", 'unsupported: use "oms record <alias>" after pushing').option("--record", 'unsupported: use "oms record <alias>" after pushing').option("--remote <name>", "remote to push to (repeatable; omit to choose interactively)", collectRepeatable, []).addHelpText("after", `${pushHelp}${exitHelp}`).action(async (aliases, options) => {
18649
+ ).argument("<aliases...>", "repo aliases to push").option("--commit", 'unsupported: use "oms record <alias>" after pushing').option("--record", 'unsupported: use "oms record <alias>" after pushing').option("--remote <name>", "remote to push to (repeatable; omit to choose interactively)", collectRepeatable, []).addHelpText("after", `${pushHelp}${workspaceContextHelp}${exitHelp}`).action(async (aliases, options) => {
18527
18650
  await exitWith(runManage("push", aliases, options));
18528
18651
  });
18529
18652
  program2.command("unsync").description(
18530
18653
  `Deinitialize and remove the submodule for each alias (keeps ${MANIFEST_FILENAME} entry).`
18531
- ).argument("[aliases...]", "repo aliases to unsync (omit for interactive multi-select)").option("--all", "unsync every registered source repo").option("--force", "discard uncommitted changes in the submodule").option("--commit", "create the root topology commit (chore(oms): remove ...) without prompting").addHelpText("after", `${unsyncHelp}${exitHelp}`).action(async (aliases, options) => {
18654
+ ).argument("[aliases...]", "repo aliases to unsync (omit for interactive multi-select)").option("--all", "unsync every registered source repo").option("--force", "discard uncommitted changes in the submodule").option("--commit", "create the root topology commit (chore(oms): remove ...) without prompting").addHelpText("after", `${unsyncHelp}${workspaceContextHelp}${exitHelp}`).action(async (aliases, options) => {
18532
18655
  await exitWith(runUnsync(aliases, options));
18533
18656
  });
18534
18657
  var agentCommand = program2.command("agent").description(`Manage OMS agent instruction blocks under ${DATA_DIRNAME}/ (AGENTS.md, CLAUDE.md).`).addHelpText("after", exitHelp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-space",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Manage multi-repo workspaces with Git submodules.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/divlook/oh-my-space",