@savvy-web/cli 1.5.9 → 1.6.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/cli/index.js +12 -4
- package/commands/repos/commands/add.js +73 -0
- package/commands/repos/commands/note.js +106 -0
- package/commands/repos/commands/pin.js +64 -0
- package/commands/repos/commands/status.js +70 -0
- package/commands/repos/commands/sync.js +53 -0
- package/commands/repos/index.js +28 -0
- package/index.d.ts +29 -2
- package/index.js +4 -1
- package/package.json +2 -2
package/cli/index.js
CHANGED
|
@@ -4,9 +4,10 @@ import { cleanCommand } from "../commands/clean.js";
|
|
|
4
4
|
import { commitCommand } from "../commands/commit/index.js";
|
|
5
5
|
import { initCommand } from "../commands/init.js";
|
|
6
6
|
import { lintCommand } from "../commands/lint/index.js";
|
|
7
|
+
import { reposCommand } from "../commands/repos/index.js";
|
|
7
8
|
import { Command } from "@effect/cli";
|
|
8
9
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
9
|
-
import { BiomeSchemaSyncLive, ChangesetConfigLive, ChangesetConfigReaderLive, Changesets, ConfigDiscoveryLive, ManagedSectionLive, SilkPublishabilityDetectorLive, ToolDiscoveryLive, VersioningStrategyLive } from "@savvy-web/silk-effects";
|
|
10
|
+
import { BiomeSchemaSyncLive, ChangesetConfigLive, ChangesetConfigReaderLive, Changesets, ConfigDiscoveryLive, ManagedSectionLive, Repos, SilkPublishabilityDetectorLive, ToolDiscoveryLive, VersioningStrategyLive } from "@savvy-web/silk-effects";
|
|
10
11
|
import { Effect, Layer } from "effect";
|
|
11
12
|
import { PackageManagerDetectorLive, PointInTimeWorkspaceLive, PublishabilityDetectorLive, WorkspaceDiscoveryLive, WorkspaceRootLive } from "workspaces-effect";
|
|
12
13
|
|
|
@@ -52,11 +53,12 @@ const rootCommand = Command.make("savvy").pipe(Command.withSubcommands([
|
|
|
52
53
|
cleanCommand,
|
|
53
54
|
commitCommand,
|
|
54
55
|
changesetCommand,
|
|
55
|
-
lintCommand
|
|
56
|
+
lintCommand,
|
|
57
|
+
reposCommand
|
|
56
58
|
]));
|
|
57
59
|
const cli = Command.run(rootCommand, {
|
|
58
60
|
name: "savvy",
|
|
59
|
-
version: "1.
|
|
61
|
+
version: "1.6.0"
|
|
60
62
|
});
|
|
61
63
|
/**
|
|
62
64
|
* Shared base layer: workspace services, the changeset config reader, and the
|
|
@@ -128,7 +130,13 @@ const InspectorAndAnalyzerLive = Changesets.BranchAnalyzerLive.pipe(Layer.provid
|
|
|
128
130
|
* remaining `CommandExecutor`/`FileSystem`/`Path` flow up to `NodeContext.layer`.
|
|
129
131
|
*/
|
|
130
132
|
const DepsRegenGroupLive = Changesets.DepsRegenLive.pipe(Layer.provide(InspectorAndAnalyzerLive), Layer.provide(PointInTimeWorkspaceLive.pipe(Layer.provide(WorkspaceLive))), Layer.provide(PublishabilityDetectorLive), Layer.provide(ChangesetConfigLive.pipe(Layer.provide(ChangesetConfigReaderLive))));
|
|
131
|
-
|
|
133
|
+
/**
|
|
134
|
+
* `Repos.ReposManager`, provided its `Repos.ReposConfigStoreLive` dependency.
|
|
135
|
+
* Both draw their platform requirements (`FileSystem`, `Path`,
|
|
136
|
+
* `CommandExecutor`) from `NodeContext.layer` below.
|
|
137
|
+
*/
|
|
138
|
+
const ReposGroupLive = Repos.ReposManagerLive.pipe(Layer.provide(Repos.ReposConfigStoreLive));
|
|
139
|
+
const AppLive = Layer.mergeAll(ToolDiscoveryLive, VersioningStrategyLive, InspectorAndAnalyzerLive, DepsRegenGroupLive, ReposGroupLive).pipe(Layer.provideMerge(BaseLive), Layer.provideMerge(NodeContext.layer));
|
|
132
140
|
/**
|
|
133
141
|
* Bootstrap and run the `savvy` CLI application.
|
|
134
142
|
*
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Repos } from "@savvy-web/silk-effects";
|
|
3
|
+
import { Effect, Option } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/repos/commands/add.ts
|
|
6
|
+
/**
|
|
7
|
+
* `repos add` command -- vendor a new reference repo under `.repos/`.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A thin adapter over {@link Repos.ReposManager.add}: adds a shallow git
|
|
11
|
+
* submodule, checks it out at the requested ref, optionally applies a
|
|
12
|
+
* sparse-checkout, and writes a new manifest entry -- it does not commit, so
|
|
13
|
+
* the caller reviews and commits the staged `.gitmodules`/manifest/gitlink
|
|
14
|
+
* change. A `ReposConfigError` with kind `"missing"` -- no manifest yet --
|
|
15
|
+
* is the common, friendly case and always exits 0 (the manifest is created
|
|
16
|
+
* on demand by `add` itself, so this only fires on a read that raced a
|
|
17
|
+
* concurrent removal). A `ReposConfigError` with kind `"invalid"` means the
|
|
18
|
+
* manifest exists but is corrupt or unreadable, and `GitSubmoduleError`
|
|
19
|
+
* means the underlying git command failed -- both are real failures, logged
|
|
20
|
+
* and reported via a non-zero exit code.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```bash
|
|
24
|
+
* savvy repos add https://github.com/foo/bar --ref v1.0.0 --purpose "vendor demo"
|
|
25
|
+
* savvy repos add https://github.com/foo/bar --ref main --purpose "vendor demo" --sparse src --sparse docs --name bar-vendored
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
/* v8 ignore start -- CLI option/arg definitions */
|
|
31
|
+
const urlArg = Args.text({ name: "url" });
|
|
32
|
+
const refOption = Options.text("ref").pipe(Options.withDescription("Ref (tag, branch, or commit) to check out"));
|
|
33
|
+
const purposeOption = Options.text("purpose").pipe(Options.withDescription("Why this repo is vendored"));
|
|
34
|
+
const nameOption = Options.text("name").pipe(Options.withDescription("Vendored directory name; defaults to the URL's last path segment"), Options.optional);
|
|
35
|
+
const sparseOption = Options.text("sparse").pipe(Options.withDescription("Sparse-checkout path; repeatable"), Options.repeated);
|
|
36
|
+
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Repo root to add within"), Options.withDefault("."));
|
|
37
|
+
/* v8 ignore stop */
|
|
38
|
+
/**
|
|
39
|
+
* Add handler; exported for tests.
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
const runReposAdd = (cwd, opts) => Effect.gen(function* () {
|
|
44
|
+
const result = yield* (yield* Repos.ReposManager).add(cwd, opts);
|
|
45
|
+
yield* Effect.log(`${result.name} @ ${result.ref} -> ${result.path}`);
|
|
46
|
+
yield* Effect.log("staged — review and commit");
|
|
47
|
+
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
48
|
+
if (error.kind === "missing") return Effect.log("no .repos/config.json — nothing vendored");
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return Effect.log(error.message);
|
|
51
|
+
}), Effect.catchTag("GitSubmoduleError", (error) => {
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return Effect.log(error.message);
|
|
54
|
+
}));
|
|
55
|
+
/* v8 ignore start -- CLI registration; handler tested via runReposAdd */
|
|
56
|
+
const addCommand = Command.make("add", {
|
|
57
|
+
url: urlArg,
|
|
58
|
+
ref: refOption,
|
|
59
|
+
purpose: purposeOption,
|
|
60
|
+
name: nameOption,
|
|
61
|
+
sparse: sparseOption,
|
|
62
|
+
cwd: cwdOption
|
|
63
|
+
}, ({ url, ref, purpose, name, sparse, cwd }) => runReposAdd(cwd, {
|
|
64
|
+
url,
|
|
65
|
+
ref,
|
|
66
|
+
purpose,
|
|
67
|
+
...Option.isSome(name) ? { name: name.value } : {},
|
|
68
|
+
...sparse.length > 0 ? { sparse } : {}
|
|
69
|
+
})).pipe(Command.withDescription("Vendor a new reference repo under .repos/; stages the change without committing"));
|
|
70
|
+
/* v8 ignore stop */
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { addCommand, runReposAdd };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Repos } from "@savvy-web/silk-effects";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/repos/commands/note.ts
|
|
6
|
+
/**
|
|
7
|
+
* `repos note` command group -- agent notes for a vendored `.repos/` repo.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A thin adapter over {@link Repos.ReposManager.note}: `add` appends a note
|
|
11
|
+
* (capped at the manifest's per-repo note limit), `remove` deletes one, and
|
|
12
|
+
* `promote` folds a note's text into the entry's curated orientation
|
|
13
|
+
* (`layout` or `startHere`) and removes it from the note list. `name` and
|
|
14
|
+
* `--cwd` are parsed on the parent `note` command and threaded to whichever
|
|
15
|
+
* leaf ran via Effect's `Command.Context` mechanism (the parent `Command`
|
|
16
|
+
* doubles as an `Effect` requiring its own context tag, which
|
|
17
|
+
* `Command.withSubcommands` provides to the chosen leaf's handler).
|
|
18
|
+
*
|
|
19
|
+
* A `ReposConfigError` with kind `"missing"` -- nothing vendored yet -- is
|
|
20
|
+
* the common, friendly case and always exits 0. A `ReposConfigError` with
|
|
21
|
+
* kind `"invalid"` means the manifest exists but is corrupt or unreadable,
|
|
22
|
+
* `RepoNotFoundError` means the named repo isn't in the manifest, and
|
|
23
|
+
* `NoteNotFoundError` means the note id doesn't exist on that repo -- all
|
|
24
|
+
* three are real failures, logged and reported via a non-zero exit code.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```bash
|
|
28
|
+
* savvy repos note my-repo add "entry point is src/index.ts"
|
|
29
|
+
* savvy repos note my-repo remove n-1234
|
|
30
|
+
* savvy repos note my-repo promote n-1234 --into startHere
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Note handler; exported for tests.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
const runReposNote = (cwd, name, op) => Effect.gen(function* () {
|
|
41
|
+
const result = yield* (yield* Repos.ReposManager).note(cwd, name, op);
|
|
42
|
+
yield* Effect.log(`${result.name}: ${result.op} note ${result.id} (${result.noteCount} notes)`);
|
|
43
|
+
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
44
|
+
if (error.kind === "missing") return Effect.log("no .repos/config.json — nothing vendored");
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return Effect.log(error.message);
|
|
47
|
+
}), Effect.catchTag("RepoNotFoundError", (error) => {
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return Effect.log(error.message);
|
|
50
|
+
}), Effect.catchTag("NoteNotFoundError", (error) => {
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return Effect.log(error.message);
|
|
53
|
+
}));
|
|
54
|
+
/* v8 ignore start -- CLI registration; handler tested via runReposNote */
|
|
55
|
+
const nameArg = Args.text({ name: "name" });
|
|
56
|
+
const noteTextArg = Args.text({ name: "text" });
|
|
57
|
+
const noteIdArg = Args.text({ name: "id" });
|
|
58
|
+
const intoOption = Options.choice("into", ["layout", "startHere"]).pipe(Options.withDescription("Curated orientation field to promote the note into"));
|
|
59
|
+
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Repo root whose manifest holds the notes"), Options.withDefault("."));
|
|
60
|
+
const _noteGroup = Command.make("note", {
|
|
61
|
+
name: nameArg,
|
|
62
|
+
cwd: cwdOption
|
|
63
|
+
});
|
|
64
|
+
const addLeaf = Command.make("add", { note: noteTextArg }, ({ note }) => Effect.gen(function* () {
|
|
65
|
+
const { name, cwd } = yield* _noteGroup;
|
|
66
|
+
yield* runReposNote(cwd, name, {
|
|
67
|
+
op: "add",
|
|
68
|
+
note
|
|
69
|
+
});
|
|
70
|
+
})).pipe(Command.withDescription("Append an agent note to a vendored repo"));
|
|
71
|
+
const removeLeaf = Command.make("remove", { id: noteIdArg }, ({ id }) => Effect.gen(function* () {
|
|
72
|
+
const { name, cwd } = yield* _noteGroup;
|
|
73
|
+
yield* runReposNote(cwd, name, {
|
|
74
|
+
op: "remove",
|
|
75
|
+
id
|
|
76
|
+
});
|
|
77
|
+
})).pipe(Command.withDescription("Remove an agent note from a vendored repo"));
|
|
78
|
+
const promoteLeaf = Command.make("promote", {
|
|
79
|
+
id: noteIdArg,
|
|
80
|
+
into: intoOption
|
|
81
|
+
}, ({ id, into }) => Effect.gen(function* () {
|
|
82
|
+
const { name, cwd } = yield* _noteGroup;
|
|
83
|
+
yield* runReposNote(cwd, name, {
|
|
84
|
+
op: "promote",
|
|
85
|
+
id,
|
|
86
|
+
into
|
|
87
|
+
});
|
|
88
|
+
})).pipe(Command.withDescription("Promote an agent note into curated orientation (layout or startHere)"));
|
|
89
|
+
const _noteCommand = _noteGroup.pipe(Command.withSubcommands([
|
|
90
|
+
addLeaf,
|
|
91
|
+
removeLeaf,
|
|
92
|
+
promoteLeaf
|
|
93
|
+
]), Command.withDescription("Agent notes for a vendored repo: add, remove, promote"));
|
|
94
|
+
/**
|
|
95
|
+
* The `savvy repos note` command group.
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* Typed as `unknown` at the export boundary to avoid TypeScript declaration-emit
|
|
99
|
+
* errors from Effect's internal types (TS4023), matching the `reposCommand`
|
|
100
|
+
* export pattern.
|
|
101
|
+
*/
|
|
102
|
+
const noteCommand = _noteCommand;
|
|
103
|
+
/* v8 ignore stop */
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { noteCommand, runReposNote };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Repos } from "@savvy-web/silk-effects";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/repos/commands/pin.ts
|
|
6
|
+
/**
|
|
7
|
+
* `repos pin` command -- re-pin a vendored `.repos/` submodule to a new ref.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A thin adapter over {@link Repos.ReposManager.pin}: shallow-fetches the new
|
|
11
|
+
* ref, detaches HEAD onto it, rewrites the manifest entry, and stages the
|
|
12
|
+
* gitlink plus the manifest -- it does not commit, so the caller reviews and
|
|
13
|
+
* commits the staged change with the ready-made message the result carries.
|
|
14
|
+
* A `ReposConfigError` with kind `"missing"` -- nothing vendored yet -- is
|
|
15
|
+
* the common, friendly case and always exits 0. A `ReposConfigError` with
|
|
16
|
+
* kind `"invalid"` means the manifest exists but is corrupt or unreadable,
|
|
17
|
+
* `GitSubmoduleError` means the underlying git command failed, and
|
|
18
|
+
* `RepoNotFoundError` means the named repo isn't in the manifest -- all
|
|
19
|
+
* three are real failures, logged and reported via a non-zero exit code.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```bash
|
|
23
|
+
* savvy repos pin my-repo v2.0.0
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
/* v8 ignore start -- CLI option/arg definitions */
|
|
29
|
+
const nameArg = Args.text({ name: "name" });
|
|
30
|
+
const refArg = Args.text({ name: "ref" });
|
|
31
|
+
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Repo root to pin within"), Options.withDefault("."));
|
|
32
|
+
/* v8 ignore stop */
|
|
33
|
+
/**
|
|
34
|
+
* Pin handler; exported for tests.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
const runReposPin = (cwd, name, ref) => Effect.gen(function* () {
|
|
39
|
+
const result = yield* (yield* Repos.ReposManager).pin(cwd, name, ref);
|
|
40
|
+
yield* Effect.log(`${result.name}: ${result.oldCommit ?? "unknown"} -> ${result.newCommit}`);
|
|
41
|
+
yield* Effect.log(result.commitMessage);
|
|
42
|
+
yield* Effect.log("staged — review and commit");
|
|
43
|
+
for (const staleId of result.staleNoteIds) yield* Effect.log(`warning: note ${staleId} is now stale against ${ref}`);
|
|
44
|
+
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
45
|
+
if (error.kind === "missing") return Effect.log("no .repos/config.json — nothing vendored");
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return Effect.log(error.message);
|
|
48
|
+
}), Effect.catchTag("GitSubmoduleError", (error) => {
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return Effect.log(error.message);
|
|
51
|
+
}), Effect.catchTag("RepoNotFoundError", (error) => {
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return Effect.log(error.message);
|
|
54
|
+
}));
|
|
55
|
+
/* v8 ignore start -- CLI registration; handler tested via runReposPin */
|
|
56
|
+
const pinCommand = Command.make("pin", {
|
|
57
|
+
name: nameArg,
|
|
58
|
+
ref: refArg,
|
|
59
|
+
cwd: cwdOption
|
|
60
|
+
}, ({ name, ref, cwd }) => runReposPin(cwd, name, ref)).pipe(Command.withDescription("Re-pin a vendored repo to a new ref; stages the change without committing"));
|
|
61
|
+
/* v8 ignore stop */
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { pinCommand, runReposPin };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Repos } from "@savvy-web/silk-effects";
|
|
3
|
+
import { Console, Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/repos/commands/status.ts
|
|
6
|
+
/**
|
|
7
|
+
* `repos status` command -- drift report for vendored `.repos/` submodules.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A thin adapter over {@link Repos.ReposManager.status}: reports, per vendored
|
|
11
|
+
* repo, whether the submodule is present, dirty, and whether any agent notes
|
|
12
|
+
* have gone stale relative to the pinned ref. A `ReposConfigError` with kind
|
|
13
|
+
* `"missing"` is the common, friendly case (nothing has been vendored yet) --
|
|
14
|
+
* not an error -- so it is rendered as a plain message (or an empty JSON
|
|
15
|
+
* report, in `--json` mode) with exit code 0. A `ReposConfigError` with kind
|
|
16
|
+
* `"invalid"` means the manifest exists but is corrupt or unreadable -- that
|
|
17
|
+
* is a real failure, logged and reported via a non-zero exit code.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```bash
|
|
21
|
+
* savvy repos status
|
|
22
|
+
* savvy repos status --json
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
/* v8 ignore start -- CLI option definitions */
|
|
28
|
+
const jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit the structured drift report as JSON"), Options.withDefault(false));
|
|
29
|
+
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Repo root to inspect"), Options.withDefault("."));
|
|
30
|
+
/* v8 ignore stop */
|
|
31
|
+
/**
|
|
32
|
+
* Drift report handler; exported for tests.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
const runReposStatus = (cwd, json) => Effect.gen(function* () {
|
|
37
|
+
const report = yield* (yield* Repos.ReposManager).status(cwd);
|
|
38
|
+
if (!report.clean) process.exitCode = 1;
|
|
39
|
+
if (json) {
|
|
40
|
+
yield* Console.log(JSON.stringify(report, null, 2));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const repo of report.repos) {
|
|
44
|
+
const flags = [
|
|
45
|
+
repo.present ? void 0 : "missing",
|
|
46
|
+
repo.dirty ? "dirty" : void 0,
|
|
47
|
+
repo.staleNoteIds.length > 0 ? `${repo.staleNoteIds.length} stale notes` : void 0
|
|
48
|
+
].filter((f) => f !== void 0);
|
|
49
|
+
yield* Effect.log(`${repo.name} @ ${repo.ref}${flags.length > 0 ? ` [${flags.join(", ")}]` : " [ok]"}`);
|
|
50
|
+
}
|
|
51
|
+
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
52
|
+
if (error.kind === "missing") {
|
|
53
|
+
if (json) return Console.log(JSON.stringify({
|
|
54
|
+
repos: [],
|
|
55
|
+
clean: true
|
|
56
|
+
}, null, 2));
|
|
57
|
+
return Effect.log("no .repos/config.json — nothing vendored");
|
|
58
|
+
}
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return Effect.log(error.message);
|
|
61
|
+
}));
|
|
62
|
+
/* v8 ignore start -- CLI registration; handler tested via runReposStatus */
|
|
63
|
+
const statusCommand = Command.make("status", {
|
|
64
|
+
json: jsonOption,
|
|
65
|
+
cwd: cwdOption
|
|
66
|
+
}, ({ json, cwd }) => runReposStatus(cwd, json)).pipe(Command.withDescription("Drift report: gitlink vs manifest ref, dirty and unsynced submodules"));
|
|
67
|
+
/* v8 ignore stop */
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
export { runReposStatus, statusCommand };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli";
|
|
2
|
+
import { Repos } from "@savvy-web/silk-effects";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/repos/commands/sync.ts
|
|
6
|
+
/**
|
|
7
|
+
* `repos sync` command -- reconcile vendored `.repos/` submodules with the manifest.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* A thin adapter over {@link Repos.ReposManager.sync}: initializes missing
|
|
11
|
+
* submodules, re-applies sparse-checkout patterns, and clears stale git
|
|
12
|
+
* locks left behind by an interrupted fetch. Sync is idempotent repair, so
|
|
13
|
+
* a `ReposConfigError` with kind `"missing"` -- the common, friendly case
|
|
14
|
+
* (nothing to sync yet) -- always exits 0. A `ReposConfigError` with kind
|
|
15
|
+
* `"invalid"` means the manifest exists but is corrupt or unreadable, and
|
|
16
|
+
* `GitSubmoduleError` means the underlying git command failed -- both are
|
|
17
|
+
* real failures, logged and reported via a non-zero exit code.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```bash
|
|
21
|
+
* savvy repos sync
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
/* v8 ignore start -- CLI option definitions */
|
|
27
|
+
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Repo root to sync"), Options.withDefault("."));
|
|
28
|
+
/* v8 ignore stop */
|
|
29
|
+
/**
|
|
30
|
+
* Sync handler; exported for tests.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
const runReposSync = (cwd) => Effect.gen(function* () {
|
|
35
|
+
const report = yield* (yield* Repos.ReposManager).sync(cwd);
|
|
36
|
+
for (const name of report.clearedLocks) yield* Effect.log(`${name}: cleared stale lock`);
|
|
37
|
+
for (const name of report.initialized) yield* Effect.log(`${name}: initialized`);
|
|
38
|
+
for (const name of report.sparseApplied) yield* Effect.log(`${name}: sparse-checkout applied`);
|
|
39
|
+
if (report.initialized.length === 0 && report.sparseApplied.length === 0 && report.clearedLocks.length === 0) yield* Effect.log("all vendored repos up to date");
|
|
40
|
+
}).pipe(Effect.catchTag("ReposConfigError", (error) => {
|
|
41
|
+
if (error.kind === "missing") return Effect.log("no .repos/config.json — nothing vendored");
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return Effect.log(error.message);
|
|
44
|
+
}), Effect.catchTag("GitSubmoduleError", (error) => {
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return Effect.log(error.message);
|
|
47
|
+
}));
|
|
48
|
+
/* v8 ignore start -- CLI registration; handler tested via runReposSync */
|
|
49
|
+
const syncCommand = Command.make("sync", { cwd: cwdOption }, ({ cwd }) => runReposSync(cwd)).pipe(Command.withDescription("Reconcile vendored submodules with the manifest: init missing, apply sparse, clear locks"));
|
|
50
|
+
/* v8 ignore stop */
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { runReposSync, syncCommand };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { addCommand, runReposAdd } from "./commands/add.js";
|
|
2
|
+
import { noteCommand, runReposNote } from "./commands/note.js";
|
|
3
|
+
import { pinCommand, runReposPin } from "./commands/pin.js";
|
|
4
|
+
import { runReposStatus, statusCommand } from "./commands/status.js";
|
|
5
|
+
import { runReposSync, syncCommand } from "./commands/sync.js";
|
|
6
|
+
import { Command } from "@effect/cli";
|
|
7
|
+
|
|
8
|
+
//#region src/commands/repos/index.ts
|
|
9
|
+
/* v8 ignore start -- CLI registration; each command tested via exported handler */
|
|
10
|
+
const _reposCommand = Command.make("repos").pipe(Command.withSubcommands([
|
|
11
|
+
statusCommand,
|
|
12
|
+
syncCommand,
|
|
13
|
+
pinCommand,
|
|
14
|
+
addCommand,
|
|
15
|
+
noteCommand
|
|
16
|
+
]), Command.withDescription("Vendored reference repos under .repos/"));
|
|
17
|
+
/**
|
|
18
|
+
* The `savvy repos` command group.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Typed as `unknown` at the export boundary to avoid TypeScript declaration-emit
|
|
22
|
+
* errors from Effect's internal types (TS4023), matching the `changesetCommand`
|
|
23
|
+
* export pattern.
|
|
24
|
+
*/
|
|
25
|
+
const reposCommand = _reposCommand;
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
export { reposCommand, runReposStatus, runReposSync };
|
package/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Effect } from "effect";
|
|
|
3
3
|
import { WorkspaceDiscovery, WorkspaceRoot } from "workspaces-effect";
|
|
4
4
|
import { FileSystem } from "@effect/platform";
|
|
5
5
|
import { PlatformError } from "@effect/platform/Error";
|
|
6
|
-
import { BiomeSchemaSync, ConfigDiscovery, ManagedSection, SectionParseError, SectionWriteError, ToolDiscovery, VersioningStrategy } from "@savvy-web/silk-effects";
|
|
6
|
+
import { BiomeSchemaSync, ConfigDiscovery, ManagedSection, Repos, SectionParseError, SectionWriteError, ToolDiscovery, VersioningStrategy } from "@savvy-web/silk-effects";
|
|
7
7
|
import { JsoncParseError } from "jsonc-effect";
|
|
8
8
|
//#region src/cli/index.d.ts
|
|
9
9
|
/**
|
|
@@ -232,5 +232,32 @@ declare function runLintInit(opts: {
|
|
|
232
232
|
*/
|
|
233
233
|
declare const lintCommand: Command.Command<"lint", any, any, any>;
|
|
234
234
|
//#endregion
|
|
235
|
-
|
|
235
|
+
//#region src/commands/repos/commands/status.d.ts
|
|
236
|
+
/**
|
|
237
|
+
* Drift report handler; exported for tests.
|
|
238
|
+
*
|
|
239
|
+
* @internal
|
|
240
|
+
*/
|
|
241
|
+
declare const runReposStatus: (cwd: string, json: boolean) => Effect.Effect<void, Repos.GitSubmoduleError, Repos.ReposManager>;
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/commands/repos/commands/sync.d.ts
|
|
244
|
+
/**
|
|
245
|
+
* Sync handler; exported for tests.
|
|
246
|
+
*
|
|
247
|
+
* @internal
|
|
248
|
+
*/
|
|
249
|
+
declare const runReposSync: (cwd: string) => Effect.Effect<void, never, Repos.ReposManager>;
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/commands/repos/index.d.ts
|
|
252
|
+
/**
|
|
253
|
+
* The `savvy repos` command group.
|
|
254
|
+
*
|
|
255
|
+
* @remarks
|
|
256
|
+
* Typed as `unknown` at the export boundary to avoid TypeScript declaration-emit
|
|
257
|
+
* errors from Effect's internal types (TS4023), matching the `changesetCommand`
|
|
258
|
+
* export pattern.
|
|
259
|
+
*/
|
|
260
|
+
declare const reposCommand: Command.Command<"repos", any, any, any>;
|
|
261
|
+
//#endregion
|
|
262
|
+
export { changesetCommand, checkCommand, commitCommand, initCommand, lintCommand, reposCommand, runChangesetCheck, runChangesetInit, runCheck, runCli, runCommitCheck, runCommitInit, runInit, runLintCheck, runLintInit, runReposStatus, runReposSync };
|
|
236
263
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -9,6 +9,9 @@ import { commitCommand } from "./commands/commit/index.js";
|
|
|
9
9
|
import { runLintInit } from "./commands/lint/init.js";
|
|
10
10
|
import { initCommand, runInit } from "./commands/init.js";
|
|
11
11
|
import { lintCommand } from "./commands/lint/index.js";
|
|
12
|
+
import { runReposStatus } from "./commands/repos/commands/status.js";
|
|
13
|
+
import { runReposSync } from "./commands/repos/commands/sync.js";
|
|
14
|
+
import { reposCommand } from "./commands/repos/index.js";
|
|
12
15
|
import { runCli } from "./cli/index.js";
|
|
13
16
|
|
|
14
|
-
export { changesetCommand, checkCommand, commitCommand, initCommand, lintCommand, runChangesetCheck, runChangesetInit, runCheck, runCli, runCommitCheck, runCommitInit, runInit, runLintCheck, runLintInit };
|
|
17
|
+
export { changesetCommand, checkCommand, commitCommand, initCommand, lintCommand, reposCommand, runChangesetCheck, runChangesetInit, runCheck, runCli, runCommitCheck, runCommitInit, runInit, runLintCheck, runLintInit, runReposStatus, runReposSync };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@effect/sql": "^0.51.1",
|
|
42
42
|
"@effect/typeclass": "^0.40.0",
|
|
43
43
|
"@effect/workflow": "^0.18.2",
|
|
44
|
-
"@savvy-web/silk-effects": "3.
|
|
44
|
+
"@savvy-web/silk-effects": "3.3.0",
|
|
45
45
|
"effect": "^3.21.4",
|
|
46
46
|
"jsonc-effect": "^0.3.1",
|
|
47
47
|
"workspaces-effect": "^2.0.3",
|