oh-my-space 0.13.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,12 +70,20 @@ 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:
76
84
 
77
85
  ```bash
78
- oms switch api feature/login # local branch, no remote needed
86
+ oms branch switch api feature/login # local branch, no remote needed
79
87
  oms commit api -m "feat: add login" # commit inside oms/api (submodule only)
80
88
  oms push api # creates origin/feature/login (submodule branch only)
81
89
  oms record api # commit the moved oms/api pointer in your project history
@@ -83,7 +91,7 @@ oms record api # commit the moved oms/api pointer in your
83
91
 
84
92
  `oms commit` and `oms push` stay inside the submodule and never touch the root gitlink; `oms record` is the only command that commits an existing root pointer update. After a successful `oms commit`/`oms pull`/`oms push`, `oms` prints an `oms record <alias>` hint when the pointer has moved. Run `oms status --json` for a machine-readable view of which scope changed.
85
93
 
86
- Omit the alias or branch on `oms switch` and `oms checkout` to pick one interactively — synced submodules, and local or `origin/*` branches respectively.
94
+ Omit the alias or branch on `oms branch switch` and `oms branch checkout` to pick one interactively — synced submodules, and local or `origin/*` branches respectively.
87
95
 
88
96
  ## Listing branches
89
97
 
@@ -92,7 +100,7 @@ Omit the alias or branch on `oms switch` and `oms checkout` to pick one interact
92
100
  ```bash
93
101
  oms branch list api
94
102
  oms branch list
95
- oms branch # choose list or delete interactively
103
+ oms branch # choose list, switch, checkout, or delete interactively
96
104
  ```
97
105
 
98
106
  Before listing, OMS safely initializes an existing registered submodule when needed, reconciles every remote declared for that alias in `oms.yaml`, and runs `git fetch <remote> --prune` sequentially in manifest order. A failed fetch is retried once. If both attempts fail, cached refs remain visible as `stale`; a failed remote without usable refs is `unavailable`. These degraded states still exit 0 when local refs can be inspected. Extra local remotes are not fetched or included in the remote inventory, although an actual configured upstream on one of them remains visible for its local branch.
@@ -113,7 +121,7 @@ When `branch` is omitted from `oms.yaml`, a successful origin fetch refreshes `o
113
121
  oms branch delete api feature/login # safe delete (git branch -d)
114
122
  oms branch delete api feature/login --force # force delete (git branch -D)
115
123
  oms branch delete # pick alias, then branch, interactively
116
- oms branch # interactive list/delete action selector
124
+ oms branch # interactive list/switch/checkout/delete action selector
117
125
  ```
118
126
 
119
127
  - **Local only.** It never deletes a remote branch or a remote-tracking ref, never fetches or pushes, and never stages or commits the root gitlink. A missing local branch whose name exists on `origin` is reported as local-only.
@@ -133,7 +141,7 @@ oms branch # interactive list/delete action sel
133
141
 
134
142
  `oms` does not replace Git submodules. It adds a small command layer for the workflow details that make submodules awkward. Submodules already give you a reproducible pin (the parent records each source's exact commit), visibility (`git status` shows when a pointer moved), and history (the pointer travels with your commits). The friction is in everyday branch work, and that is what `oms` smooths over:
135
143
 
136
- - **Start branches locally.** `oms switch <alias> <branch>` starts a local branch right away, even before it exists on the remote. `oms checkout <alias> <branch>` fetches origin and checks out an existing remote branch as a tracking branch. The remote branch is created lazily on your first `oms push`.
144
+ - **Start branches locally.** `oms branch switch <alias> <branch>` starts a local branch right away, even before it exists on the remote. `oms branch checkout <alias> <branch>` fetches origin and checks out an existing remote branch as a tracking branch. The remote branch is created lazily on your first `oms push`.
137
145
  - **Stay on a branch.** `oms sync` attaches the baseline branch at the pinned commit instead of leaving a detached HEAD, so everyday submodule work never strands you off a branch.
138
146
  - **Keep pointer moves visible, commit them explicitly.** `oms pull` and `oms push` synchronize only the submodule branch — they never stage or commit the root gitlink. The moved pointer shows up in `git status`, `oms status` flags when a submodule has drifted from the recorded pointer, and `oms record <alias>` commits that pointer update in the parent repo.
139
147
 
@@ -173,7 +181,7 @@ The three skills are named by the Git domain each manages:
173
181
  | --- | --- | --- |
174
182
  | `oms-workspace` | Scope-ambiguous Git work in the workspace — committing from the root, a moved `oms status` pointer, a push, or adding/removing a repo with `oms sync`/`oms unsync`. | Establishes workspace state and root-versus-submodule scope before acting, and separates repo add/remove topology from recording a moved pointer. |
175
183
  | `oms-pointer` | After `oms commit` or `oms pull` moves a submodule's commit. | Records the moved root pointer with `oms record`, so a submodule change is not left without a recorded pointer and the root pointer is not committed by mistake. |
176
- | `oms-branch` | Starting or switching a branch inside a submodule. | Chooses `oms switch` (new local branch) versus `oms checkout` (track a remote branch) and avoids detached HEAD. |
184
+ | `oms-branch` | Starting or switching a branch inside a submodule. | Chooses `oms branch switch` (new local branch) versus `oms branch checkout` (track a remote branch) and avoids detached HEAD. |
177
185
 
178
186
  Skill firing is best-effort — an agent loads a skill only when it judges the skill's description relevant — so the skills complement, rather than replace, `oms <command> --help` and the always-on marker block inside `oms/`. Each skill defers exact `oms status --json` field semantics to `oms status --help`, the version-matched authoritative source that ships with the installed CLI.
179
187
 
@@ -183,14 +191,14 @@ 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`. |
192
- | `oms 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
- | `oms 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 switch`. |
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`. |
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). |
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`. |
195
203
  | `oms branch delete [alias] [branch]` | workspace root | Deletes one LOCAL branch inside one initialized submodule with `git branch -d` (safe) or `-D` (`--force`). | Local only — never deletes a remote or remote-tracking ref, never touches the root gitlink. Protects the current branch and every resolved baseline (see below). Omit alias/branch to pick interactively; a registered-but-uninitialized alias is initialized first. Exit 0 on success, 1 on input/precondition errors, 2 on a declined/failed/concurrent deletion. |
196
204
  | `oms fetch ...` | workspace root | `git fetch <remote> --prune` in each submodule. | `--remote <name>` (repeatable) picks the remote(s); omit to choose interactively, defaults to `origin`. |
@@ -256,6 +264,7 @@ JSON schema: [`oms.schema.json`](./oms.schema.json) (also reachable at `https://
256
264
 
257
265
  Detailed migration steps are organized per version under [`docs/migrations/`](./docs/migrations/).
258
266
 
267
+ - [0.13.x → 0.14.0](./docs/migrations/0.13.x-to-0.14.0.md) — relocates `oms switch` / `oms checkout` under the `oms branch` group as `oms branch switch` / `oms branch checkout` (top-level commands removed, no aliases)
259
268
  - [0.11.x → 0.12.0](./docs/migrations/0.11.x-to-0.12.0.md) — adds `oms branch delete` and makes `oms sync` reconcile declarative `.gitmodules` metadata with stricter baseline validation and a durable finalization recovery preflight
260
269
  - [0.9.x → 0.10.0](./docs/migrations/0.9.x-to-0.10.0.md) — scopes each command to a single Git boundary and makes root pointer commits explicit via `oms record`
261
270
  - [0.7.x → 0.8.0](./docs/migrations/0.7.x-to-0.8.0.md) — splits `oms checkout` into `oms switch` (local branches) and `oms checkout` (remote branches)
@@ -1 +1 @@
1
- {"commit":"be1d798a55c5e7fae0b33ef6057661a1ac25a5d7"}
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
  }
@@ -16033,7 +16121,7 @@ async function resolveInitializedAlias(repos, repoRoot, alias, actionLabel) {
16033
16121
  }
16034
16122
  const initialized2 = repos.filter((r) => submoduleInitialized(repoRoot, r.alias));
16035
16123
  if (initialized2.length === 0) {
16036
- R2.error(`No synced submodules to ${actionLabel}. Run "oms sync" first.`);
16124
+ R2.error(`No synced submodules available for "oms ${actionLabel}". Run "oms sync" first.`);
16037
16125
  return null;
16038
16126
  }
16039
16127
  if (!process.stdin.isTTY) {
@@ -16041,7 +16129,7 @@ async function resolveInitializedAlias(repos, repoRoot, alias, actionLabel) {
16041
16129
  return null;
16042
16130
  }
16043
16131
  const choice = await xe({
16044
- message: `Select a source repo to ${actionLabel}`,
16132
+ message: `Select a source repo for "oms ${actionLabel}"`,
16045
16133
  options: initialized2.map((r) => ({
16046
16134
  value: r.alias,
16047
16135
  label: r.alias,
@@ -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;
@@ -17118,6 +17201,72 @@ async function runBranchList(aliasArg) {
17118
17201
  }
17119
17202
  __name(runBranchList, "runBranchList");
17120
17203
 
17204
+ // scripts/lib/branch-ops.ts
17205
+ async function runSwitch(alias, branch, options) {
17206
+ const loaded = loadForSubmodules();
17207
+ if (!loaded) return 1;
17208
+ const { repos, repoRoot } = loaded;
17209
+ const repo = await resolveInitializedAlias(repos, repoRoot, alias, "branch switch");
17210
+ if (!repo) return 1;
17211
+ const dir = aliasDir(repoRoot, repo.alias);
17212
+ let target = branch;
17213
+ if (!target) {
17214
+ const picked = await pickBranch(listLocalBranches(dir), `${repo.alias}: select a local branch`, true);
17215
+ if (!picked) return 1;
17216
+ target = picked;
17217
+ }
17218
+ if (localBranchExists(dir, target)) {
17219
+ R2.step(`${repo.alias}: git switch ${target}`);
17220
+ const r2 = runSub(repoRoot, repo.alias, ["switch", target], true);
17221
+ if (!r2.success) return 2;
17222
+ R2.success(`${repo.alias}: on ${target}`);
17223
+ return 0;
17224
+ }
17225
+ const args = ["switch", "-c", target, ...options.from ? [options.from] : []];
17226
+ R2.step(`${repo.alias}: git ${args.join(" ")}`);
17227
+ const r = runSub(repoRoot, repo.alias, args, true);
17228
+ if (!r.success) return 2;
17229
+ R2.success(`${repo.alias}: created new local branch ${target}. Push it with "oms push ${repo.alias}".`);
17230
+ return 0;
17231
+ }
17232
+ __name(runSwitch, "runSwitch");
17233
+ async function runCheckout(alias, branch) {
17234
+ const loaded = loadForSubmodules();
17235
+ if (!loaded) return 1;
17236
+ const { repos, repoRoot } = loaded;
17237
+ const repo = await resolveInitializedAlias(repos, repoRoot, alias, "branch checkout");
17238
+ if (!repo) return 1;
17239
+ const dir = aliasDir(repoRoot, repo.alias);
17240
+ R2.step(`${repo.alias}: git fetch origin --prune`);
17241
+ const fetch2 = runSub(repoRoot, repo.alias, ["fetch", "origin", "--prune"], true);
17242
+ if (!fetch2.success) return 2;
17243
+ let target = branch;
17244
+ if (!target) {
17245
+ const picked = await pickBranch(listRemoteBranches(dir), `${repo.alias}: select a remote branch (origin/*)`, false);
17246
+ if (!picked) return 1;
17247
+ target = picked;
17248
+ }
17249
+ if (localBranchExists(dir, target)) {
17250
+ R2.step(`${repo.alias}: git switch ${target}`);
17251
+ const r = runSub(repoRoot, repo.alias, ["switch", target], true);
17252
+ if (!r.success) return 2;
17253
+ R2.success(`${repo.alias}: on ${target}`);
17254
+ return 0;
17255
+ }
17256
+ if (remoteBranchExists(dir, target)) {
17257
+ R2.step(`${repo.alias}: git switch -c ${target} origin/${target}`);
17258
+ const r = runSub(repoRoot, repo.alias, ["switch", "-c", target, `origin/${target}`], true);
17259
+ if (!r.success) return 2;
17260
+ R2.success(`${repo.alias}: on ${target} (tracking origin/${target})`);
17261
+ return 0;
17262
+ }
17263
+ R2.error(
17264
+ `${repo.alias}: "${target}" not found on origin. To create a new local branch, run "oms branch switch ${repo.alias} ${target}".`
17265
+ );
17266
+ return 1;
17267
+ }
17268
+ __name(runCheckout, "runCheckout");
17269
+
17121
17270
  // scripts/lib/branch-delete.ts
17122
17271
  function interactive2() {
17123
17272
  return Boolean(process.stdin.isTTY) || promptQueueActive();
@@ -17136,6 +17285,8 @@ async function runBranch(command) {
17136
17285
  message: "Select a branch action",
17137
17286
  options: [
17138
17287
  { value: "list", label: "list branches in a submodule" },
17288
+ { value: "switch", label: "switch to or create a local branch in a submodule" },
17289
+ { value: "checkout", label: "check out a remote branch in a submodule" },
17139
17290
  { value: "delete", label: "delete a local branch in a submodule" }
17140
17291
  ]
17141
17292
  });
@@ -17144,6 +17295,8 @@ async function runBranch(command) {
17144
17295
  return 1;
17145
17296
  }
17146
17297
  if (action === "list") return runBranchList(void 0);
17298
+ if (action === "switch") return runSwitch(void 0, void 0, {});
17299
+ if (action === "checkout") return runCheckout(void 0, void 0);
17147
17300
  if (action === "delete") return runBranchDelete(void 0, void 0, {});
17148
17301
  return 1;
17149
17302
  }
@@ -17218,7 +17371,7 @@ function checkSubmodulePreconditions(repoRoot, repo) {
17218
17371
  if (!anchored) {
17219
17372
  return {
17220
17373
  ok: false,
17221
- reason: `${repo.alias}: submodule HEAD is detached and not anchored to the recorded gitlink. Attach it with "oms switch ${repo.alias} <branch>" first.`
17374
+ reason: `${repo.alias}: submodule HEAD is detached and not anchored to the recorded gitlink. Attach it with "oms branch switch ${repo.alias} <branch>" first.`
17222
17375
  };
17223
17376
  }
17224
17377
  }
@@ -17408,72 +17561,6 @@ async function runBranchDelete(aliasArg, branchArg, options) {
17408
17561
  }
17409
17562
  __name(runBranchDelete, "runBranchDelete");
17410
17563
 
17411
- // scripts/lib/branch-ops.ts
17412
- async function runSwitch(alias, branch, options) {
17413
- const loaded = loadForSubmodules();
17414
- if (!loaded) return 1;
17415
- const { repos, repoRoot } = loaded;
17416
- const repo = await resolveInitializedAlias(repos, repoRoot, alias, "switch");
17417
- if (!repo) return 1;
17418
- const dir = aliasDir(repoRoot, repo.alias);
17419
- let target = branch;
17420
- if (!target) {
17421
- const picked = await pickBranch(listLocalBranches(dir), `${repo.alias}: select a local branch`, true);
17422
- if (!picked) return 1;
17423
- target = picked;
17424
- }
17425
- if (localBranchExists(dir, target)) {
17426
- R2.step(`${repo.alias}: git switch ${target}`);
17427
- const r2 = runSub(repoRoot, repo.alias, ["switch", target], true);
17428
- if (!r2.success) return 2;
17429
- R2.success(`${repo.alias}: on ${target}`);
17430
- return 0;
17431
- }
17432
- const args = ["switch", "-c", target, ...options.from ? [options.from] : []];
17433
- R2.step(`${repo.alias}: git ${args.join(" ")}`);
17434
- const r = runSub(repoRoot, repo.alias, args, true);
17435
- if (!r.success) return 2;
17436
- R2.success(`${repo.alias}: created new local branch ${target}. Push it with "oms push ${repo.alias}".`);
17437
- return 0;
17438
- }
17439
- __name(runSwitch, "runSwitch");
17440
- async function runCheckout(alias, branch) {
17441
- const loaded = loadForSubmodules();
17442
- if (!loaded) return 1;
17443
- const { repos, repoRoot } = loaded;
17444
- const repo = await resolveInitializedAlias(repos, repoRoot, alias, "checkout");
17445
- if (!repo) return 1;
17446
- const dir = aliasDir(repoRoot, repo.alias);
17447
- R2.step(`${repo.alias}: git fetch origin --prune`);
17448
- const fetch2 = runSub(repoRoot, repo.alias, ["fetch", "origin", "--prune"], true);
17449
- if (!fetch2.success) return 2;
17450
- let target = branch;
17451
- if (!target) {
17452
- const picked = await pickBranch(listRemoteBranches(dir), `${repo.alias}: select a remote branch (origin/*)`, false);
17453
- if (!picked) return 1;
17454
- target = picked;
17455
- }
17456
- if (localBranchExists(dir, target)) {
17457
- R2.step(`${repo.alias}: git switch ${target}`);
17458
- const r = runSub(repoRoot, repo.alias, ["switch", target], true);
17459
- if (!r.success) return 2;
17460
- R2.success(`${repo.alias}: on ${target}`);
17461
- return 0;
17462
- }
17463
- if (remoteBranchExists(dir, target)) {
17464
- R2.step(`${repo.alias}: git switch -c ${target} origin/${target}`);
17465
- const r = runSub(repoRoot, repo.alias, ["switch", "-c", target, `origin/${target}`], true);
17466
- if (!r.success) return 2;
17467
- R2.success(`${repo.alias}: on ${target} (tracking origin/${target})`);
17468
- return 0;
17469
- }
17470
- R2.error(
17471
- `${repo.alias}: "${target}" not found on origin. To create a new local branch, run "oms switch ${repo.alias} ${target}".`
17472
- );
17473
- return 1;
17474
- }
17475
- __name(runCheckout, "runCheckout");
17476
-
17477
17564
  // scripts/lib/commit.ts
17478
17565
  async function runCommit(alias, options) {
17479
17566
  const loaded = loadForSubmodules();
@@ -17496,7 +17583,7 @@ async function runCommit(alias, options) {
17496
17583
  return 1;
17497
17584
  }
17498
17585
  if (currentBranch(dir) === null) {
17499
- R2.error(`${selected}: detached HEAD. Run "oms switch ${selected} <branch>" before committing.`);
17586
+ R2.error(`${selected}: detached HEAD. Run "oms branch switch ${selected} <branch>" before committing.`);
17500
17587
  return 1;
17501
17588
  }
17502
17589
  const messages = options.message ?? [];
@@ -17609,7 +17696,7 @@ async function runDoctor() {
17609
17696
  }
17610
17697
  const { repos, repoRoot } = loaded;
17611
17698
  if (abortOnLegacyRenameAt(repoRoot)) return 1;
17612
- R2.success(`Workspace root: ${repoRoot}`);
17699
+ R2.success(`Workspace manifest directory: ${repoRoot}`);
17613
17700
  R2.success(`${MANIFEST_FILENAME}: ${repos.length} repo(s) configured`);
17614
17701
  const git = spawnSync3("git", ["--version"], {
17615
17702
  encoding: "utf8",
@@ -17633,12 +17720,26 @@ async function runDoctor() {
17633
17720
  );
17634
17721
  warnings++;
17635
17722
  }
17636
- if (!isGitRepo(repoRoot)) {
17723
+ const identity = inspectWorkspaceGitIdentity(repoRoot);
17724
+ if (identity.kind === "no-work-tree") {
17637
17725
  R2.warn(
17638
17726
  `workspace is not a git repository. oms manages sources as submodules; run "git init" at ${repoRoot}.`
17639
17727
  );
17640
- warnings++;
17728
+ return 2;
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;
17641
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}`);
17642
17743
  if (abortOnLegacyWorktree(repoRoot, repos)) return 1;
17643
17744
  if (gitignoreIgnoresOms(repoRoot)) {
17644
17745
  R2.warn(`.gitignore excludes ${GITIGNORE_ENTRY}, but submodules must be tracked. Run "oms sync" to remove it.`);
@@ -17657,7 +17758,7 @@ async function runDoctor() {
17657
17758
  const dir = aliasDir(repoRoot, repo.alias);
17658
17759
  const branch = currentBranch(dir);
17659
17760
  if (!branch) {
17660
- R2.warn(`${repo.alias}: detached HEAD. Run "oms switch ${repo.alias} <branch>" to get on a branch.`);
17761
+ R2.warn(`${repo.alias}: detached HEAD. Run "oms branch switch ${repo.alias} <branch>" to get on a branch.`);
17661
17762
  warnings++;
17662
17763
  } else {
17663
17764
  R2.success(`${repo.alias}: submodule OK (branch=${branch})`);
@@ -17672,6 +17773,16 @@ __name(runDoctor, "runDoctor");
17672
17773
 
17673
17774
  // scripts/lib/help.ts
17674
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
+ `;
17675
17786
  var statusHelp = `
17676
17787
  Machine-readable mode prints exactly one JSON object on stdout. The schemaVersion 1 payload has seven
17677
17788
  top-level keys: schemaVersion, toolVersion, workspaceRoot, currentAlias, root, repos, and errors.
@@ -17686,6 +17797,7 @@ Examples:
17686
17797
  var commitHelp = `
17687
17798
  Scope: commits inside the selected oms/<alias>/ submodule only \u2014 never the root gitlink. Existing staged
17688
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.
17689
17801
  Examples:
17690
17802
  $ oms commit api -m "feat: add login" # commit submodule source changes
17691
17803
  $ oms commit -m "fix: typo" # infer the alias from the current oms/<alias>/ directory
@@ -17693,6 +17805,7 @@ Examples:
17693
17805
  var recordHelp = `
17694
17806
  Scope: commits an existing root gitlink pointer update for one alias in the ROOT repository only
17695
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.
17696
17809
  Example:
17697
17810
  $ oms record api
17698
17811
  `;
@@ -17760,15 +17873,29 @@ repos:
17760
17873
  branch: main
17761
17874
  `;
17762
17875
  async function runInit(options) {
17763
- 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
+ }
17764
17891
  if (existsSync11(target) && !options.force) {
17765
17892
  R2.error(`${MANIFEST_FILENAME} already exists at ${target}. Use --force to overwrite.`);
17766
17893
  return 1;
17767
17894
  }
17768
17895
  writeFileSync6(target, INIT_TEMPLATE);
17769
17896
  R2.success(`created ${MANIFEST_FILENAME} at ${target}`);
17770
- ensureOmsNotIgnored(process.cwd());
17771
- if (!isGitRepo(process.cwd())) {
17897
+ ensureOmsNotIgnored(cwd);
17898
+ if (identity.kind === "no-work-tree") {
17772
17899
  R2.info(`oms manages sources as git submodules; run "git init" here if this is not a git repo yet.`);
17773
17900
  }
17774
17901
  R2.info(`edit alias/remotes/branch, then run "oms sync".`);
@@ -17805,7 +17932,7 @@ function pullRepo(repo, repoRoot, remote) {
17805
17932
  const branch = currentBranch(aliasDir(repoRoot, repo.alias));
17806
17933
  if (!branch) {
17807
17934
  R2.error(
17808
- `${repo.alias}: detached HEAD. Run "oms switch ${repo.alias} <branch>" before pulling.`
17935
+ `${repo.alias}: detached HEAD. Run "oms branch switch ${repo.alias} <branch>" before pulling.`
17809
17936
  );
17810
17937
  return "failed";
17811
17938
  }
@@ -17834,7 +17961,7 @@ function pushRepo(repo, repoRoot, remotes) {
17834
17961
  const branch = currentBranch(aliasDir(repoRoot, repo.alias));
17835
17962
  if (!branch) {
17836
17963
  R2.error(
17837
- `${repo.alias}: detached HEAD. Run "oms switch ${repo.alias} <branch>" before pushing.`
17964
+ `${repo.alias}: detached HEAD. Run "oms branch switch ${repo.alias} <branch>" before pushing.`
17838
17965
  );
17839
17966
  return "failed";
17840
17967
  }
@@ -17932,7 +18059,7 @@ import { spawnSync as spawnSync5 } from "node:child_process";
17932
18059
  var import_semver = __toESM(require_semver2(), 1);
17933
18060
 
17934
18061
  // scripts/lib/install-context.ts
17935
- import { existsSync as existsSync12, realpathSync } from "node:fs";
18062
+ import { existsSync as existsSync12, realpathSync as realpathSync2 } from "node:fs";
17936
18063
  import { delimiter, dirname as dirname5, join as join12, resolve as resolve5 } from "node:path";
17937
18064
  function formatCommand(command) {
17938
18065
  return [command.executable, ...command.args].join(" ");
@@ -17947,7 +18074,7 @@ function globalUpdateCommand(manager) {
17947
18074
  __name(globalUpdateCommand, "globalUpdateCommand");
17948
18075
  function safeRealpath(path) {
17949
18076
  try {
17950
- return realpathSync(path);
18077
+ return realpathSync2(path);
17951
18078
  } catch {
17952
18079
  return path;
17953
18080
  }
@@ -18438,8 +18565,6 @@ var commandNames = /* @__PURE__ */ new Set([
18438
18565
  "status",
18439
18566
  "commit",
18440
18567
  "record",
18441
- "switch",
18442
- "checkout",
18443
18568
  "branch",
18444
18569
  "fetch",
18445
18570
  "pull",
@@ -18454,42 +18579,42 @@ var collectRepeatable = /* @__PURE__ */ __name((value, acc) => [...acc, value],
18454
18579
  var program2 = new Command();
18455
18580
  program2.name("oms").description(
18456
18581
  `Manage source repositories listed in ${MANIFEST_FILENAME} as git submodules under ${DATA_DIRNAME}/<alias>/.`
18457
- ).version(readPackageVersion()).addHelpText("after", exitHelp);
18458
- 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) => {
18459
18584
  await exitWith(runInit(options));
18460
18585
  });
18461
18586
  program2.command("doctor").description(
18462
18587
  `Check ${MANIFEST_FILENAME}, git availability, and the submodule state of each registered alias.`
18463
- ).addHelpText("after", exitHelp).action(async () => {
18588
+ ).addHelpText("after", `${workspaceContextHelp}${exitHelp}`).action(async () => {
18464
18589
  await exitWith(runDoctor());
18465
18590
  });
18466
18591
  program2.command("sync").description(
18467
18592
  `Register each repo as a submodule at ${DATA_DIRNAME}/<alias>/ (or initialize and refresh an existing one), checked out on its baseline branch.`
18468
- ).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) => {
18469
18594
  await exitWith(runSync(aliases, options));
18470
18595
  });
