oh-my-space 0.9.0 → 0.9.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 +24 -0
- package/dist/build-info.json +1 -1
- package/dist/oms.js +496 -2
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ oms checkout <alias> <branch> # fetch origin, then check out a remote branch
|
|
|
72
72
|
oms push <alias>... # push the branch (lazy remote) + stage the pointer
|
|
73
73
|
oms push <alias> --remote upstream # target a specific remote (repeatable; defaults to origin)
|
|
74
74
|
oms unsync <alias>... # deinitialize and remove a submodule
|
|
75
|
+
oms update --check # check whether a newer oms CLI is available
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
For `oms switch` and `oms checkout`, omitting the alias and/or branch prompts you to pick one interactively (synced submodules, and local or `origin/*` branches respectively).
|
|
@@ -113,6 +114,29 @@ oms/
|
|
|
113
114
|
| `oms pull ...` | workspace root | `git pull --ff-only <remote>` on each submodule's current branch, then stages the moved pointer. | Requires the submodule to be on a branch. `--remote <name>` selects a single remote (defaults to `origin`). |
|
|
114
115
|
| `oms push <alias>...` | workspace root | `git push <remote> <branch>` (creating the remote branch on first push), then stages the moved pointer. | `--remote <name>` (repeatable) picks the remote(s), defaults to `origin`; upstream is set only for `origin`. `--commit` also commits the pointer update in the parent. |
|
|
115
116
|
| `oms unsync <alias>` / `--all` | workspace root | `git submodule deinit` + `git rm` for the alias; drops an empty `.gitmodules`. | Keeps the `oms.yaml` entry. Use `--force` to discard uncommitted changes. |
|
|
117
|
+
| `oms update` | anywhere | Checks the npm registry and safely updates the installed `oms` CLI only when it detects a confident global install. | Use `--check` for a non-mutating check. Use `--yes` to skip the confirmation prompt for confident global updates. Project-local, temporary runner, development, and unknown installs print guidance only. |
|
|
118
|
+
|
|
119
|
+
## Updating the CLI
|
|
120
|
+
|
|
121
|
+
Check whether the installed CLI is current:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
oms update --check
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Run an update when `oms` can confidently identify a global npm, pnpm, Yarn classic, or Bun installation:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
oms update
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For automation, `--yes` skips the confirmation prompt after `oms` has printed the detected context and selected command:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
oms update --yes
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`oms update` does not edit project manifests or temporary runner caches. If the install is project-local, temporary, development, or unknown, it prints safe manual guidance instead of mutating the environment.
|
|
116
140
|
|
|
117
141
|
## `oms.yaml` format
|
|
118
142
|
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"3c598f24e0cfc453bc81728553b0c40fb1b34591"}
|
package/dist/oms.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync, } from "node:fs";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, rmdirSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { cancel, isCancel, log, multiselect, select, text } from "@clack/prompts";
|
|
8
|
+
import semver from "semver";
|
|
8
9
|
import { parse as parseYaml } from "yaml";
|
|
9
10
|
const ALIAS_PATTERN = /^[a-z0-9][a-z0-9_@-]*$/;
|
|
10
11
|
const REMOTE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
@@ -23,9 +24,364 @@ const REMOTES_MIGRATION_DOC = "docs/migrations/0.6.x-to-0.7.0.md";
|
|
|
23
24
|
const DOCS_REPO_BLOB_BASE = "https://github.com/divlook/oh-my-space/blob";
|
|
24
25
|
const MIN_GIT_MAJOR = 2;
|
|
25
26
|
const MIN_GIT_MINOR = 40;
|
|
27
|
+
const PACKAGE_NAME = "oh-my-space";
|
|
28
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
|
|
29
|
+
const REGISTRY_TIMEOUT_MS = 10_000;
|
|
26
30
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
31
|
const useColor = process.stdout.isTTY;
|
|
28
32
|
const dim = (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s);
|
|
33
|
+
function isTestMode() {
|
|
34
|
+
return process.env.OMS_TEST_MODE === "1";
|
|
35
|
+
}
|
|
36
|
+
function testEnv(name) {
|
|
37
|
+
return isTestMode() ? process.env[name] : undefined;
|
|
38
|
+
}
|
|
39
|
+
function formatCommand(command) {
|
|
40
|
+
return [command.executable, ...command.args].join(" ");
|
|
41
|
+
}
|
|
42
|
+
function globalUpdateCommand(manager) {
|
|
43
|
+
if (manager === "npm")
|
|
44
|
+
return { executable: "npm", args: ["install", "-g", `${PACKAGE_NAME}@latest`] };
|
|
45
|
+
if (manager === "pnpm")
|
|
46
|
+
return { executable: "pnpm", args: ["add", "-g", `${PACKAGE_NAME}@latest`] };
|
|
47
|
+
if (manager === "yarn")
|
|
48
|
+
return { executable: "yarn", args: ["global", "add", `${PACKAGE_NAME}@latest`] };
|
|
49
|
+
return { executable: "bun", args: ["add", "-g", `${PACKAGE_NAME}@latest`] };
|
|
50
|
+
}
|
|
51
|
+
async function fetchLatestPackageVersion() {
|
|
52
|
+
const mocked = testEnv("OMS_TEST_REGISTRY_RESPONSE");
|
|
53
|
+
if (mocked !== undefined)
|
|
54
|
+
return latestFromRegistryJson(JSON.parse(mocked));
|
|
55
|
+
const failure = testEnv("OMS_TEST_REGISTRY_FAILURE");
|
|
56
|
+
if (failure)
|
|
57
|
+
throw new Error(failure);
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
response = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) });
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
throw new Error(`Could not reach npm registry: ${e instanceof Error ? e.message : String(e)}`);
|
|
64
|
+
}
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`npm registry request failed with HTTP ${response.status}`);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return latestFromRegistryJson(await response.json());
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (e instanceof Error)
|
|
73
|
+
throw e;
|
|
74
|
+
throw new Error(`Could not parse npm registry response: ${String(e)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function latestFromRegistryJson(data) {
|
|
78
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
79
|
+
throw new Error("npm registry response was not a JSON object");
|
|
80
|
+
}
|
|
81
|
+
const distTags = data["dist-tags"];
|
|
82
|
+
if (!distTags || typeof distTags !== "object" || Array.isArray(distTags)) {
|
|
83
|
+
throw new Error("npm registry response is missing dist-tags.latest");
|
|
84
|
+
}
|
|
85
|
+
const latest = distTags.latest;
|
|
86
|
+
if (typeof latest !== "string" || latest.length === 0) {
|
|
87
|
+
throw new Error("npm registry response is missing dist-tags.latest");
|
|
88
|
+
}
|
|
89
|
+
return latest;
|
|
90
|
+
}
|
|
91
|
+
function compareVersions(currentVersion, latestVersion) {
|
|
92
|
+
const current = semver.valid(currentVersion);
|
|
93
|
+
if (!current)
|
|
94
|
+
throw new Error(`Installed version is not valid semver: ${currentVersion}`);
|
|
95
|
+
const latest = semver.valid(latestVersion);
|
|
96
|
+
if (!latest)
|
|
97
|
+
throw new Error(`Registry latest version is not valid semver: ${latestVersion}`);
|
|
98
|
+
return semver.compare(current, latest);
|
|
99
|
+
}
|
|
100
|
+
function safeRealpath(path) {
|
|
101
|
+
try {
|
|
102
|
+
return realpathSync(path);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return path;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function runtimePlatform() {
|
|
109
|
+
const mocked = testEnv("OMS_TEST_PLATFORM");
|
|
110
|
+
return mocked === "win32" ? "win32" : process.platform;
|
|
111
|
+
}
|
|
112
|
+
function binaryPathNames(name) {
|
|
113
|
+
if (runtimePlatform() !== "win32")
|
|
114
|
+
return [name];
|
|
115
|
+
const extensions = new Set([...(process.env.PATHEXT ?? "").split(";"), ".cmd", ".ps1", ".exe", ".bat"]);
|
|
116
|
+
const names = [name];
|
|
117
|
+
for (const ext of extensions) {
|
|
118
|
+
if (!ext)
|
|
119
|
+
continue;
|
|
120
|
+
const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`;
|
|
121
|
+
names.push(`${name}${normalizedExt.toLowerCase()}`, `${name}${normalizedExt}`);
|
|
122
|
+
}
|
|
123
|
+
return [...new Set(names)];
|
|
124
|
+
}
|
|
125
|
+
function resolvePathBinary(name) {
|
|
126
|
+
const mocked = testEnv("OMS_TEST_PATH_BIN");
|
|
127
|
+
if (mocked !== undefined)
|
|
128
|
+
return mocked.length > 0 ? mocked : null;
|
|
129
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
130
|
+
if (!dir)
|
|
131
|
+
continue;
|
|
132
|
+
for (const candidateName of binaryPathNames(name)) {
|
|
133
|
+
const candidate = join(dir, candidateName);
|
|
134
|
+
if (existsSync(candidate))
|
|
135
|
+
return candidate;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function findPackageRoot(start) {
|
|
141
|
+
let current = resolve(start);
|
|
142
|
+
while (true) {
|
|
143
|
+
const pkgPath = join(current, "package.json");
|
|
144
|
+
if (existsSync(pkgPath))
|
|
145
|
+
return current;
|
|
146
|
+
const parent = dirname(current);
|
|
147
|
+
if (parent === current)
|
|
148
|
+
return null;
|
|
149
|
+
current = parent;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function readPackageName(root) {
|
|
153
|
+
try {
|
|
154
|
+
return readJson(join(root, "package.json"))?.name ?? null;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function collectRuntimeEvidence() {
|
|
161
|
+
const mocked = testEnv("OMS_TEST_RUNTIME_EVIDENCE");
|
|
162
|
+
if (mocked !== undefined)
|
|
163
|
+
return JSON.parse(mocked);
|
|
164
|
+
const modulePath = testEnv("OMS_TEST_MODULE_PATH") ?? fileURLToPath(import.meta.url);
|
|
165
|
+
const detectedPackageRoot = findPackageRoot(dirname(modulePath)) ?? packageRoot;
|
|
166
|
+
const mockedArgv1 = testEnv("OMS_TEST_ARGV1");
|
|
167
|
+
const runningBin = mockedArgv1 !== undefined
|
|
168
|
+
? resolve(mockedArgv1)
|
|
169
|
+
: process.argv[1]
|
|
170
|
+
? resolve(process.argv[1])
|
|
171
|
+
: modulePath;
|
|
172
|
+
const pathBin = resolvePathBinary("oms");
|
|
173
|
+
return {
|
|
174
|
+
packageRoot: detectedPackageRoot,
|
|
175
|
+
realPackageRoot: safeRealpath(detectedPackageRoot),
|
|
176
|
+
runningBin,
|
|
177
|
+
realRunningBin: safeRealpath(runningBin),
|
|
178
|
+
pathBin,
|
|
179
|
+
realPathBin: pathBin ? safeRealpath(pathBin) : null,
|
|
180
|
+
packageName: readPackageName(detectedPackageRoot),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function commandForProject(manager, dev) {
|
|
184
|
+
if (manager === "npm")
|
|
185
|
+
return `npm install ${dev ? "--save-dev " : ""}${PACKAGE_NAME}@latest`;
|
|
186
|
+
if (manager === "pnpm")
|
|
187
|
+
return `pnpm add ${dev ? "-D " : ""}${PACKAGE_NAME}@latest`;
|
|
188
|
+
if (manager === "yarn")
|
|
189
|
+
return `yarn add ${dev ? "--dev " : ""}${PACKAGE_NAME}@latest`;
|
|
190
|
+
return `bun add ${dev ? "-d " : ""}${PACKAGE_NAME}@latest`;
|
|
191
|
+
}
|
|
192
|
+
function nearestProjectRoot(start) {
|
|
193
|
+
let current = resolve(start);
|
|
194
|
+
while (true) {
|
|
195
|
+
if (existsSync(join(current, "package.json")))
|
|
196
|
+
return current;
|
|
197
|
+
const parent = dirname(current);
|
|
198
|
+
if (parent === current)
|
|
199
|
+
return null;
|
|
200
|
+
current = parent;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function inferProjectManager(projectRoot, pkg) {
|
|
204
|
+
const declared = pkg.packageManager?.split("@")[0];
|
|
205
|
+
if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm")
|
|
206
|
+
return declared;
|
|
207
|
+
if (existsSync(join(projectRoot, "pnpm-lock.yaml")))
|
|
208
|
+
return "pnpm";
|
|
209
|
+
if (existsSync(join(projectRoot, "yarn.lock")))
|
|
210
|
+
return "yarn";
|
|
211
|
+
if (existsSync(join(projectRoot, "bun.lockb")) || existsSync(join(projectRoot, "bun.lock")))
|
|
212
|
+
return "bun";
|
|
213
|
+
return "npm";
|
|
214
|
+
}
|
|
215
|
+
function projectGuidance(packageRootPath) {
|
|
216
|
+
const projectRoot = nearestProjectRoot(dirname(packageRootPath));
|
|
217
|
+
if (!projectRoot)
|
|
218
|
+
return [`npm install --save-dev ${PACKAGE_NAME}@latest`];
|
|
219
|
+
const pkg = readJson(join(projectRoot, "package.json"));
|
|
220
|
+
if (!pkg)
|
|
221
|
+
return [`npm install --save-dev ${PACKAGE_NAME}@latest`];
|
|
222
|
+
const manager = inferProjectManager(projectRoot, pkg);
|
|
223
|
+
const dev = !(pkg.dependencies && PACKAGE_NAME in pkg.dependencies);
|
|
224
|
+
return [commandForProject(manager, dev)];
|
|
225
|
+
}
|
|
226
|
+
function pathParts(path) {
|
|
227
|
+
return normalizePath(safeRealpath(path)).split("/").filter(Boolean);
|
|
228
|
+
}
|
|
229
|
+
function normalizePath(path) {
|
|
230
|
+
return path.replace(/\\/g, "/");
|
|
231
|
+
}
|
|
232
|
+
function isPackageRootNamed(packageRootPath) {
|
|
233
|
+
return pathParts(packageRootPath).at(-1) === PACKAGE_NAME;
|
|
234
|
+
}
|
|
235
|
+
function hasBinPath(binPaths, expectedPaths) {
|
|
236
|
+
const normalizeForComparison = (path) => {
|
|
237
|
+
const normalized = normalizePath(path);
|
|
238
|
+
return runtimePlatform() === "win32" ? normalized.toLowerCase() : normalized;
|
|
239
|
+
};
|
|
240
|
+
const actual = binPaths.map(normalizeForComparison);
|
|
241
|
+
return expectedPaths.some((expected) => actual.includes(normalizeForComparison(expected)));
|
|
242
|
+
}
|
|
243
|
+
function commandShimPaths(prefix, name) {
|
|
244
|
+
return [`${prefix}/${name}`, `${prefix}/${name}.cmd`, `${prefix}/${name}.ps1`];
|
|
245
|
+
}
|
|
246
|
+
function isNpmGlobalLayout(packageRootPath, binPaths) {
|
|
247
|
+
const root = normalizePath(packageRootPath);
|
|
248
|
+
if (root.endsWith(`/lib/node_modules/${PACKAGE_NAME}`)) {
|
|
249
|
+
const prefix = root.slice(0, -`/lib/node_modules/${PACKAGE_NAME}`.length);
|
|
250
|
+
return hasBinPath(binPaths, commandShimPaths(`${prefix}/bin`, "oms"));
|
|
251
|
+
}
|
|
252
|
+
if (root.endsWith(`/node_modules/${PACKAGE_NAME}`)) {
|
|
253
|
+
const prefix = root.slice(0, -`/node_modules/${PACKAGE_NAME}`.length);
|
|
254
|
+
return hasBinPath(binPaths, commandShimPaths(prefix, "oms"));
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
function isPnpmGlobalLayout(packageRootPath, binPaths) {
|
|
259
|
+
const root = normalizePath(packageRootPath);
|
|
260
|
+
const suffix = `/node_modules/${PACKAGE_NAME}`;
|
|
261
|
+
const marker = "/global/";
|
|
262
|
+
const markerIndex = root.lastIndexOf(marker);
|
|
263
|
+
if (markerIndex === -1 || !root.endsWith(suffix))
|
|
264
|
+
return false;
|
|
265
|
+
const storeVersion = root.slice(markerIndex + marker.length, -suffix.length);
|
|
266
|
+
if (!/^\d+$/.test(storeVersion))
|
|
267
|
+
return false;
|
|
268
|
+
const prefix = root.slice(0, markerIndex);
|
|
269
|
+
return hasBinPath(binPaths, [...commandShimPaths(prefix, "oms"), ...commandShimPaths(`${prefix}/bin`, "oms")]);
|
|
270
|
+
}
|
|
271
|
+
function isYarnGlobalLayout(packageRootPath, binPaths) {
|
|
272
|
+
const root = normalizePath(packageRootPath);
|
|
273
|
+
const suffix = `/.config/yarn/global/node_modules/${PACKAGE_NAME}`;
|
|
274
|
+
if (!root.endsWith(suffix))
|
|
275
|
+
return false;
|
|
276
|
+
const home = root.slice(0, -suffix.length);
|
|
277
|
+
return hasBinPath(binPaths, commandShimPaths(`${home}/.yarn/bin`, "oms"));
|
|
278
|
+
}
|
|
279
|
+
function isBunGlobalLayout(packageRootPath, binPaths) {
|
|
280
|
+
const root = normalizePath(packageRootPath);
|
|
281
|
+
const suffix = `/.bun/install/global/node_modules/${PACKAGE_NAME}`;
|
|
282
|
+
if (!root.endsWith(suffix))
|
|
283
|
+
return false;
|
|
284
|
+
const home = root.slice(0, -suffix.length);
|
|
285
|
+
return hasBinPath(binPaths, commandShimPaths(`${home}/.bun/bin`, "oms"));
|
|
286
|
+
}
|
|
287
|
+
function isProjectLocalLayout(evidence) {
|
|
288
|
+
const projectRoot = nearestProjectRoot(dirname(evidence.realPackageRoot));
|
|
289
|
+
if (!projectRoot)
|
|
290
|
+
return false;
|
|
291
|
+
const root = normalizePath(evidence.realPackageRoot);
|
|
292
|
+
const project = normalizePath(projectRoot);
|
|
293
|
+
if (!root.startsWith(`${project}/`))
|
|
294
|
+
return false;
|
|
295
|
+
const binPaths = [evidence.pathBin, evidence.runningBin, evidence.realPathBin, evidence.realRunningBin]
|
|
296
|
+
.filter((path) => Boolean(path))
|
|
297
|
+
.map(normalizePath);
|
|
298
|
+
return hasBinPath(binPaths, commandShimPaths(`${project}/node_modules/.bin`, "oms"));
|
|
299
|
+
}
|
|
300
|
+
function globalManagerFromPaths(evidence) {
|
|
301
|
+
const root = normalizePath(evidence.realPackageRoot);
|
|
302
|
+
const binPaths = [evidence.pathBin, evidence.runningBin, evidence.realPathBin, evidence.realRunningBin]
|
|
303
|
+
.filter((path) => Boolean(path))
|
|
304
|
+
.map(normalizePath);
|
|
305
|
+
const managers = new Set();
|
|
306
|
+
if (isNpmGlobalLayout(root, binPaths))
|
|
307
|
+
managers.add("npm");
|
|
308
|
+
if (isPnpmGlobalLayout(root, binPaths))
|
|
309
|
+
managers.add("pnpm");
|
|
310
|
+
if (isYarnGlobalLayout(root, binPaths))
|
|
311
|
+
managers.add("yarn");
|
|
312
|
+
if (isBunGlobalLayout(root, binPaths))
|
|
313
|
+
managers.add("bun");
|
|
314
|
+
return managers.size === 1 ? [...managers][0] : null;
|
|
315
|
+
}
|
|
316
|
+
function detectInstallContext() {
|
|
317
|
+
const mocked = testEnv("OMS_TEST_INSTALL_CONTEXT");
|
|
318
|
+
if (mocked !== undefined)
|
|
319
|
+
return JSON.parse(mocked);
|
|
320
|
+
const evidence = collectRuntimeEvidence();
|
|
321
|
+
const warnings = [];
|
|
322
|
+
if (evidence.packageName !== PACKAGE_NAME) {
|
|
323
|
+
return {
|
|
324
|
+
kind: "unknown",
|
|
325
|
+
label: `unknown install (package root is not ${PACKAGE_NAME})`,
|
|
326
|
+
guidance: [`npm install -g ${PACKAGE_NAME}@latest`, `pnpm add -g ${PACKAGE_NAME}@latest`],
|
|
327
|
+
warnings,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (evidence.realPathBin && evidence.realPathBin !== evidence.realRunningBin) {
|
|
331
|
+
warnings.push(`PATH-resolved oms differs from the running executable: ${evidence.pathBin}`);
|
|
332
|
+
}
|
|
333
|
+
const rootParts = pathParts(evidence.realPackageRoot);
|
|
334
|
+
if (rootParts.includes("_npx") || rootParts.includes("dlx") || rootParts.includes("bunx")) {
|
|
335
|
+
return {
|
|
336
|
+
kind: "ephemeral",
|
|
337
|
+
label: "temporary runner install",
|
|
338
|
+
guidance: [`Run ${PACKAGE_NAME} with @latest, or install it globally with npm install -g ${PACKAGE_NAME}@latest.`],
|
|
339
|
+
warnings,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (existsSync(join(evidence.packageRoot, "scripts", "oms.ts")) || existsSync(join(evidence.packageRoot, "tsconfig.json"))) {
|
|
343
|
+
return {
|
|
344
|
+
kind: "development",
|
|
345
|
+
label: "development checkout",
|
|
346
|
+
guidance: ["Update the source checkout with git, then rebuild."],
|
|
347
|
+
warnings,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (normalizePath(evidence.realPackageRoot).includes(`/node_modules/${PACKAGE_NAME}`)) {
|
|
351
|
+
const manager = globalManagerFromPaths(evidence);
|
|
352
|
+
if (manager && isPackageRootNamed(evidence.realPackageRoot)) {
|
|
353
|
+
const updateCommand = globalUpdateCommand(manager);
|
|
354
|
+
return {
|
|
355
|
+
kind: "global",
|
|
356
|
+
label: `global ${manager} install`,
|
|
357
|
+
manager,
|
|
358
|
+
updateCommand,
|
|
359
|
+
guidance: [formatCommand(updateCommand)],
|
|
360
|
+
warnings,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (!isProjectLocalLayout(evidence)) {
|
|
364
|
+
return {
|
|
365
|
+
kind: "unknown",
|
|
366
|
+
label: "unknown install context",
|
|
367
|
+
guidance: [`npm install -g ${PACKAGE_NAME}@latest`, `pnpm add -g ${PACKAGE_NAME}@latest`],
|
|
368
|
+
warnings,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
kind: "project",
|
|
373
|
+
label: "project-local install",
|
|
374
|
+
guidance: projectGuidance(evidence.realPackageRoot),
|
|
375
|
+
warnings,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
kind: "unknown",
|
|
380
|
+
label: "unknown install context",
|
|
381
|
+
guidance: [`npm install -g ${PACKAGE_NAME}@latest`, `pnpm add -g ${PACKAGE_NAME}@latest`],
|
|
382
|
+
warnings,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
29
385
|
function validateSources(data) {
|
|
30
386
|
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
31
387
|
throw new Error(`${MANIFEST_FILENAME}: root must be a mapping`);
|
|
@@ -1140,6 +1496,134 @@ function readPackageVersion() {
|
|
|
1140
1496
|
const pkg = readJson(join(packageRoot, "package.json"));
|
|
1141
1497
|
return pkg?.version ?? "0.0.0";
|
|
1142
1498
|
}
|
|
1499
|
+
function printUpdateHeader(currentVersion, latestVersion) {
|
|
1500
|
+
log.info(`Current version: ${currentVersion}`);
|
|
1501
|
+
log.info(`Latest version: ${latestVersion}`);
|
|
1502
|
+
}
|
|
1503
|
+
function printInstallContext(context) {
|
|
1504
|
+
log.info(`Detected context: ${context.label}`);
|
|
1505
|
+
for (const warning of context.warnings)
|
|
1506
|
+
log.warn(warning);
|
|
1507
|
+
if (context.updateCommand)
|
|
1508
|
+
log.info(`Selected command: ${formatCommand(context.updateCommand)}`);
|
|
1509
|
+
}
|
|
1510
|
+
function printGuidance(context) {
|
|
1511
|
+
if (context.guidance.length === 0)
|
|
1512
|
+
return;
|
|
1513
|
+
log.info("Manual update guidance:");
|
|
1514
|
+
for (const command of context.guidance)
|
|
1515
|
+
log.message(` ${command}`);
|
|
1516
|
+
}
|
|
1517
|
+
function commandAvailability(command) {
|
|
1518
|
+
const mocked = testEnv("OMS_TEST_MANAGER_AVAILABLE");
|
|
1519
|
+
if (mocked !== undefined)
|
|
1520
|
+
return mocked === "1" || mocked === "true";
|
|
1521
|
+
const result = spawnSync(command.executable, ["--version"], {
|
|
1522
|
+
encoding: "utf8",
|
|
1523
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1524
|
+
shell: runtimePlatform() === "win32",
|
|
1525
|
+
});
|
|
1526
|
+
return result.status === 0;
|
|
1527
|
+
}
|
|
1528
|
+
function runUpdateCommand(command) {
|
|
1529
|
+
const mocked = testEnv("OMS_TEST_UPDATE_EXIT");
|
|
1530
|
+
if (mocked !== undefined) {
|
|
1531
|
+
log.step(formatCommand(command));
|
|
1532
|
+
return Number.parseInt(mocked, 10);
|
|
1533
|
+
}
|
|
1534
|
+
log.step(formatCommand(command));
|
|
1535
|
+
const result = spawnSync(command.executable, command.args, { stdio: "inherit", shell: runtimePlatform() === "win32" });
|
|
1536
|
+
if (result.status !== null)
|
|
1537
|
+
return result.status;
|
|
1538
|
+
log.error(`Package manager exited from signal ${result.signal ?? "unknown"}.`);
|
|
1539
|
+
return 1;
|
|
1540
|
+
}
|
|
1541
|
+
function verifyPostUpdate(latestVersion) {
|
|
1542
|
+
const mocked = testEnv("OMS_TEST_VERIFY_VERSION");
|
|
1543
|
+
const result = mocked !== undefined
|
|
1544
|
+
? { status: 0, stdout: mocked, stderr: "" }
|
|
1545
|
+
: spawnSync("oms", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
1546
|
+
if (result.status !== 0) {
|
|
1547
|
+
log.warn("Post-update verification could not run oms --version from PATH.");
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const observed = result.stdout.trim().replace(/^oms\s+/, "");
|
|
1551
|
+
if (observed !== latestVersion) {
|
|
1552
|
+
log.warn(`Post-update verification saw ${observed || "empty output"}, expected ${latestVersion}.`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
async function confirmUpdate(command) {
|
|
1556
|
+
const choice = await select({
|
|
1557
|
+
message: `Run ${formatCommand(command)}?`,
|
|
1558
|
+
options: [
|
|
1559
|
+
{ value: "yes", label: "Yes, update oms" },
|
|
1560
|
+
{ value: "no", label: "No, do not update" },
|
|
1561
|
+
],
|
|
1562
|
+
});
|
|
1563
|
+
if (isCancel(choice)) {
|
|
1564
|
+
cancel("Cancelled.");
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1567
|
+
return choice === "yes";
|
|
1568
|
+
}
|
|
1569
|
+
async function runUpdate(options) {
|
|
1570
|
+
let latestVersion;
|
|
1571
|
+
const currentVersion = readPackageVersion();
|
|
1572
|
+
try {
|
|
1573
|
+
latestVersion = await fetchLatestPackageVersion();
|
|
1574
|
+
const comparison = compareVersions(currentVersion, latestVersion);
|
|
1575
|
+
printUpdateHeader(currentVersion, latestVersion);
|
|
1576
|
+
if (comparison === 0) {
|
|
1577
|
+
log.success("oms is up to date.");
|
|
1578
|
+
return 0;
|
|
1579
|
+
}
|
|
1580
|
+
if (comparison > 0) {
|
|
1581
|
+
log.info("Installed version is newer than the npm registry latest; no downgrade will be performed.");
|
|
1582
|
+
return 0;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
catch (e) {
|
|
1586
|
+
log.error(e instanceof Error ? e.message : String(e));
|
|
1587
|
+
return 1;
|
|
1588
|
+
}
|
|
1589
|
+
log.warn("Update available.");
|
|
1590
|
+
const context = detectInstallContext();
|
|
1591
|
+
printInstallContext(context);
|
|
1592
|
+
if (options.check) {
|
|
1593
|
+
if (!context.updateCommand)
|
|
1594
|
+
printGuidance(context);
|
|
1595
|
+
return 0;
|
|
1596
|
+
}
|
|
1597
|
+
if (context.kind !== "global" || !context.updateCommand) {
|
|
1598
|
+
log.info("Automatic update is only supported for confident global installs.");
|
|
1599
|
+
printGuidance(context);
|
|
1600
|
+
return 0;
|
|
1601
|
+
}
|
|
1602
|
+
const command = context.updateCommand;
|
|
1603
|
+
if (!options.yes) {
|
|
1604
|
+
if (!process.stdin.isTTY) {
|
|
1605
|
+
log.info(`Non-interactive shell detected. Re-run with --yes to execute: ${formatCommand(command)}`);
|
|
1606
|
+
return 0;
|
|
1607
|
+
}
|
|
1608
|
+
const confirmed = await confirmUpdate(command);
|
|
1609
|
+
if (!confirmed) {
|
|
1610
|
+
log.info("Update cancelled; no changes made.");
|
|
1611
|
+
return 0;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (!commandAvailability(command)) {
|
|
1615
|
+
log.error(`${command.executable} is not executable from PATH. Would have run: ${formatCommand(command)}`);
|
|
1616
|
+
return 1;
|
|
1617
|
+
}
|
|
1618
|
+
const updateExit = runUpdateCommand(command);
|
|
1619
|
+
if (updateExit !== 0) {
|
|
1620
|
+
log.error(`Package manager update failed (exit ${updateExit}).`);
|
|
1621
|
+
return 1;
|
|
1622
|
+
}
|
|
1623
|
+
verifyPostUpdate(latestVersion);
|
|
1624
|
+
log.success("Update command completed.");
|
|
1625
|
+
return 0;
|
|
1626
|
+
}
|
|
1143
1627
|
/** The commit baked in at build time, or null when unavailable (dev/no-git build). */
|
|
1144
1628
|
function readBuildCommit() {
|
|
1145
1629
|
const info = readJson(join(dirname(fileURLToPath(import.meta.url)), "build-info.json"));
|
|
@@ -1171,6 +1655,7 @@ const commandNames = new Set([
|
|
|
1171
1655
|
"pull",
|
|
1172
1656
|
"push",
|
|
1173
1657
|
"unsync",
|
|
1658
|
+
"update",
|
|
1174
1659
|
"help",
|
|
1175
1660
|
]);
|
|
1176
1661
|
const program = new Command();
|
|
@@ -1273,6 +1758,15 @@ program
|
|
|
1273
1758
|
.action(async (aliases, options) => {
|
|
1274
1759
|
await exitWith(runUnsync(aliases, options));
|
|
1275
1760
|
});
|
|
1761
|
+
program
|
|
1762
|
+
.command("update")
|
|
1763
|
+
.description("Check for and safely update the oms CLI. Only confident global installs are updated automatically.")
|
|
1764
|
+
.option("--check", "check for an available update without mutating the installation")
|
|
1765
|
+
.option("--yes", "run a confirmed global update without prompting")
|
|
1766
|
+
.addHelpText("after", exitHelp)
|
|
1767
|
+
.action(async (options) => {
|
|
1768
|
+
await exitWith(runUpdate(options));
|
|
1769
|
+
});
|
|
1276
1770
|
const requestedCommand = process.argv[2];
|
|
1277
1771
|
if (requestedCommand && !requestedCommand.startsWith("-") && !commandNames.has(requestedCommand)) {
|
|
1278
1772
|
console.error(`error: unknown command '${requestedCommand}'`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-space",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
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",
|
|
@@ -46,11 +46,13 @@
|
|
|
46
46
|
"@changesets/changelog-github": "^0.7.0",
|
|
47
47
|
"@changesets/cli": "^2.29.7",
|
|
48
48
|
"@types/node": "^20.19.0",
|
|
49
|
+
"@types/semver": "^7.7.1",
|
|
49
50
|
"typescript": "^5.9.3"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"@clack/prompts": "^1.4.0",
|
|
53
54
|
"commander": "^14.0.3",
|
|
55
|
+
"semver": "^7.8.4",
|
|
54
56
|
"yaml": "^2.8.1"
|
|
55
57
|
}
|
|
56
58
|
}
|