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.
- package/README.md +15 -29
- package/dist/oms.js +130 -47
- 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 `
|
|
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
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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
|
-
`
|
|
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 `
|
|
85
|
-
| `oms sync <alias>` / `--all` | workspace root | Bare-clones missing repos, fetches origin, creates the baseline worktree. | Adds `
|
|
86
|
-
| `oms unsync <alias>` / `--all` | workspace root | Removes every worktree, the bare clone, and the `.git` placeholder for the alias. | Keeps the `
|
|
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
|
-
## `
|
|
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
|
-
|
|
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
|
-
|
|
114
|
+
## Migrating between versions
|
|
115
115
|
|
|
116
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
37
|
+
throw new Error(`${MANIFEST_FILENAME}: "repos" must be an array`);
|
|
32
38
|
}
|
|
33
39
|
if (repos.length === 0) {
|
|
34
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
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(
|
|
56
|
+
throw new Error(`${MANIFEST_FILENAME}: ${where} missing required "alias"`);
|
|
51
57
|
}
|
|
52
58
|
if (!ALIAS_PATTERN.test(r.alias)) {
|
|
53
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
180
|
-
|
|
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
|
|
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
|
|
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}:
|
|
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}:
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
536
|
-
return { repos: validateSources(parseYaml(readFileSync(
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
4
|
-
"description": "CLI for managing external source repositories declared in
|
|
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
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"oms",
|
|
16
|
+
"worktree",
|
|
17
17
|
"git",
|
|
18
18
|
"cli"
|
|
19
19
|
],
|