oh-my-space 1.1.0 → 1.2.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 +22 -17
- package/dist/build-info.json +1 -1
- package/dist/oms.js +150 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/oh-my-space)
|
|
4
4
|
|
|
5
|
-
`oh-my-space` (OMS)
|
|
5
|
+
`oh-my-space` (OMS) manages multi-repository workspaces with Git submodules. Source repositories use normal branches. The main project records each source repository's exact commit.
|
|
6
6
|
|
|
7
7
|
## Who it helps
|
|
8
8
|
|
|
9
9
|
Use OMS when you:
|
|
10
10
|
|
|
11
|
-
- develop one product across several repositories and
|
|
12
|
-
- need reproducible workspaces
|
|
13
|
-
-
|
|
14
|
-
-
|
|
11
|
+
- develop one product across several repositories and need adjacent checkouts.
|
|
12
|
+
- need reproducible workspaces with exact source commits recorded in the main project.
|
|
13
|
+
- need branch, commit, pull, and push workflows that avoid accidental detached checkouts.
|
|
14
|
+
- need source-commit changes to remain visible until you record them in the main project.
|
|
15
15
|
- use AI coding tools that need a clear boundary between the main project and source repositories.
|
|
16
16
|
|
|
17
|
-
OMS
|
|
17
|
+
OMS preserves Git's reproducibility and automates routine submodule setup and bounded recovery. OMS asks when a choice depends on your intent.
|
|
18
18
|
|
|
19
19
|
## Requirements
|
|
20
20
|
|
|
21
21
|
- [Node.js](https://nodejs.org) `>=20.19.0`
|
|
22
22
|
- Git `>=2.40`
|
|
23
|
-
- A Git repository
|
|
23
|
+
- A Git repository with `oms.yaml` at its top level before you synchronize repositories
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
@@ -52,25 +52,30 @@ repos:
|
|
|
52
52
|
branch: main # optional; defaults to the remote's default branch
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Synchronize the workspace
|
|
55
|
+
Synchronize the workspace:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
58
|
oms sync --all
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Inspect its state:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
59
64
|
oms status
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
The source repository is now available at `oms/api/`.
|
|
67
|
+
The source repository is now available at `oms/api/`. Follow [Getting started](https://github.com/divlook/oh-my-space/blob/main/docs/getting-started.md) for your first branch, commit, push, and recorded-commit workflow.
|
|
63
68
|
|
|
64
69
|
## Documentation
|
|
65
70
|
|
|
66
|
-
- [Getting started](https://github.com/divlook/oh-my-space/blob/main/docs/getting-started.md)
|
|
67
|
-
- [How OMS works](https://github.com/divlook/oh-my-space/blob/main/docs/how-oms-works.md)
|
|
68
|
-
- [Commands](https://github.com/divlook/oh-my-space/blob/main/docs/commands.md)
|
|
69
|
-
- [Configure your workspace](https://github.com/divlook/oh-my-space/blob/main/docs/configure-your-workspace.md)
|
|
70
|
-
- [AI coding tools](https://github.com/divlook/oh-my-space/blob/main/docs/ai-coding-tools.md)
|
|
71
|
-
- [Migration guides](https://github.com/divlook/oh-my-space/blob/main/docs/migrations/README.md)
|
|
72
|
-
- [Development](https://github.com/divlook/oh-my-space/blob/main/docs/development.md)
|
|
73
|
-
- [Release channels](https://github.com/divlook/oh-my-space/blob/main/docs/release-channels.md)
|
|
71
|
+
- [Getting started](https://github.com/divlook/oh-my-space/blob/main/docs/getting-started.md): Read for workspace setup and your first source change.
|
|
72
|
+
- [How OMS works](https://github.com/divlook/oh-my-space/blob/main/docs/how-oms-works.md): Read for layout, repository boundaries, recorded commits, managed trees, synchronization, status, safety, and recovery.
|
|
73
|
+
- [Commands](https://github.com/divlook/oh-my-space/blob/main/docs/commands.md): Read to choose a command and Git scope. Use `oms <command> --help` for exact arguments, options, and exit behavior.
|
|
74
|
+
- [Configure your workspace](https://github.com/divlook/oh-my-space/blob/main/docs/configure-your-workspace.md): Read to define repositories, remotes, and starting branches in `oms.yaml`.
|
|
75
|
+
- [AI coding tools](https://github.com/divlook/oh-my-space/blob/main/docs/ai-coding-tools.md): Read to install agent instructions and workspace skills that preserve repository boundaries.
|
|
76
|
+
- [Migration guides](https://github.com/divlook/oh-my-space/blob/main/docs/migrations/README.md): Read for version-specific upgrade instructions.
|
|
77
|
+
- [Development](https://github.com/divlook/oh-my-space/blob/main/docs/development.md): Read to build, test, or contribute to OMS.
|
|
78
|
+
- [Release channels](https://github.com/divlook/oh-my-space/blob/main/docs/release-channels.md): Read to install stable or beta releases or maintain npm release channels.
|
|
74
79
|
|
|
75
80
|
## License
|
|
76
81
|
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"b9240ff3a85fd5ffc004faf2f8fa3f1cfa096f5a","skills":{"oms-branch":{"version":"1.2.0","omsVersion":">=1.0.0-0"},"oms-pointer":{"version":"1.2.0","omsVersion":">=1.0.0-0"},"oms-workspace":{"version":"1.4.0","omsVersion":">=1.2.0-0"}}}
|
package/dist/oms.js
CHANGED
|
@@ -14539,10 +14539,10 @@ __name(isDirty, "isDirty");
|
|
|
14539
14539
|
// scripts/lib/agent.ts
|
|
14540
14540
|
var OMS_MARKER_START = "<!-- OMS START -->";
|
|
14541
14541
|
var OMS_MARKER_END = "<!-- OMS END -->";
|
|
14542
|
-
var OMS_SCOPE_GUARDRAIL = `- Run \`oms status --json\` before Git work involving \`oms/\` to
|
|
14542
|
+
var OMS_SCOPE_GUARDRAIL = `- Run \`oms status --json\` before Git work involving \`oms/\` to identify the main project and each source repository.
|
|
14543
14543
|
- Treat each \`oms/<alias>/\` directory as a separate Git repository.
|
|
14544
|
-
- Use \`oms\` commands for
|
|
14545
|
-
-
|
|
14544
|
+
- Use \`oms\` commands for workflows in \`oms/<alias>/\`.
|
|
14545
|
+
- Create main-project commits for existing pointer updates only when the user explicitly runs \`oms record <alias>\`.`;
|
|
14546
14546
|
var OMS_INSTRUCTION_BLOCK = `${OMS_MARKER_START}
|
|
14547
14547
|
## OMS Workspace Rules
|
|
14548
14548
|
|
|
@@ -19343,6 +19343,113 @@ async function runDoctor(gitRunner = productionGitRunner) {
|
|
|
19343
19343
|
}
|
|
19344
19344
|
__name(runDoctor, "runDoctor");
|
|
19345
19345
|
|
|
19346
|
+
// scripts/lib/exec.ts
|
|
19347
|
+
import { spawn } from "node:child_process";
|
|
19348
|
+
import { realpathSync as realpathSync3 } from "node:fs";
|
|
19349
|
+
import { resolve as resolve7 } from "node:path";
|
|
19350
|
+
function targetFailure(repoRoot, alias) {
|
|
19351
|
+
const registration = aliasRegistration(repoRoot, alias);
|
|
19352
|
+
if (registration !== "initialized") return registration;
|
|
19353
|
+
if (!submoduleInitialized(repoRoot, alias)) return "checkout is not initialized";
|
|
19354
|
+
const cwd = aliasDir(repoRoot, alias);
|
|
19355
|
+
const top = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
19356
|
+
const actual = runGit(cwd, ["rev-parse", "--absolute-git-dir"]);
|
|
19357
|
+
const expected = runGit(repoRoot, ["rev-parse", "--git-path", `modules/${submodulePath(alias)}`]);
|
|
19358
|
+
try {
|
|
19359
|
+
if (top.success && actual.success && expected.success && realpathSync3(top.stdout.trim()) === realpathSync3(cwd) && realpathSync3(actual.stdout.trim()) === realpathSync3(resolve7(repoRoot, expected.stdout.trim()))) return null;
|
|
19360
|
+
} catch {
|
|
19361
|
+
}
|
|
19362
|
+
return "checkout does not belong to the declared canonical submodule";
|
|
19363
|
+
}
|
|
19364
|
+
__name(targetFailure, "targetFailure");
|
|
19365
|
+
function describe(result) {
|
|
19366
|
+
switch (result.kind) {
|
|
19367
|
+
case "exit":
|
|
19368
|
+
return `exit ${result.code}`;
|
|
19369
|
+
case "signal":
|
|
19370
|
+
return `signal ${result.signal}`;
|
|
19371
|
+
case "spawn":
|
|
19372
|
+
return `spawn failure: ${result.reason}`;
|
|
19373
|
+
case "target":
|
|
19374
|
+
return `target failure: ${result.reason}`;
|
|
19375
|
+
case "interrupted":
|
|
19376
|
+
return "interrupted";
|
|
19377
|
+
case "not-run":
|
|
19378
|
+
return "not run (interrupted invocation)";
|
|
19379
|
+
}
|
|
19380
|
+
}
|
|
19381
|
+
__name(describe, "describe");
|
|
19382
|
+
async function runExec(aliases, options, command) {
|
|
19383
|
+
const [executable, ...args] = command;
|
|
19384
|
+
if (!executable) {
|
|
19385
|
+
R2.error("Usage: oms exec [aliases...] [--all] -- <command> [args...]");
|
|
19386
|
+
return 1;
|
|
19387
|
+
}
|
|
19388
|
+
const loaded = loadForSubmodules();
|
|
19389
|
+
if (!loaded) return 1;
|
|
19390
|
+
const { repos, repoRoot } = loaded;
|
|
19391
|
+
if (refuseInsideManagedTree(repoRoot)) return 1;
|
|
19392
|
+
if (repos.length === 0) {
|
|
19393
|
+
R2.error("No repositories declared in the workspace.");
|
|
19394
|
+
return 1;
|
|
19395
|
+
}
|
|
19396
|
+
const picked = await selectRepos(repos, aliases, options, "exec");
|
|
19397
|
+
if (!picked || picked.length === 0) return 1;
|
|
19398
|
+
const results = [];
|
|
19399
|
+
let interrupted = false;
|
|
19400
|
+
let active2;
|
|
19401
|
+
const interrupt = /* @__PURE__ */ __name(() => {
|
|
19402
|
+
if (interrupted) return;
|
|
19403
|
+
interrupted = true;
|
|
19404
|
+
active2?.kill("SIGINT");
|
|
19405
|
+
}, "interrupt");
|
|
19406
|
+
process.on("SIGINT", interrupt);
|
|
19407
|
+
try {
|
|
19408
|
+
for (const repo of picked) {
|
|
19409
|
+
let result;
|
|
19410
|
+
if (interrupted) {
|
|
19411
|
+
result = { kind: "not-run" };
|
|
19412
|
+
} else {
|
|
19413
|
+
const failure = targetFailure(repoRoot, repo.alias);
|
|
19414
|
+
if (failure) {
|
|
19415
|
+
result = { kind: "target", reason: `${failure}. State preserved; run "oms sync ${repo.alias}".` };
|
|
19416
|
+
} else {
|
|
19417
|
+
R2.step(`${repo.alias}: exec`);
|
|
19418
|
+
result = await new Promise((complete) => {
|
|
19419
|
+
let spawnError;
|
|
19420
|
+
try {
|
|
19421
|
+
active2 = spawn(executable, args, { cwd: aliasDir(repoRoot, repo.alias), shell: false, stdio: "inherit" });
|
|
19422
|
+
} catch (error) {
|
|
19423
|
+
complete({ kind: "spawn", reason: error instanceof Error ? error.message : String(error) });
|
|
19424
|
+
return;
|
|
19425
|
+
}
|
|
19426
|
+
active2.once("error", (error) => {
|
|
19427
|
+
spawnError = error;
|
|
19428
|
+
});
|
|
19429
|
+
active2.once("close", (code, signal) => {
|
|
19430
|
+
active2 = void 0;
|
|
19431
|
+
if (signal === "SIGINT") interrupted = true;
|
|
19432
|
+
if (interrupted) complete({ kind: "interrupted" });
|
|
19433
|
+
else if (spawnError) complete({ kind: "spawn", reason: spawnError.message });
|
|
19434
|
+
else if (signal) complete({ kind: "signal", signal });
|
|
19435
|
+
else complete({ kind: "exit", code: code ?? 1 });
|
|
19436
|
+
});
|
|
19437
|
+
});
|
|
19438
|
+
}
|
|
19439
|
+
}
|
|
19440
|
+
results.push({ alias: repo.alias, result });
|
|
19441
|
+
R2.info(`${repo.alias}: ${describe(result)}`);
|
|
19442
|
+
}
|
|
19443
|
+
} finally {
|
|
19444
|
+
process.off("SIGINT", interrupt);
|
|
19445
|
+
}
|
|
19446
|
+
R2.step("Execution summary");
|
|
19447
|
+
for (const { alias, result } of results) R2.info(`${alias}: ${describe(result)}`);
|
|
19448
|
+
if (interrupted) return 130;
|
|
19449
|
+
return results.every(({ result }) => result.kind === "exit" && result.code === 0) ? 0 : 2;
|
|
19450
|
+
}
|
|
19451
|
+
__name(runExec, "runExec");
|
|
19452
|
+
|
|
19346
19453
|
// scripts/lib/help.ts
|
|
19347
19454
|
var exitHelp = "\nExit codes: 0 ok | 1 usage/config error | 2 one or more git operations failed.";
|
|
19348
19455
|
var workspaceContextHelp = `
|
|
@@ -19411,6 +19518,38 @@ Examples:
|
|
|
19411
19518
|
$ oms unsync api --no-commit # leave the removal topology unstaged
|
|
19412
19519
|
$ oms unsync api --commit # same as the default; accepted for compatibility
|
|
19413
19520
|
`;
|
|
19521
|
+
var execHelp = `
|
|
19522
|
+
Scope: runs only in initialized canonical checkouts at oms/<alias>/, never managed trees.
|
|
19523
|
+
OMS performs no preparation: no initialization, registration, fetching, branch switching, pointer
|
|
19524
|
+
recording, or repair. Dirty files and detached HEAD are allowed. Unavailable targets fail individually
|
|
19525
|
+
with "oms sync <alias>" guidance; later eligible targets still run.
|
|
19526
|
+
|
|
19527
|
+
The first -- separates OMS options from the executable and literal arguments. Child --help, --all,
|
|
19528
|
+
and additional -- arguments are passed unchanged. OMS does not interpret shell syntax; invoke an
|
|
19529
|
+
available shell explicitly when needed. A separator and non-empty executable are required.
|
|
19530
|
+
Explicit aliases run once in first-occurrence order; --all takes precedence and uses manifest order.
|
|
19531
|
+
Without a selection, OMS prompts only in an interactive terminal; otherwise name aliases or use --all.
|
|
19532
|
+
|
|
19533
|
+
Children inherit your environment, permissions, stdin, stdout, and stderr. Output streams live under
|
|
19534
|
+
alias-labelled headers, with a final result for every alias. Stdin is consumed sequentially, not replayed
|
|
19535
|
+
for each child. Non-zero exits, non-SIGINT signals, spawn failures, and target failures do not stop later
|
|
19536
|
+
targets. Numeric child exit codes and failure reasons appear in results, not as the OMS exit code.
|
|
19537
|
+
|
|
19538
|
+
Ctrl+C (SIGINT) interrupts the active child and stops later targets. OMS waits for that child to close,
|
|
19539
|
+
reports unstarted aliases as not run, and exits 130. A child ending with SIGINT also interrupts the
|
|
19540
|
+
invocation. OMS does not forcibly kill children that ignore SIGINT or contain intentionally detached
|
|
19541
|
+
descendants; such children can delay exit, and detached descendants may continue running.
|
|
19542
|
+
|
|
19543
|
+
This runner is not a sandbox. User commands can change files, Git state, or remote services and access
|
|
19544
|
+
the network. OMS performs no additional network operations, implicit commit or push, or rollback.
|
|
19545
|
+
Completed effects and changes from failed or interrupted children are preserved.
|
|
19546
|
+
Examples:
|
|
19547
|
+
$ oms exec api web -- npm test
|
|
19548
|
+
$ oms exec --all -- node -e 'console.log(process.cwd())'
|
|
19549
|
+
$ oms exec api -- sh -c 'npm test && npm run build'
|
|
19550
|
+
Exit codes: 0 all targets succeeded | 1 usage/workspace/selection error | 2 command or target failure
|
|
19551
|
+
130 invocation interrupted
|
|
19552
|
+
`;
|
|
19414
19553
|
var fetchHelp = `
|
|
19415
19554
|
Preparation initializes existing registrations automatically. An unregistered alias is offered an explicit
|
|
19416
19555
|
sync; accepting creates a root topology commit, then fetch continues. A multi-alias selection asks once;
|
|
@@ -19908,6 +20047,7 @@ __name(exitWith, "exitWith");
|
|
|
19908
20047
|
var commandNames = /* @__PURE__ */ new Set([
|
|
19909
20048
|
"init",
|
|
19910
20049
|
"doctor",
|
|
20050
|
+
"exec",
|
|
19911
20051
|
"sync",
|
|
19912
20052
|
"status",
|
|
19913
20053
|
"commit",
|
|
@@ -19924,6 +20064,9 @@ var commandNames = /* @__PURE__ */ new Set([
|
|
|
19924
20064
|
"help"
|
|
19925
20065
|
]);
|
|
19926
20066
|
var collectRepeatable = /* @__PURE__ */ __name((value, acc) => [...acc, value], "collectRepeatable");
|
|
20067
|
+
var execSeparator = process.argv[2] === "exec" ? process.argv.indexOf("--", 3) : -1;
|
|
20068
|
+
var execCommand = execSeparator === -1 ? [] : process.argv.slice(execSeparator + 1);
|
|
20069
|
+
var cliArgs = execSeparator === -1 ? process.argv : process.argv.slice(0, execSeparator);
|
|
19927
20070
|
var program2 = new Command();
|
|
19928
20071
|
program2.name("oms").description(
|
|
19929
20072
|
`Manage source repositories listed in ${MANIFEST_FILENAME} as git submodules under ${DATA_DIRNAME}/<alias>/.`
|
|
@@ -19936,6 +20079,9 @@ program2.command("doctor").description(
|
|
|
19936
20079
|
).addHelpText("after", `${workspaceContextHelp}${exitHelp}`).action(async () => {
|
|
19937
20080
|
await exitWith(runDoctor());
|
|
19938
20081
|
});
|
|
20082
|
+
program2.command("exec").description("Run a command sequentially in initialized canonical source checkouts without preparation.").usage("[aliases...] [--all] -- <command> [args...]").argument("[aliases...]", "repo aliases to execute in (omit for interactive multi-select)").option("--all", "select every declared source repo in manifest order").addHelpText("after", `${execHelp}${workspaceContextHelp}`).action(async (aliases, options) => {
|
|
20083
|
+
await exitWith(runExec(aliases, options, execCommand));
|
|
20084
|
+
});
|
|
19939
20085
|
program2.command("sync").description(
|
|
19940
20086
|
`Register each repo as a submodule at ${DATA_DIRNAME}/<alias>/ (or initialize and refresh an existing one), checked out on its baseline branch.`
|
|
19941
20087
|
).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", "accepted for compatibility; the root topology commit (chore(oms): add ...) is created by default").option("--no-commit", "leave the root topology changes unstaged instead of committing them").addHelpText("after", `${syncHelp}${workspaceContextHelp}${exitHelp}`).action(async (aliases, options) => {
|
|
@@ -20017,4 +20163,4 @@ if (requestedCommand && !requestedCommand.startsWith("-") && !commandNames.has(r
|
|
|
20017
20163
|
console.error(`error: unknown command '${requestedCommand}'`);
|
|
20018
20164
|
process.exit(1);
|
|
20019
20165
|
}
|
|
20020
|
-
await program2.parseAsync();
|
|
20166
|
+
await program2.parseAsync(cliArgs);
|