@savvy-web/cli 2.3.0 → 2.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 +9 -3
- package/cli/index.js +2 -2
- package/commands/repos/commands/remove.js +72 -0
- package/commands/repos/commands/rename.js +74 -0
- package/commands/repos/commands/restore.js +78 -0
- package/commands/repos/commands/status.js +25 -3
- package/commands/repos/commands/sync.js +7 -3
- package/commands/repos/index.js +22 -7
- package/index.d.ts +18 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -55,11 +55,11 @@ npx savvy clean --globs dist,.turbo,coverage
|
|
|
55
55
|
- `savvy commit` — the husky/Claude hook handlers (session-start, pre-commit-message, post-commit-verify).
|
|
56
56
|
- `savvy changeset` — changeset lint, check, transform, version, config validation, and dependency changesets.
|
|
57
57
|
- `savvy lint` — formatters for package.json, the pnpm workspace file and YAML.
|
|
58
|
-
- `savvy repos` — the vendored reference repos declared in `.repos/config.json`: `status
|
|
58
|
+
- `savvy repos` — the vendored reference repos declared in `.repos/config.json`: `status` (with `--drift`), `sync`, `pin`, `add`, `note`, `remove`, `rename` and `restore`.
|
|
59
59
|
|
|
60
60
|
## Vendored repos are read-only
|
|
61
61
|
|
|
62
|
-
`savvy repos sync`, `add` and `
|
|
62
|
+
`savvy repos sync`, `add`, `pin`, `remove`, `rename` and `restore` leave every vendored tree under `.repos/` read-only at the OS level — files `0444`, directories `0555` (an executable file locks at `0555` instead, and unlocks back to `0755` rather than losing its executable bit). Reading a vendored source needs no extra step; writing to one fails with `EACCES` by design, because those trees mirror an upstream repo at a pinned ref and any local edit is lost on the next sync. A dirty tree from a hand bypass recovers with `savvy repos restore <name...>` (or with no names, every dirty tree) rather than a manual `git reset`. Restore is destructive by design: it hard-resets each tree it touches to the pinned commit, so uncommitted edits and untracked files in that tree are gone.
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
65
|
npx savvy repos sync
|
|
@@ -67,9 +67,15 @@ npx savvy repos sync
|
|
|
67
67
|
|
|
68
68
|
npx savvy repos pin effect v4.0.0
|
|
69
69
|
# fetches, checks out the new ref, re-locks, and stages the gitlink and manifest
|
|
70
|
+
|
|
71
|
+
npx savvy repos restore
|
|
72
|
+
# hard-resets every dirty tree to its pinned commit and reports the clean ones as skipped
|
|
73
|
+
|
|
74
|
+
npx savvy repos status --drift
|
|
75
|
+
# reports where the manifest, .gitmodules, the worktree and git submodule status disagree
|
|
70
76
|
```
|
|
71
77
|
|
|
72
|
-
A manual `chmod` only holds until the next
|
|
78
|
+
A manual `chmod` only holds until the next mutation. Route changes through `savvy repos` instead.
|
|
73
79
|
|
|
74
80
|
Run any command with `--help` to see its full surface:
|
|
75
81
|
|
package/cli/index.js
CHANGED
|
@@ -76,7 +76,7 @@ const rootCommand = Command.make("savvy").pipe(Command.withSubcommands([
|
|
|
76
76
|
* CLI application: reads argv from the Stdio service provided by NodeServices.
|
|
77
77
|
* (v4's `Command.run` takes only `version` — the name comes from the root command.)
|
|
78
78
|
*/
|
|
79
|
-
const cli = Command.run(rootCommand, { version: "2.
|
|
79
|
+
const cli = Command.run(rootCommand, { version: "2.4.0" });
|
|
80
80
|
/**
|
|
81
81
|
* Shared workspace services from `@effected/workspaces`, wired as a
|
|
82
82
|
* self-contained unit and built ONCE (layers memoize by reference).
|
|
@@ -115,7 +115,7 @@ const InspectorAndAnalyzerLive = Changesets.BranchAnalyzer.layer.pipe(Layer.prov
|
|
|
115
115
|
* `Repos.ReposLockdown.layer` dependencies plus the shared `Git` service.
|
|
116
116
|
* Platform requirements (`FileSystem`, `Path`) flow up to `NodeServices.layer`.
|
|
117
117
|
*/
|
|
118
|
-
const ReposGroupLive = Repos.ReposManager.layer.pipe(Layer.provide(Repos.ReposConfigStore.layer), Layer.provide(Repos.ReposLockdown.layer), Layer.provide(GitLive));
|
|
118
|
+
const ReposGroupLive = Layer.mergeAll(Repos.ReposManager.layer, Repos.ReposDrift.layer).pipe(Layer.provide(Repos.ReposConfigStore.layer), Layer.provide(Repos.ReposLockdown.layer), Layer.provide(GitLive));
|
|
119
119
|
/**
|
|
120
120
|
* `ToolDiscovery` from `@effected/commands`, wired to this workspace. Its
|
|
121
121
|
* `LocalExec` contract — the argv prefix that runs a project-local binary — is
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Repos } from "@savvy-web/silk-effects";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/repos/commands/remove.ts
|
|
6
|
+
/**
|
|
7
|
+
* `repos remove` command -- unvendor a `.repos/` submodule.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A thin adapter over {@link Repos.ReposManager.remove}: deinitializes and
|
|
11
|
+
* removes the submodule's gitlink, worktree, and module gitdir, drops its
|
|
12
|
+
* `.gitmodules` section, and removes the manifest entry -- it does not
|
|
13
|
+
* commit, so the caller reviews and commits the staged removal with the
|
|
14
|
+
* ready-made message the result carries. Any notes the entry carried are
|
|
15
|
+
* surfaced in the result so a durable one can be promoted elsewhere first.
|
|
16
|
+
*
|
|
17
|
+
* A `ReposConfigError` with kind `"missing"` -- nothing vendored yet -- is
|
|
18
|
+
* the common, friendly case and always exits 0. A `ReposConfigError` with
|
|
19
|
+
* kind `"invalid"` means the manifest exists but is corrupt or unreadable.
|
|
20
|
+
* `RepoNotFoundError` means the named repo isn't in the manifest -- this is
|
|
21
|
+
* a REAL failure (unlike the "missing manifest" case above), since removing
|
|
22
|
+
* a name that was never vendored is almost always a typo the caller should
|
|
23
|
+
* see. `GitSubmoduleError` means the underlying git command failed, and
|
|
24
|
+
* `ReposLockdownError` means the OS-permission lockdown pass on a vendored
|
|
25
|
+
* tree failed -- all four (invalid config, not-found, git failure, lockdown
|
|
26
|
+
* failure) are real failures, logged and reported via a non-zero exit code.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```bash
|
|
30
|
+
* savvy repos remove my-repo
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
/* v8 ignore start -- CLI option/arg definitions */
|
|
36
|
+
const nameArg = Argument.string("name");
|
|
37
|
+
const cwdOption = Flag.directory("cwd").pipe(Flag.withDescription("Repo root to remove within"), Flag.withDefault("."));
|
|
38
|
+
/* v8 ignore stop */
|
|
39
|
+
/**
|
|
40
|
+
* Remove handler; exported for tests.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
const runReposRemove = (cwd, name) => Effect.gen(function* () {
|
|
45
|
+
const result = yield* (yield* Repos.ReposManager).remove(cwd, name);
|
|
46
|
+
yield* Effect.log(`${result.name}: removed (${result.path})`);
|
|
47
|
+
yield* Effect.log(result.commitMessage);
|
|
48
|
+
yield* Effect.log("staged — review and commit");
|
|
49
|
+
for (const note of result.removedNotes) yield* Effect.log(`warning: note ${note.id} (${note.ref}) was removed with the entry — promote first if durable`);
|
|
50
|
+
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
51
|
+
if (error.kind === "missing") return Effect.log("no .repos/config.json — nothing vendored");
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return Effect.log(error.message);
|
|
54
|
+
}), Effect.catchTag("RepoNotFoundError", (error) => {
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return Effect.log(error.message);
|
|
57
|
+
}), Effect.catchTag("GitSubmoduleError", (error) => {
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return Effect.log(error.message);
|
|
60
|
+
}), Effect.catchTag("ReposLockdownError", (error) => {
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return Effect.log(error.message);
|
|
63
|
+
}));
|
|
64
|
+
/* v8 ignore start -- CLI registration; handler tested via runReposRemove */
|
|
65
|
+
const removeCommand = Command.make("remove", {
|
|
66
|
+
name: nameArg,
|
|
67
|
+
cwd: cwdOption
|
|
68
|
+
}, ({ name, cwd }) => runReposRemove(cwd, name)).pipe(Command.withDescription("Unvendor a repo under .repos/; stages the removal without committing"));
|
|
69
|
+
/* v8 ignore stop */
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { removeCommand, runReposRemove };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Repos } from "@savvy-web/silk-effects";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/repos/commands/rename.ts
|
|
6
|
+
/**
|
|
7
|
+
* `repos rename` command -- rename a vendored `.repos/` submodule's manifest
|
|
8
|
+
* key and worktree.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* A thin adapter over {@link Repos.ReposManager.rename}: moves the worktree
|
|
12
|
+
* (`git mv`), re-points the module gitdir's `core.worktree` values,
|
|
13
|
+
* canonicalizes the `.gitmodules` section name, and renames the manifest
|
|
14
|
+
* key -- it does not commit, so the caller reviews and commits the staged
|
|
15
|
+
* rename with the ready-made message the result carries.
|
|
16
|
+
*
|
|
17
|
+
* A `ReposConfigError` with kind `"missing"` -- nothing vendored yet -- is
|
|
18
|
+
* the common, friendly case and always exits 0. A `ReposConfigError` with
|
|
19
|
+
* kind `"invalid"` means either the manifest exists but is corrupt/unreadable
|
|
20
|
+
* OR the requested new name is invalid/already vendored. `RepoNotFoundError`
|
|
21
|
+
* means the old name isn't in the manifest -- this is a REAL failure, since
|
|
22
|
+
* renaming a name that was never vendored is almost always a typo the caller
|
|
23
|
+
* should see. `GitSubmoduleError` means the underlying git command failed,
|
|
24
|
+
* and `ReposLockdownError` means the OS-permission lockdown pass on a
|
|
25
|
+
* vendored tree failed -- all four (invalid config, not-found, git failure,
|
|
26
|
+
* lockdown failure) are real failures, logged and reported via a non-zero
|
|
27
|
+
* exit code.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```bash
|
|
31
|
+
* savvy repos rename old-name new-name
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
/* v8 ignore start -- CLI option/arg definitions */
|
|
37
|
+
const oldNameArg = Argument.string("old-name");
|
|
38
|
+
const newNameArg = Argument.string("new-name");
|
|
39
|
+
const cwdOption = Flag.directory("cwd").pipe(Flag.withDescription("Repo root to rename within"), Flag.withDefault("."));
|
|
40
|
+
/* v8 ignore stop */
|
|
41
|
+
/**
|
|
42
|
+
* Rename handler; exported for tests.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
const runReposRename = (cwd, oldName, newName) => Effect.gen(function* () {
|
|
47
|
+
const result = yield* (yield* Repos.ReposManager).rename(cwd, oldName, newName);
|
|
48
|
+
yield* Effect.log(`${result.oldName}: renamed to ${result.newName} (${result.path})`);
|
|
49
|
+
yield* Effect.log(result.commitMessage);
|
|
50
|
+
yield* Effect.log("staged — review and commit");
|
|
51
|
+
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
52
|
+
if (error.kind === "missing") return Effect.log("no .repos/config.json — nothing vendored");
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return Effect.log(error.message);
|
|
55
|
+
}), Effect.catchTag("RepoNotFoundError", (error) => {
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
return Effect.log(error.message);
|
|
58
|
+
}), Effect.catchTag("GitSubmoduleError", (error) => {
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return Effect.log(error.message);
|
|
61
|
+
}), Effect.catchTag("ReposLockdownError", (error) => {
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return Effect.log(error.message);
|
|
64
|
+
}));
|
|
65
|
+
/* v8 ignore start -- CLI registration; handler tested via runReposRename */
|
|
66
|
+
const renameCommand = Command.make("rename", {
|
|
67
|
+
oldName: oldNameArg,
|
|
68
|
+
newName: newNameArg,
|
|
69
|
+
cwd: cwdOption
|
|
70
|
+
}, ({ oldName, newName, cwd }) => runReposRename(cwd, oldName, newName)).pipe(Command.withDescription("Rename a vendored repo's manifest key and worktree; stages the change without committing"));
|
|
71
|
+
/* v8 ignore stop */
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { renameCommand, runReposRename };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Repos } from "@savvy-web/silk-effects";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/repos/commands/restore.ts
|
|
6
|
+
/**
|
|
7
|
+
* `repos restore` command -- hard-reset one or more vendored `.repos/`
|
|
8
|
+
* submodules back to their pinned gitlink commit.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* A thin adapter over {@link Repos.ReposManager.restore}: explicit only --
|
|
12
|
+
* never invoked by `sync` or any other implicit path. DESTRUCTIVE to
|
|
13
|
+
* uncommitted worktree edits and untracked files in the repos it touches:
|
|
14
|
+
* each targeted repo is hard-reset to its staged gitlink commit (falling
|
|
15
|
+
* back to the committed one) and its sparse-checkout paths re-applied.
|
|
16
|
+
* With one or more `name` arguments, exactly those repos are restored, even
|
|
17
|
+
* if already clean -- an explicit ask is always honored. With no names, every
|
|
18
|
+
* DIRTY repo (per `status`) is restored and the clean ones are reported as
|
|
19
|
+
* skipped.
|
|
20
|
+
*
|
|
21
|
+
* A `ReposConfigError` with kind `"missing"` -- nothing vendored yet -- is
|
|
22
|
+
* the common, friendly case and always exits 0. A `ReposConfigError` with
|
|
23
|
+
* kind `"invalid"` means the manifest exists but is corrupt or unreadable.
|
|
24
|
+
* `RepoNotFoundError` means an explicitly named repo isn't in the manifest --
|
|
25
|
+
* a real failure, since restoring a name that was never vendored is almost
|
|
26
|
+
* always a typo the caller should see. `GitSubmoduleError` means the
|
|
27
|
+
* underlying git command failed (including the case where a repo has
|
|
28
|
+
* neither a staged nor a committed gitlink commit to restore to), and
|
|
29
|
+
* `ReposLockdownError` means the OS-permission lockdown pass on a vendored
|
|
30
|
+
* tree failed -- all four are real failures, logged and reported via a
|
|
31
|
+
* non-zero exit code.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```bash
|
|
35
|
+
* savvy repos restore my-repo
|
|
36
|
+
* savvy repos restore my-repo other-repo
|
|
37
|
+
* savvy repos restore
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
/* v8 ignore start -- CLI option/arg definitions */
|
|
43
|
+
const namesArg = Argument.string("name").pipe(Argument.variadic());
|
|
44
|
+
const cwdOption = Flag.directory("cwd").pipe(Flag.withDescription("Repo root to restore within"), Flag.withDefault("."));
|
|
45
|
+
/* v8 ignore stop */
|
|
46
|
+
/**
|
|
47
|
+
* Restore handler; exported for tests.
|
|
48
|
+
*
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
const runReposRestore = (cwd, names) => Effect.gen(function* () {
|
|
52
|
+
const result = yield* (yield* Repos.ReposManager).restore(cwd, names.length > 0 ? names : void 0);
|
|
53
|
+
for (const entry of result.restored) yield* Effect.log(`${entry.name}: restored to ${entry.commit}`);
|
|
54
|
+
for (const name of result.skippedClean) yield* Effect.log(`${name}: clean — skipped`);
|
|
55
|
+
if (result.restored.length === 0 && result.skippedClean.length === 0) yield* Effect.log("nothing to restore");
|
|
56
|
+
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
57
|
+
if (error.kind === "missing") return Effect.log("no .repos/config.json — nothing vendored");
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return Effect.log(error.message);
|
|
60
|
+
}), Effect.catchTag("RepoNotFoundError", (error) => {
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return Effect.log(error.message);
|
|
63
|
+
}), Effect.catchTag("GitSubmoduleError", (error) => {
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return Effect.log(error.message);
|
|
66
|
+
}), Effect.catchTag("ReposLockdownError", (error) => {
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return Effect.log(error.message);
|
|
69
|
+
}));
|
|
70
|
+
/* v8 ignore start -- CLI registration; handler tested via runReposRestore */
|
|
71
|
+
const restoreCommand = Command.make("restore", {
|
|
72
|
+
names: namesArg,
|
|
73
|
+
cwd: cwdOption
|
|
74
|
+
}, ({ names, cwd }) => runReposRestore(cwd, names)).pipe(Command.withDescription("Hard-reset vendored repos to their pinned commit; DESTRUCTIVE to uncommitted worktree edits. Given no names, restores every dirty repo"));
|
|
75
|
+
/* v8 ignore stop */
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
export { restoreCommand, runReposRestore };
|
|
@@ -16,16 +16,27 @@ import { Command, Flag } from "effect/unstable/cli";
|
|
|
16
16
|
* `"invalid"` means the manifest exists but is corrupt or unreadable -- that
|
|
17
17
|
* is a real failure, logged and reported via a non-zero exit code.
|
|
18
18
|
*
|
|
19
|
+
* With `--drift`, {@link Repos.ReposDrift.check} also runs (after the status
|
|
20
|
+
* check), reconciling the manifest, `.gitmodules`, the worktree, and `git
|
|
21
|
+
* submodule status`. Each detected drift prints as one line
|
|
22
|
+
* (`<name>: <kind> — <detail>`), and any drift flips the exit code to 1,
|
|
23
|
+
* mirroring the existing `!clean` rule for the status report itself. Because
|
|
24
|
+
* both calls read the same manifest, a missing manifest fails at the status
|
|
25
|
+
* call before the drift check ever runs, so the friendly exit-0 case is
|
|
26
|
+
* unaffected by `--drift`.
|
|
27
|
+
*
|
|
19
28
|
* @example
|
|
20
29
|
* ```bash
|
|
21
30
|
* savvy repos status
|
|
22
31
|
* savvy repos status --json
|
|
32
|
+
* savvy repos status --drift
|
|
23
33
|
* ```
|
|
24
34
|
*
|
|
25
35
|
* @internal
|
|
26
36
|
*/
|
|
27
37
|
/* v8 ignore start -- CLI option definitions */
|
|
28
38
|
const jsonOption = Flag.boolean("json").pipe(Flag.withDescription("Emit the structured drift report as JSON"), Flag.withDefault(false));
|
|
39
|
+
const driftOption = Flag.boolean("drift").pipe(Flag.withDescription("Also reconcile the manifest, .gitmodules, worktree, and git submodule status"), Flag.withDefault(false));
|
|
29
40
|
const cwdOption = Flag.directory("cwd").pipe(Flag.withDescription("Repo root to inspect"), Flag.withDefault("."));
|
|
30
41
|
/* v8 ignore stop */
|
|
31
42
|
/**
|
|
@@ -33,11 +44,20 @@ const cwdOption = Flag.directory("cwd").pipe(Flag.withDescription("Repo root to
|
|
|
33
44
|
*
|
|
34
45
|
* @internal
|
|
35
46
|
*/
|
|
36
|
-
const runReposStatus = (cwd, json) => Effect.gen(function* () {
|
|
47
|
+
const runReposStatus = (cwd, json, drift = false) => Effect.gen(function* () {
|
|
37
48
|
const report = yield* (yield* Repos.ReposManager).status(cwd);
|
|
38
49
|
if (!report.clean) process.exitCode = 1;
|
|
50
|
+
let driftReport;
|
|
51
|
+
if (drift) {
|
|
52
|
+
driftReport = yield* (yield* Repos.ReposDrift).check(cwd);
|
|
53
|
+
if (!driftReport.clean) process.exitCode = 1;
|
|
54
|
+
}
|
|
39
55
|
if (json) {
|
|
40
|
-
|
|
56
|
+
const payload = driftReport === void 0 ? report : {
|
|
57
|
+
...report,
|
|
58
|
+
drift: driftReport
|
|
59
|
+
};
|
|
60
|
+
yield* Console.log(JSON.stringify(payload, null, 2));
|
|
41
61
|
return;
|
|
42
62
|
}
|
|
43
63
|
for (const repo of report.repos) {
|
|
@@ -48,6 +68,7 @@ const runReposStatus = (cwd, json) => Effect.gen(function* () {
|
|
|
48
68
|
].filter((f) => f !== void 0);
|
|
49
69
|
yield* Effect.log(`${repo.name} @ ${repo.ref}${flags.length > 0 ? ` [${flags.join(", ")}]` : " [ok]"}`);
|
|
50
70
|
}
|
|
71
|
+
if (driftReport !== void 0) for (const item of driftReport.drifts) yield* Effect.log(`${item.name}: ${item.kind} — ${item.detail}`);
|
|
51
72
|
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
52
73
|
if (error.kind === "missing") {
|
|
53
74
|
if (json) return Console.log(JSON.stringify({
|
|
@@ -62,8 +83,9 @@ const runReposStatus = (cwd, json) => Effect.gen(function* () {
|
|
|
62
83
|
/* v8 ignore start -- CLI registration; handler tested via runReposStatus */
|
|
63
84
|
const statusCommand = Command.make("status", {
|
|
64
85
|
json: jsonOption,
|
|
86
|
+
drift: driftOption,
|
|
65
87
|
cwd: cwdOption
|
|
66
|
-
}, ({ json, cwd }) => runReposStatus(cwd, json)).pipe(Command.withDescription("Drift report: gitlink vs manifest ref, dirty and unsynced submodules"));
|
|
88
|
+
}, ({ json, drift, cwd }) => runReposStatus(cwd, json, drift)).pipe(Command.withDescription("Drift report: gitlink vs manifest ref, dirty and unsynced submodules"));
|
|
67
89
|
/* v8 ignore stop */
|
|
68
90
|
|
|
69
91
|
//#endregion
|
|
@@ -8,8 +8,10 @@ import { Command, Flag } from "effect/unstable/cli";
|
|
|
8
8
|
*
|
|
9
9
|
* @remarks
|
|
10
10
|
* A thin adapter over {@link Repos.ReposManager.sync}: initializes missing
|
|
11
|
-
* submodules, re-applies sparse-checkout patterns,
|
|
12
|
-
*
|
|
11
|
+
* submodules, re-applies sparse-checkout patterns, clears stale git locks
|
|
12
|
+
* left behind by an interrupted fetch, reconciles a submodule's URL when it
|
|
13
|
+
* drifts from the manifest, and registers an orphan manifest entry (no
|
|
14
|
+
* gitlink at all) from scratch. Sync is idempotent repair, so
|
|
13
15
|
* a `ReposConfigError` with kind `"missing"` -- the common, friendly case
|
|
14
16
|
* (nothing to sync yet) -- always exits 0. A `ReposConfigError` with kind
|
|
15
17
|
* `"invalid"` means the manifest exists but is corrupt or unreadable,
|
|
@@ -38,7 +40,9 @@ const runReposSync = (cwd) => Effect.gen(function* () {
|
|
|
38
40
|
for (const name of report.clearedLocks) yield* Effect.log(`${name}: cleared stale lock`);
|
|
39
41
|
for (const name of report.initialized) yield* Effect.log(`${name}: initialized`);
|
|
40
42
|
for (const name of report.sparseApplied) yield* Effect.log(`${name}: sparse-checkout applied`);
|
|
41
|
-
|
|
43
|
+
for (const name of report.urlSynced) yield* Effect.log(`${name}: url reconciled`);
|
|
44
|
+
for (const name of report.registered) yield* Effect.log(`${name}: registered`);
|
|
45
|
+
if (report.initialized.length === 0 && report.sparseApplied.length === 0 && report.clearedLocks.length === 0 && report.urlSynced.length === 0 && report.registered.length === 0) yield* Effect.log("all vendored repos up to date");
|
|
42
46
|
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
43
47
|
if (error.kind === "missing") return Effect.log("no .repos/config.json — nothing vendored");
|
|
44
48
|
process.exitCode = 1;
|
package/commands/repos/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { addCommand, runReposAdd } from "./commands/add.js";
|
|
2
2
|
import { noteCommand, runReposNote } from "./commands/note.js";
|
|
3
3
|
import { pinCommand, runReposPin } from "./commands/pin.js";
|
|
4
|
+
import { removeCommand, runReposRemove } from "./commands/remove.js";
|
|
5
|
+
import { renameCommand, runReposRename } from "./commands/rename.js";
|
|
6
|
+
import { restoreCommand, runReposRestore } from "./commands/restore.js";
|
|
4
7
|
import { runReposStatus, statusCommand } from "./commands/status.js";
|
|
5
8
|
import { runReposSync, syncCommand } from "./commands/sync.js";
|
|
6
9
|
import { Command } from "effect/unstable/cli";
|
|
@@ -12,7 +15,10 @@ const _reposCommand = Command.make("repos").pipe(Command.withSubcommands([
|
|
|
12
15
|
syncCommand,
|
|
13
16
|
pinCommand,
|
|
14
17
|
addCommand,
|
|
15
|
-
noteCommand
|
|
18
|
+
noteCommand,
|
|
19
|
+
removeCommand,
|
|
20
|
+
renameCommand,
|
|
21
|
+
restoreCommand
|
|
16
22
|
]), Command.withDescription("Vendored reference repos under .repos/"));
|
|
17
23
|
/**
|
|
18
24
|
* The `savvy repos` command group.
|
|
@@ -25,13 +31,22 @@ const _reposCommand = Command.make("repos").pipe(Command.withSubcommands([
|
|
|
25
31
|
* annotation preserves the exact Error/Requirements channels, so the root
|
|
26
32
|
* layer graph stays compiler-validated.
|
|
27
33
|
*
|
|
28
|
-
* The requirements channel names `Repos.ReposManager
|
|
29
|
-
* `Command.withSubcommands` propagates each
|
|
30
|
-
* the group's `R`, which the root assembly
|
|
34
|
+
* The requirements channel names `Repos.ReposManager | Repos.ReposDrift`
|
|
35
|
+
* rather than `never`: `Command.withSubcommands` propagates each
|
|
36
|
+
* subcommand's requirements up into the group's `R`, which the root assembly
|
|
37
|
+
* discharges via `AppLive`. `Repos.ReposDrift` joins the union because
|
|
38
|
+
* `status --drift` runs `Repos.ReposDrift.check` after the status check.
|
|
31
39
|
*
|
|
32
|
-
* The error channel
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
40
|
+
* The error channel is `Repos.GitSubmoduleError` alone: `add`/`pin`/`note`/
|
|
41
|
+
* `remove`/`rename`/`restore`/`sync` each `catchTag` every error their
|
|
42
|
+
* underlying `ReposManager` method can produce — `ReposConfigError`,
|
|
43
|
+
* `GitSubmoduleError`, `RepoNotFoundError`, `NoteNotFoundError`, and (for the
|
|
44
|
+
* six ops that unlock/re-lock the vendored tree around a git mutation)
|
|
45
|
+
* `ReposLockdownError` — down to a logged message and a non-zero exit code,
|
|
46
|
+
* so none of those propagate past the handler. `status` only `catchTag`s
|
|
47
|
+
* `ReposConfigError`; `GitSubmoduleError` from either `ReposManager.status`
|
|
48
|
+
* or (under `--drift`) `Repos.ReposDrift.check` is the sole channel that
|
|
49
|
+
* escapes uncaught, so it is the only member left in this union.
|
|
35
50
|
*/
|
|
36
51
|
const reposCommand = _reposCommand;
|
|
37
52
|
|
package/index.d.ts
CHANGED
|
@@ -272,7 +272,7 @@ declare const lintCommand: Command.Command<"lint", {}, {}, import("@effected/yam
|
|
|
272
272
|
*
|
|
273
273
|
* @internal
|
|
274
274
|
*/
|
|
275
|
-
declare const runReposStatus: (cwd: string, json: boolean) => Effect.Effect<void, Repos.GitSubmoduleError, Repos.ReposManager>;
|
|
275
|
+
declare const runReposStatus: (cwd: string, json: boolean, drift?: boolean) => Effect.Effect<void, Repos.GitSubmoduleError, Repos.ReposDrift | Repos.ReposManager>;
|
|
276
276
|
//#endregion
|
|
277
277
|
//#region src/commands/repos/commands/sync.d.ts
|
|
278
278
|
/**
|
|
@@ -294,15 +294,24 @@ declare const runReposSync: (cwd: string) => Effect.Effect<void, never, Repos.Re
|
|
|
294
294
|
* annotation preserves the exact Error/Requirements channels, so the root
|
|
295
295
|
* layer graph stays compiler-validated.
|
|
296
296
|
*
|
|
297
|
-
* The requirements channel names `Repos.ReposManager
|
|
298
|
-
* `Command.withSubcommands` propagates each
|
|
299
|
-
* the group's `R`, which the root assembly
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
297
|
+
* The requirements channel names `Repos.ReposManager | Repos.ReposDrift`
|
|
298
|
+
* rather than `never`: `Command.withSubcommands` propagates each
|
|
299
|
+
* subcommand's requirements up into the group's `R`, which the root assembly
|
|
300
|
+
* discharges via `AppLive`. `Repos.ReposDrift` joins the union because
|
|
301
|
+
* `status --drift` runs `Repos.ReposDrift.check` after the status check.
|
|
302
|
+
*
|
|
303
|
+
* The error channel is `Repos.GitSubmoduleError` alone: `add`/`pin`/`note`/
|
|
304
|
+
* `remove`/`rename`/`restore`/`sync` each `catchTag` every error their
|
|
305
|
+
* underlying `ReposManager` method can produce — `ReposConfigError`,
|
|
306
|
+
* `GitSubmoduleError`, `RepoNotFoundError`, `NoteNotFoundError`, and (for the
|
|
307
|
+
* six ops that unlock/re-lock the vendored tree around a git mutation)
|
|
308
|
+
* `ReposLockdownError` — down to a logged message and a non-zero exit code,
|
|
309
|
+
* so none of those propagate past the handler. `status` only `catchTag`s
|
|
310
|
+
* `ReposConfigError`; `GitSubmoduleError` from either `ReposManager.status`
|
|
311
|
+
* or (under `--drift`) `Repos.ReposDrift.check` is the sole channel that
|
|
312
|
+
* escapes uncaught, so it is the only member left in this union.
|
|
304
313
|
*/
|
|
305
|
-
declare const reposCommand: Command.Command<"repos", Record<string, never>, Record<string, never>, Repos.GitSubmoduleError
|
|
314
|
+
declare const reposCommand: Command.Command<"repos", Record<string, never>, Record<string, never>, Repos.GitSubmoduleError, Repos.ReposManager | Repos.ReposDrift>;
|
|
306
315
|
//#endregion
|
|
307
316
|
export { changesetCommand, checkCommand, commitCommand, initCommand, lintCommand, reposCommand, runChangesetCheck, runChangesetInit, runCheck, runCli, runCommitCheck, runCommitInit, runInit, runLintCheck, runLintInit, runReposStatus, runReposSync };
|
|
308
317
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The savvy CLI — unified commit, changeset, and lint commands for the Silk Suite",
|
|
6
6
|
"homepage": "https://github.com/savvy-web/systems/tree/main/packages/cli",
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@effect/platform-node": "4.0.0-beta.101",
|
|
35
35
|
"@effected/commands": "^0.3.1",
|
|
36
|
-
"@effected/git": "^0.
|
|
36
|
+
"@effected/git": "^0.6.0",
|
|
37
37
|
"@effected/jsonc": "^0.5.2",
|
|
38
38
|
"@effected/templates": "^0.1.1",
|
|
39
|
-
"@effected/workspaces": "^0.10.
|
|
39
|
+
"@effected/workspaces": "^0.10.2",
|
|
40
40
|
"@effected/yaml": "^0.6.1",
|
|
41
|
-
"@savvy-web/silk-effects": "5.
|
|
41
|
+
"@savvy-web/silk-effects": "5.5.0",
|
|
42
42
|
"effect": "4.0.0-beta.101"
|
|
43
43
|
}
|
|
44
44
|
}
|