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 CHANGED
@@ -2,25 +2,25 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/oh-my-space.svg)](https://www.npmjs.com/package/oh-my-space)
4
4
 
5
- `oh-my-space` (OMS) is a small CLI for managing multi-repository workspaces with Git submodules. Declare source repositories once, work in normal branches, and let the main project record the exact commit used from each repository.
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 want them checked out side by side;
12
- - need reproducible workspaces where the main project records each source repository's exact commit;
13
- - want everyday branch, commit, pull, and push workflows without accidentally working in a detached checkout;
14
- - want source-commit changes to remain visible until you deliberately record them in the main project;
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 keeps Git's reproducibility while automating routine submodule setup and bounded recovery. When a choice depends on your intent, it asks instead of changing repository state silently.
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 whose top-level directory contains `oms.yaml` before repositories are synchronized
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 and inspect its state:
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/`. Continue with [Getting started](https://github.com/divlook/oh-my-space/blob/main/docs/getting-started.md) to complete the first branch, commit, push, and recorded-commit workflow.
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) — set up a workspace and complete the first source change from branch creation through recording the commit in the main project.
67
- - [How OMS works](https://github.com/divlook/oh-my-space/blob/main/docs/how-oms-works.md) — understand workspace layout, repository boundaries, recorded commits, synchronization, status, safety behavior, and recovery.
68
- - [Commands](https://github.com/divlook/oh-my-space/blob/main/docs/commands.md) — choose the right command and Git scope; use `oms <command> --help` for exact arguments, options, and exit behavior.
69
- - [Configure your workspace](https://github.com/divlook/oh-my-space/blob/main/docs/configure-your-workspace.md) — define repositories, remotes, and starting branches in `oms.yaml`.
70
- - [AI coding tools](https://github.com/divlook/oh-my-space/blob/main/docs/ai-coding-tools.md) — install agent instructions and workspace skills that preserve repository boundaries.
71
- - [Migration guides](https://github.com/divlook/oh-my-space/blob/main/docs/migrations/README.md) — follow version-specific upgrade instructions.
72
- - [Development](https://github.com/divlook/oh-my-space/blob/main/docs/development.md) — build, test, and contribute to OMS.
73
- - [Release channels](https://github.com/divlook/oh-my-space/blob/main/docs/release-channels.md) — install stable or beta releases and maintain npm release channels.
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
 
@@ -1 +1 @@
1
- {"commit":"6ccf47ab4baa1eec1c4b3e62103189b59f630c66","skills":{"oms-branch":{"version":"1.1.0","omsVersion":">=1.0.0-0"},"oms-pointer":{"version":"1.1.0","omsVersion":">=1.0.0-0"},"oms-workspace":{"version":"1.2.0","omsVersion":">=1.0.0-0"}}}
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 read root versus submodule state.
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 scoped submodule workflows; do not guess root repository versus submodule Git scope.
14545
- - Do not create root commits for existing submodule pointer updates unless the user explicitly runs \`oms record <alias>\`.`;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-space",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Manage multi-repo workspaces with Git submodules.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/divlook/oh-my-space",