oh-my-space 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -29
  2. package/dist/oms.js +130 -47
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # oh-my-space
2
2
 
3
- `oh-my-space` provides the `oms` CLI for managing external source repositories declared in `sources.yaml`. Each registered repo lives as a bare clone under `sources/<alias>/.bare/` and the baseline branch (and any additional ones you ask for) checks out as a Git worktree at `sources/<alias>/<branch>/`.
3
+ `oh-my-space` provides the `oms` CLI for managing external source repositories declared in `oms.yaml`. Each registered repo lives as a bare clone under `oms/<alias>/.bare/` and the baseline branch (and any additional ones you ask for) checks out as a Git worktree at `oms/<alias>/<branch>/`.
4
4
 
5
5
  Package name: `oh-my-space`
6
6
 
@@ -32,7 +32,7 @@ bun install -g oh-my-space
32
32
 
33
33
  ## Quick start
34
34
 
35
- Create a `sources.yaml` in your project root:
35
+ Create an `oms.yaml` in your project root:
36
36
 
37
37
  ```yaml
38
38
  repos:
@@ -46,7 +46,7 @@ repos:
46
46
  Sync and explore:
47
47
 
48
48
  ```bash
49
- oms doctor # validate sources.yaml + bare/worktree state
49
+ oms doctor # validate oms.yaml + bare/worktree state
50
50
  oms sync --list # list registered source repos
51
51
  oms sync <alias>... # bare-clone + create the baseline worktree
52
52
  oms sync --all # sync every registered source repo
@@ -57,14 +57,14 @@ oms worktree remove <alias> <br> # remove a single worktree
57
57
  oms unsync <alias>... # remove ALL worktrees + the bare clone
58
58
  ```
59
59
 
60
- `oms sync` adds `sources/` to your workspace `.gitignore` on first run; the bare clone and worktrees stay out of your project history.
60
+ `oms sync` adds `oms/` to your workspace `.gitignore` on first run; the bare clone and worktrees stay out of your project history.
61
61
 
62
62
  ## Layout
63
63
 
64
64
  For an alias `api` with the baseline branch `main` and an extra `feature/foo` worktree:
65
65
 
66
66
  ```
67
- sources/
67
+ oms/
68
68
  └── api/
69
69
  ├── .bare/ # git clone --bare result
70
70
  ├── .git # 'gitdir: ./.bare' placeholder
@@ -77,13 +77,13 @@ Slash-containing branch names are kept nested as directories.
77
77
 
78
78
  ## Managing source repositories
79
79
 
80
- `sources.yaml` declares each source repo with `alias`, `url`, and optional `branch` (the baseline). Additional worktrees are created on demand with `oms worktree add` and are *not* persisted in `sources.yaml` — they're treated as per-machine working space.
80
+ `oms.yaml` declares each source repo with `alias`, `url`, and optional `branch` (the baseline). Additional worktrees are created on demand with `oms worktree add` and are *not* persisted in `oms.yaml` — they're treated as per-machine working space.
81
81
 
82
82
  | Command | Runs in | Does | Notes |
83
83
  | --- | --- | --- | --- |
84
- | `oms doctor` | project root or child path | Checks `sources.yaml`, git availability, `.gitignore`, and each alias's bare clone + `remote.origin.fetch`. | Returns exit 2 if any warning is raised. |
85
- | `oms sync <alias>` / `--all` | workspace root | Bare-clones missing repos, fetches origin, creates the baseline worktree. | Adds `sources/` to `.gitignore` if absent. |
86
- | `oms unsync <alias>` / `--all` | workspace root | Removes every worktree, the bare clone, and the `.git` placeholder for the alias. | Keeps the `sources.yaml` entry. Use `--force` to discard uncommitted changes. |
84
+ | `oms doctor` | project root or child path | Checks `oms.yaml`, git availability, `.gitignore`, and each alias's bare clone + `remote.origin.fetch`. | Returns exit 2 if any warning is raised. |
85
+ | `oms sync <alias>` / `--all` | workspace root | Bare-clones missing repos, fetches origin, creates the baseline worktree. | Adds `oms/` to `.gitignore` if absent. |
86
+ | `oms unsync <alias>` / `--all` | workspace root | Removes every worktree, the bare clone, and the `.git` placeholder for the alias. | Keeps the `oms.yaml` entry. Use `--force` to discard uncommitted changes. |
87
87
  | `oms fetch ...` | workspace root | `git fetch origin --prune` on each bare clone. | Worktrees are not touched. |