18471
- 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) => {
18472
18597
  await exitWith(runStatus(aliases, options));
18473
18598
  });
18474
- 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) => {
18475
18600
  await exitWith(runCommit(alias, options));
18476
18601
  });
18477
- 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) => {
18478
18603
  await exitWith(runRecord(alias));
18479
18604
  });
18480
- program2.command("switch").description(
18605
+ var branchCommand = program2.command("branch").description("Inspect, switch, checkout, or delete submodule branches (interactive action selector).").addHelpText("after", `${workspaceContextHelp}${exitHelp}`).action(async () => {
18606
+ await exitWith(runBranch(branchCommand));
18607
+ });
18608
+ branchCommand.command("switch").description(
18481
18609
  "Switch a submodule to a LOCAL branch, creating it locally if it does not exist yet (no remote required)."
18482
- ).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) => {
18483
18611
  await exitWith(runSwitch(alias, branch, options));
18484
18612
  });
18485
- program2.command("checkout").description(
18613
+ branchCommand.command("checkout").description(
18486
18614
  "Fetch origin, then check out a REMOTE branch (origin/*) as a local tracking branch."
18487
- ).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) => {
18488
18616
  await exitWith(runCheckout(alias, branch));
18489
18617
  });
18490
- var branchCommand = program2.command("branch").description("Inspect or delete submodule branches (interactive action selector).").addHelpText("after", exitHelp).action(async () => {
18491
- await exitWith(runBranch(branchCommand));
18492
- });
18493
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(
18494
18619
  "after",
18495
18620
  `
@@ -18502,31 +18627,31 @@ Behavior:
18502
18627
  Examples:
18503
18628
  $ oms branch list api
18504
18629
  $ oms branch list
18505
- ${exitHelp}`
18630
+ ${workspaceContextHelp}${exitHelp}`
18506
18631
  ).action(async (alias) => {
18507
18632
  await exitWith(runBranchList(alias));
18508
18633
  });
18509
18634
  branchCommand.command("delete").description(
18510
18635
  "Delete a LOCAL branch inside one initialized submodule (never a remote branch or the root gitlink)."
18511
- ).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) => {
18512
18637
  await exitWith(runBranchDelete(alias, branch, options));
18513
18638
  });
18514
- 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) => {
18515
18640
  await exitWith(runManage("fetch", aliases, options));
18516
18641
  });
18517
18642
  program2.command("pull").description(
18518
18643
  "Pull the submodule branch only (git pull --ff-only <remote>); never stages or commits the root gitlink (defaults to origin)."
18519
- ).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) => {
18520
18645
  await exitWith(runManage("pull", aliases, options));
18521
18646
  });
18522
18647
  program2.command("push").description(
18523
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).'
18524
- ).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) => {
18525
18650
  await exitWith(runManage("push", aliases, options));
18526
18651
  });
18527
18652
  program2.command("unsync").description(
18528
18653
  `Deinitialize and remove the submodule for each alias (keeps ${MANIFEST_FILENAME} entry).`
18529
- ).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) => {
18530
18655
  await exitWith(runUnsync(aliases, options));
18531
18656
  });
18532
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.13.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",