88
88
  | `oms pull ...` | workspace root | `git pull --ff-only` on the baseline worktree of each selected alias. | Requires the worktree to be on its branch with upstream set (the default after `oms sync`). |
89
89
  | `oms push <alias>...` | workspace root | `git push` on the baseline worktree of each explicitly listed alias. | No `--all`, no force, no automatic upstream setup. |
@@ -91,7 +91,7 @@ Slash-containing branch names are kept nested as directories.
91
91
  | `oms worktree list [alias]` | anywhere under root | Lists every worktree for the alias (or all aliases). | |
92
92
  | `oms worktree remove <alias> <branch>` | workspace root | `git worktree remove <branch>` plus parent-directory cleanup for slash branches. | `--force` discards uncommitted changes. |
93
93
 
94
- ## `sources.yaml` format
94
+ ## `oms.yaml` format
95
95
 
96
96
  ```yaml
97
97
  repos:
@@ -109,28 +109,14 @@ Rules:
109
109
  - `url` is required.
110
110
  - `branch` is optional; when omitted the bare clone's default branch is used as the baseline worktree.
111
111
 
112
- ## Migrating from 0.2.x
112
+ JSON schema: [`oms.schema.json`](./oms.schema.json) (also reachable at `https://raw.githubusercontent.com/divlook/oh-my-space/main/oms.schema.json` for YAML LSPs).
113
113
 
114
- 0.2.x stored each alias as a Git submodule. 0.3.0 replaces that with a bare clone + worktrees. The data model is breaking — `oms sync` refuses to run while `.gitmodules` still registers a `sources/<alias>` path. Convert your workspace manually:
114
+ ## Migrating between versions
115
115
 
116
- ```bash
117
- # 1. For each alias in sources.yaml, deinit and remove the submodule.
118
- git submodule deinit -f sources/<alias>
119
- git rm -f sources/<alias>
120
- rm -rf .git/modules/<alias>
121
-
122
- # 2. Drop the now-empty .gitmodules if nothing else uses it.
123
- [ -s .gitmodules ] || rm -f .gitmodules
124
-
125
- # 3. Add sources/ to .gitignore (oms sync will also do this).
126
- grep -qxF 'sources/' .gitignore 2>/dev/null || echo 'sources/' >> .gitignore
116
+ 자세한 마이그레이션 절차는 [`docs/migrations/`](./docs/migrations/) 에 버전별로 정리되어 있습니다.
127
117
 
128
- # 4. Re-sync with the new model.
129
- oms sync --all
130
-
131
- # 5. Commit the result.
132
- git add . && git commit -m "chore: migrate to oh-my-space 0.3.0 (bare + worktree)"
133
- ```
118
+ - [0.3.x → 0.4.0](./docs/migrations/0.3.x-to-0.4.0.md) — `sources.yaml`/`sources/` 가 `oms.yaml`/`oms/` 로 rename
119
+ - [0.2.x → 0.3.0](./docs/migrations/0.2.x-to-0.3.0.md) — submodule 에서 bare clone + worktree 로 데이터 모델 전환
134
120
 
135
121
  ## License
136
122
 
package/dist/oms.js CHANGED
@@ -10,7 +10,13 @@ const ALIAS_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
10
10
  const ALLOWED_TOP_KEYS = new Set(["repos"]);
11
11
  const ALLOWED_ITEM_KEYS = new Set(["alias", "url", "branch"]);
12
12
  const FETCH_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
13
- const GITIGNORE_ENTRY = "sources/";
13
+ const MANIFEST_FILENAME = "oms.yaml";
14
+ const DATA_DIRNAME = "oms";
15
+ const GITIGNORE_ENTRY = `${DATA_DIRNAME}/`;
16
+ const LEGACY_MANIFEST = "sources.yaml";
17
+ const LEGACY_DATA_DIRNAME = "sources";
18
+ const LEGACY_MIGRATION_DOC = "docs/migrations/0.3.x-to-0.4.0.md";
19
+ const SUBMODULE_MIGRATION_DOC = "docs/migrations/0.2.x-to-0.3.0.md";
14
20
  const MIN_GIT_MAJOR = 2;
15
21
  const MIN_GIT_MINOR = 40;
16
22
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -18,51 +24,51 @@ const useColor = process.stdout.isTTY;
18
24
  const dim = (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s);
19
25
  function validateSources(data) {
20
26
  if (!data || typeof data !== "object" || Array.isArray(data)) {
21
- throw new Error("sources.yaml: root must be a mapping");
27
+ throw new Error(`${MANIFEST_FILENAME}: root must be a mapping`);
22
28
  }
23
29
  const obj = data;
24
30
  for (const key of Object.keys(obj)) {
25
31
  if (!ALLOWED_TOP_KEYS.has(key)) {
26
- throw new Error(`sources.yaml: unknown top-level key "${key}"`);
32
+ throw new Error(`${MANIFEST_FILENAME}: unknown top-level key "${key}"`);
27
33
  }
28
34
  }
29
35
  const { repos } = obj;
30
36
  if (!Array.isArray(repos)) {
31
- throw new Error('sources.yaml: "repos" must be an array');
37
+ throw new Error(`${MANIFEST_FILENAME}: "repos" must be an array`);
32
38
  }
33
39
  if (repos.length === 0) {
34
- throw new Error('sources.yaml: "repos" must have at least one item');
40
+ throw new Error(`${MANIFEST_FILENAME}: "repos" must have at least one item`);
35
41
  }
36
42
  const validated = [];
37
43
  const seen = new Set();
38
44
  repos.forEach((item, idx) => {
39
45
  const where = `repos[${idx}]`;
40
46
  if (!item || typeof item !== "object" || Array.isArray(item)) {
41
- throw new Error(`sources.yaml: ${where} must be a mapping`);
47
+ throw new Error(`${MANIFEST_FILENAME}: ${where} must be a mapping`);
42
48
  }
43
49
  const r = item;
44
50
  for (const key of Object.keys(r)) {
45
51
  if (!ALLOWED_ITEM_KEYS.has(key)) {
46
- throw new Error(`sources.yaml: ${where} has unknown key "${key}"`);
52
+ throw new Error(`${MANIFEST_FILENAME}: ${where} has unknown key "${key}"`);
47
53
  }
48
54
  }
49
55
  if (typeof r.alias !== "string" || r.alias.length === 0) {
50
- throw new Error(`sources.yaml: ${where} missing required "alias"`);
56
+ throw new Error(`${MANIFEST_FILENAME}: ${where} missing required "alias"`);
51
57
  }
52
58
  if (!ALIAS_PATTERN.test(r.alias)) {
53
- throw new Error(`sources.yaml: ${where}.alias "${r.alias}" must match ${ALIAS_PATTERN}`);
59
+ throw new Error(`${MANIFEST_FILENAME}: ${where}.alias "${r.alias}" must match ${ALIAS_PATTERN}`);
54
60
  }
55
61
  if (seen.has(r.alias)) {
56
- throw new Error(`sources.yaml: duplicate alias "${r.alias}"`);
62
+ throw new Error(`${MANIFEST_FILENAME}: duplicate alias "${r.alias}"`);
57
63
  }
58
64
  seen.add(r.alias);
59
65
  if (typeof r.url !== "string" || r.url.length === 0) {
60
- throw new Error(`sources.yaml: ${where} missing required "url"`);
66
+ throw new Error(`${MANIFEST_FILENAME}: ${where} missing required "url"`);
61
67
  }
62
68
  let branch;
63
69
  if (r.branch !== undefined) {
64
70
  if (typeof r.branch !== "string" || r.branch.length === 0) {
65
- throw new Error(`sources.yaml: ${where}.branch must be a non-empty string`);
71
+ throw new Error(`${MANIFEST_FILENAME}: ${where}.branch must be a non-empty string`);
66
72
  }
67
73
  branch = r.branch;
68
74
  }
@@ -111,7 +117,7 @@ function runGit(cwd, args, inheritOutput = false) {
111
117
  stdout: inheritOutput ? "" : (result.stdout ?? ""),
112
118
  };
113
119
  }
114
- /** Run git from sources/<alias>/ with safe.bareRepository=all, so the .git placeholder routes to .bare. */
120
+ /** Run git from oms/<alias>/ with safe.bareRepository=all, so the .git placeholder routes to .bare. */
115
121
  function runBareGit(repoRoot, alias, args, inheritOutput = false) {
116
122
  return runGit(aliasDir(repoRoot, alias), ["-c", "safe.bareRepository=all", ...args], inheritOutput);
117
123
  }
@@ -131,7 +137,7 @@ function isGitVersionSupported(v) {
131
137
  function findWorkspaceRoot(options = {}) {
132
138
  let current = resolve(options.cwd ?? process.cwd());
133
139
  while (true) {
134
- if (existsSync(join(current, "sources.yaml")))
140
+ if (existsSync(join(current, MANIFEST_FILENAME)))
135
141
  return current;
136
142
  const parent = dirname(current);
137
143
  if (parent === current)
@@ -140,7 +146,7 @@ function findWorkspaceRoot(options = {}) {
140
146
  }
141
147
  }
142
148
  function aliasDir(repoRoot, alias) {
143
- return join(repoRoot, "sources", alias);
149
+ return join(repoRoot, DATA_DIRNAME, alias);
144
150
  }
145
151
  function bareDir(repoRoot, alias) {
146
152
  return join(aliasDir(repoRoot, alias), ".bare");
@@ -167,19 +173,68 @@ function isRegisteredSubmodule(repoRoot, sourcePath) {
167
173
  .split("\n")
168
174
  .some((line) => line.trim().split(/\s+/)[1] === sourcePath);
169
175
  }
170
- /** Returns true when .gitmodules registers any sources/<alias> entry for the loaded repos. */
176
+ /** Returns true when .gitmodules registers any sources/<alias> or oms/<alias> entry for the loaded repos. */
171
177
  function hasLegacySubmoduleLayout(repoRoot, repos) {
172
178
  if (!existsSync(join(repoRoot, ".gitmodules")))
173
179
  return false;
174
- return repos.some((repo) => isRegisteredSubmodule(repoRoot, `sources/${repo.alias}`));
180
+ return repos.some((repo) => isRegisteredSubmodule(repoRoot, `${LEGACY_DATA_DIRNAME}/${repo.alias}`)
181
+ || isRegisteredSubmodule(repoRoot, `${DATA_DIRNAME}/${repo.alias}`));
175
182
  }
176
183
  function abortOnLegacy(repoRoot, repos) {
177
184
  if (!hasLegacySubmoduleLayout(repoRoot, repos))
178
185
  return false;
179
- log.error('detected legacy submodule layout (.gitmodules registers sources/<alias>). oh-my-space 0.3.0 uses bare clone + worktrees.\n' +
180
- ' See README "Migrating from 0.2.x" for the manual steps. Aborting to avoid destructive change.');
186
+ log.error('detected legacy submodule layout (.gitmodules registers an alias path). oh-my-space 0.3.0+ uses bare clone + worktrees.\n' +
187
+ ` See ${SUBMODULE_MIGRATION_DOC} for the manual steps. Aborting to avoid destructive change.`);
181
188
  return true;
182
189
  }
190
+ /** Inspect a single directory for 0.3.x rename artifacts. */
191
+ function detectLegacyRenameArtifactsAt(dir) {
192
+ const manifest = existsSync(join(dir, LEGACY_MANIFEST));
193
+ const data = existsSync(join(dir, LEGACY_DATA_DIRNAME));
194
+ if (!manifest && !data)
195
+ return null;
196
+ return { manifest, data };
197
+ }
198
+ function emitLegacyRenameMessage(dir, found) {
199
+ const artifacts = [
200
+ found.manifest ? `'${LEGACY_MANIFEST}'` : null,
201
+ found.data ? `'${LEGACY_DATA_DIRNAME}/'` : null,
202
+ ]
203
+ .filter((s) => s !== null)
204
+ .join(" and ");
205
+ log.error(`detected legacy ${artifacts} at ${dir}.\n` +
206
+ ` oh-my-space 0.4.0 renamed the manifest to ${MANIFEST_FILENAME} and the data directory to ${DATA_DIRNAME}/.\n` +
207
+ ` See ${LEGACY_MIGRATION_DOC} for the manual steps. Aborting to avoid destructive change.`);
208
+ }
209
+ /** Block when the active oms.yaml workspace still has legacy artifacts at its root. */
210
+ function abortOnLegacyRenameAt(repoRoot) {
211
+ const found = detectLegacyRenameArtifactsAt(repoRoot);
212
+ if (!found)
213
+ return false;
214
+ emitLegacyRenameMessage(repoRoot, found);
215
+ return true;
216
+ }
217
+ /**
218
+ * When oms.yaml could not be found, walk upward looking for a 0.3.x manifest (sources.yaml).
219
+ * Only the manifest is trusted as a positive signal — a stray sources/ directory by itself
220
+ * could belong to an unrelated tool. Returns true if a hint was emitted.
221
+ */
222
+ function emitLegacyRenameHintWalkUp(options = {}) {
223
+ let current = resolve(options.cwd ?? process.cwd());
224
+ while (true) {
225
+ if (existsSync(join(current, LEGACY_MANIFEST))) {
226
+ emitLegacyRenameMessage(current, {
227
+ manifest: true,
228
+ data: existsSync(join(current, LEGACY_DATA_DIRNAME)),
229
+ });
230
+ return true;
231
+ }
232
+ const parent = dirname(current);
233
+ if (parent === current)
234
+ return false;
235
+ current = parent;
236
+ }
237
+ }
183
238
  function ensureGitignore(repoRoot) {
184
239
  const path = join(repoRoot, ".gitignore");
185
240
  let content = "";
@@ -210,7 +265,7 @@ function setupBareRepo(repo, repoRoot) {
210
265
  return false;
211
266
  }
212
267
  // Write the placeholder before any further runBareGit calls, otherwise git ascends
213
- // out of sources/<alias>/ looking for a repo and touches the wrong .git.
268
+ // out of oms/<alias>/ looking for a repo and touches the wrong .git.
214
269
  writeFileSync(gitPlaceholderPath(repoRoot, alias), "gitdir: ./.bare\n");
215
270
  const config = runBareGit(repoRoot, alias, [
216
271
  "config",
@@ -265,7 +320,7 @@ function listExistingWorktrees(repoRoot, alias) {
265
320
  if (!path || bare)
266
321
  continue;
267
322
  const rel = relative(aliasPath, path);
268
- // Skip worktrees outside sources/<alias>/ (shouldn't happen, but be safe)
323
+ // Skip worktrees outside oms/<alias>/ (shouldn't happen, but be safe)
269
324
  if (rel.startsWith(".."))
270
325
  continue;
271
326
  entries.push({ branch, path, relativePath: rel });
@@ -295,7 +350,7 @@ function cleanupEmptyParents(repoRoot, alias, branch) {
295
350
  function addWorktree(repo, repoRoot, branch) {
296
351
  const wtPath = worktreePath(repoRoot, repo.alias, branch);
297
352
  if (existsSync(wtPath)) {
298
- log.warn(`${repo.alias}: sources/${repo.alias}/${branch} already exists; skipping.`);
353
+ log.warn(`${repo.alias}: ${DATA_DIRNAME}/${repo.alias}/${branch} already exists; skipping.`);
299
354
  return true;
300
355
  }
301
356
  log.step(`${repo.alias}: git worktree add --track -B ${branch} ${branch} origin/${branch}`);
@@ -331,7 +386,7 @@ function syncRepo(repo, repoRoot) {
331
386
  if (existsSync(aliasPath)) {
332
387
  const stray = readdirSync(aliasPath).filter((e) => e !== ".git");
333
388
  if (stray.length > 0) {
334
- log.error(`${alias}: sources/${alias}/ already exists but is not a bare clone. Move or remove it manually, then retry.`);
389
+ log.error(`${alias}: ${DATA_DIRNAME}/${alias}/ already exists but is not a bare clone. Move or remove it manually, then retry.`);
335
390
  return "failed";
336
391
  }
337
392
  }
@@ -365,7 +420,7 @@ function syncRepo(repo, repoRoot) {
365
420
  }
366
421
  const branch = repo.branch ?? detectDefaultBranch(repoRoot, alias);
367
422
  if (!branch) {
368
- log.error(`${alias}: could not determine default branch from bare clone. Set "branch:" in sources.yaml.`);
423
+ log.error(`${alias}: could not determine default branch from bare clone. Set "branch:" in ${MANIFEST_FILENAME}.`);
369
424
  return "failed";
370
425
  }
371
426
  if (!addWorktree(repo, repoRoot, branch))
@@ -404,7 +459,7 @@ function unsyncRepo(repo, repoRoot, force) {
404
459
  for (const wt of worktrees) {
405
460
  const status = runGit(wt.path, ["status", "--porcelain"]);
406
461
  if (status.success && status.stdout.trim().length > 0) {
407
- log.error(`${repo.alias}: worktree at sources/${repo.alias}/${wt.relativePath} has uncommitted changes. Commit, stash, or pass --force.`);
462
+ log.error(`${repo.alias}: worktree at ${DATA_DIRNAME}/${repo.alias}/${wt.relativePath} has uncommitted changes. Commit, stash, or pass --force.`);
408
463
  return "failed";
409
464
  }
410
465
  }
@@ -528,12 +583,12 @@ function exitFromResults(results) {
528
583
  function loadRepos(options = {}) {
529
584
  const repoRoot = findWorkspaceRoot(options);
530
585
  if (!repoRoot) {
531
- log.error("Could not find sources.yaml in the current directory or its parents. Create a sources.yaml in this project, then retry.");
586
+ log.error(`Could not find ${MANIFEST_FILENAME} in the current directory or its parents. Create a ${MANIFEST_FILENAME} in this project, then retry.`);
532
587
  return null;
533
588
  }
534
589
  try {
535
- const sourcesPath = join(repoRoot, "sources.yaml");
536
- return { repos: validateSources(parseYaml(readFileSync(sourcesPath, "utf8"))), repoRoot };
590
+ const manifestPath = join(repoRoot, MANIFEST_FILENAME);
591
+ return { repos: validateSources(parseYaml(readFileSync(manifestPath, "utf8"))), repoRoot };
537
592
  }
538
593
  catch (e) {
539
594
  log.error(e instanceof Error ? e.message : String(e));
@@ -565,9 +620,13 @@ async function selectRepos(repos, aliases, options, actionLabel) {
565
620
  }
566
621
  async function runSync(aliases, options) {
567
622
  const loaded = loadRepos();
568
- if (!loaded)
623
+ if (!loaded) {
624
+ emitLegacyRenameHintWalkUp();
569
625
  return 1;
626
+ }
570
627
  const { repos, repoRoot } = loaded;
628
+ if (abortOnLegacyRenameAt(repoRoot))
629
+ return 1;
571
630
  if (options.list) {
572
631
  printList(repos);
573
632
  return 0;
@@ -585,9 +644,13 @@ async function runSync(aliases, options) {
585
644
  }
586
645
  async function runManage(command, aliases, options) {
587
646
  const loaded = loadRepos();
588
- if (!loaded)
647
+ if (!loaded) {
648
+ emitLegacyRenameHintWalkUp();
589
649
  return 1;
650
+ }
590
651
  const { repos, repoRoot } = loaded;
652
+ if (abortOnLegacyRenameAt(repoRoot))
653
+ return 1;
591
654
  if (abortOnLegacy(repoRoot, repos))
592
655
  return 1;
593
656
  const picked = await selectRepos(repos, aliases, options, command);
@@ -601,9 +664,13 @@ async function runManage(command, aliases, options) {
601
664
  }
602
665
  async function runUnsync(aliases, options) {
603
666
  const loaded = loadRepos();
604
- if (!loaded)
667
+ if (!loaded) {
668
+ emitLegacyRenameHintWalkUp();
605
669
  return 1;
670
+ }
606
671
  const { repos, repoRoot } = loaded;
672
+ if (abortOnLegacyRenameAt(repoRoot))
673
+ return 1;
607
674
  if (abortOnLegacy(repoRoot, repos))
608
675
  return 1;
609
676
  const picked = await selectRepos(repos, aliases, options, "unsync");
@@ -628,9 +695,13 @@ async function runUnsync(aliases, options) {
628
695
  }
629
696
  async function runWorktreeAdd(alias, branch) {
630
697
  const loaded = loadRepos();
631
- if (!loaded)
698
+ if (!loaded) {
699
+ emitLegacyRenameHintWalkUp();
632
700
  return 1;
701
+ }
633
702
  const { repos, repoRoot } = loaded;
703
+ if (abortOnLegacyRenameAt(repoRoot))
704
+ return 1;
634
705
  if (abortOnLegacy(repoRoot, repos))
635
706
  return 1;
636
707
  const repo = repos.find((r) => r.alias === alias);
@@ -643,7 +714,7 @@ async function runWorktreeAdd(alias, branch) {
643
714
  return 1;
644
715
  }
645
716
  if (existsSync(worktreePath(repoRoot, alias, branch))) {
646
- log.error(`${alias}: worktree for ${branch} already exists at sources/${alias}/${branch}.`);
717
+ log.error(`${alias}: worktree for ${branch} already exists at ${DATA_DIRNAME}/${alias}/${branch}.`);
647
718
  return 1;
648
719
  }
649
720
  log.step(`${alias}: git fetch origin --prune (refresh before add)`);
@@ -655,9 +726,13 @@ async function runWorktreeAdd(alias, branch) {
655
726
  }
656
727
  async function runWorktreeList(alias) {
657
728
  const loaded = loadRepos();
658
- if (!loaded)
729
+ if (!loaded) {
730
+ emitLegacyRenameHintWalkUp();
659
731
  return 1;
732
+ }
660
733
  const { repos, repoRoot } = loaded;
734
+ if (abortOnLegacyRenameAt(repoRoot))
735
+ return 1;
661
736
  if (abortOnLegacy(repoRoot, repos))
662
737
  return 1;
663
738
  const targets = alias ? repos.filter((r) => r.alias === alias) : repos;
@@ -680,7 +755,7 @@ async function runWorktreeList(alias) {
680
755
  rows.push({
681
756
  alias: repo.alias,
682
757
  branch: wt.branch,
683
- path: `sources/${repo.alias}/${wt.relativePath}`,
758
+ path: `${DATA_DIRNAME}/${repo.alias}/${wt.relativePath}`,
684
759
  });
685
760
  }
686
761
  }
@@ -694,9 +769,13 @@ async function runWorktreeList(alias) {
694
769
  }
695
770
  async function runWorktreeRemove(alias, branch, options) {
696
771
  const loaded = loadRepos();
697
- if (!loaded)
772
+ if (!loaded) {
773
+ emitLegacyRenameHintWalkUp();
698
774
  return 1;
775
+ }
699
776
  const { repos, repoRoot } = loaded;
777
+ if (abortOnLegacyRenameAt(repoRoot))
778
+ return 1;
700
779
  if (abortOnLegacy(repoRoot, repos))
701
780
  return 1;
702
781
  const repo = repos.find((r) => r.alias === alias);
@@ -723,11 +802,15 @@ async function runWorktreeRemove(alias, branch, options) {
723
802
  }
724
803
  async function runDoctor() {
725
804
  const loaded = loadRepos();
726
- if (!loaded)
805
+ if (!loaded) {
806
+ emitLegacyRenameHintWalkUp();
727
807
  return 1;
808
+ }
728
809
  const { repos, repoRoot } = loaded;
810
+ if (abortOnLegacyRenameAt(repoRoot))
811
+ return 1;
729
812
  log.success(`Workspace root: ${repoRoot}`);
730
- log.success(`sources.yaml: ${repos.length} repo(s) configured`);
813
+ log.success(`${MANIFEST_FILENAME}: ${repos.length} repo(s) configured`);
731
814
  const git = spawnSync("git", ["--version"], {
732
815
  encoding: "utf8",
733
816
  stdio: ["ignore", "pipe", "pipe"],
@@ -748,7 +831,7 @@ async function runDoctor() {
748
831
  warnings++;
749
832
  }
750
833
  if (hasLegacySubmoduleLayout(repoRoot, repos)) {
751
- log.warn('detected legacy submodule layout (.gitmodules registers sources/<alias>). See README "Migrating from 0.2.x".');
834
+ log.warn(`detected legacy submodule layout (.gitmodules registers an alias path). See ${SUBMODULE_MIGRATION_DOC}.`);
752
835
  warnings++;
753
836
  }
754
837
  const gi = join(repoRoot, ".gitignore");
@@ -768,14 +851,14 @@ async function runDoctor() {
768
851
  }
769
852
  const refspec = runBareGit(repoRoot, repo.alias, ["config", "--get", "remote.origin.fetch"]);
770
853
  if (!refspec.success || refspec.stdout.trim().length === 0) {
771
- log.warn(`${repo.alias}: bare clone is missing remote.origin.fetch. Run: git -C sources/${repo.alias}/.bare -c safe.bareRepository=all config remote.origin.fetch '${FETCH_REFSPEC}'`);
854
+ log.warn(`${repo.alias}: bare clone is missing remote.origin.fetch. Run: git -C ${DATA_DIRNAME}/${repo.alias}/.bare -c safe.bareRepository=all config remote.origin.fetch '${FETCH_REFSPEC}'`);
772
855
  warnings++;
773
856
  }
774
857
  else {
775
858
  log.success(`${repo.alias}: bare clone OK (refspec: ${refspec.stdout.trim()})`);
776
859
  }
777
860
  if (!existsSync(gitPlaceholderPath(repoRoot, repo.alias))) {
778
- log.warn(`${repo.alias}: missing .git placeholder at sources/${repo.alias}/.git. Recreate with: echo 'gitdir: ./.bare' > sources/${repo.alias}/.git`);
861
+ log.warn(`${repo.alias}: missing .git placeholder at ${DATA_DIRNAME}/${repo.alias}/.git. Recreate with: echo 'gitdir: ./.bare' > ${DATA_DIRNAME}/${repo.alias}/.git`);
779
862
  warnings++;
780
863
  }
781
864
  }
@@ -813,19 +896,19 @@ const commandNames = new Set([
813
896
  const program = new Command();
814
897
  program
815
898
  .name("oms")
816
- .description("Manage source repositories listed in sources.yaml as bare clones + worktrees under sources/<alias>/.")
899
+ .description(`Manage source repositories listed in ${MANIFEST_FILENAME} as bare clones + worktrees under ${DATA_DIRNAME}/<alias>/.`)
817
900
  .version(readPackageVersion())
818
901
  .addHelpText("after", exitHelp);
819
902
  program
820
903
  .command("doctor")
821
- .description("Check sources.yaml, git availability, bare-clone state, and .gitignore for each registered alias.")
904
+ .description(`Check ${MANIFEST_FILENAME}, git availability, bare-clone state, and .gitignore for each registered alias.`)
822
905
  .addHelpText("after", exitHelp)
823
906
  .action(async () => {
824
907
  await exitWith(runDoctor());
825
908
  });
826
909
  program
827
910
  .command("sync")
828
- .description("Bare-clone each registered repo into sources/<alias>/.bare and create the baseline worktree at sources/<alias>/<branch>/.")
911
+ .description(`Bare-clone each registered repo into ${DATA_DIRNAME}/<alias>/.bare and create the baseline worktree at ${DATA_DIRNAME}/<alias>/<branch>/.`)
829
912
  .argument("[aliases...]", "repo aliases to sync (omit for interactive multi-select)")
830
913
  .option("--all", "sync every registered source repo")
831
914
  .option("--list", "print registered repos")
@@ -861,7 +944,7 @@ program
861
944
  });
862
945
  program
863
946
  .command("unsync")
864
- .description("Remove all worktrees and the bare clone for each alias (keeps sources.yaml entry).")
947
+ .description(`Remove all worktrees and the bare clone for each alias (keeps ${MANIFEST_FILENAME} entry).`)
865
948
  .argument("[aliases...]", "repo aliases to unsync (omit for interactive multi-select)")
866
949
  .option("--all", "unsync every registered source repo")
867
950
  .option("--force", "discard uncommitted changes in worktrees")
@@ -875,7 +958,7 @@ const worktreeCmd = program
875
958
  .addHelpText("after", exitHelp);
876
959
  worktreeCmd
877
960
  .command("add")
878
- .description("Create a worktree for <branch> at sources/<alias>/<branch>/.")
961
+ .description(`Create a worktree for <branch> at ${DATA_DIRNAME}/<alias>/<branch>/.`)
879
962
  .argument("<alias>", "registered source alias")
880
963
  .argument("<branch>", "branch name (may include slashes)")
881
964
  .addHelpText("after", exitHelp)
@@ -892,7 +975,7 @@ worktreeCmd
892
975
  });
893
976
  worktreeCmd
894
977
  .command("remove")
895
- .description("Remove the worktree at sources/<alias>/<branch>.")
978
+ .description(`Remove the worktree at ${DATA_DIRNAME}/<alias>/<branch>.`)
896
979
  .argument("<alias>", "registered source alias")
897
980
  .argument("<branch>", "branch name (may include slashes)")
898
981
  .option("--force", "discard uncommitted changes")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "oh-my-space",
3
- "version": "0.3.1",
4
- "description": "CLI for managing external source repositories declared in sources.yaml.",
3
+ "version": "0.4.0",
4
+ "description": "CLI for managing external source repositories declared in oms.yaml.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/divlook/oh-my-space",
7
7
  "repository": {
@@ -12,8 +12,8 @@
12
12
  "url": "https://github.com/divlook/oh-my-space/issues"
13
13
  },
14
14
  "keywords": [
15
- "sources",
16
- "submodule",
15
+ "oms",
16
+ "worktree",
17
17
  "git",
18
18
  "cli"
19
19
  ],