reposets 0.2.0 → 0.3.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/500.js +98 -66
- package/bin/reposets.js +87 -112
- package/index.d.ts +41 -33
- package/index.js +1 -1
- package/package.json +2 -2
- package/tsdoc-metadata.json +1 -1
package/500.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Context, Data, Effect, Layer, Option, Ref, Schema } from "effect";
|
|
2
2
|
import { blake2b } from "blakejs";
|
|
3
3
|
import tweetnacl from "tweetnacl";
|
|
4
|
-
import { AppDirsConfig, ConfigError, ConfigFile, FirstMatch, Jsonifiable, TomlCodec, UpwardWalk,
|
|
4
|
+
import { AppDirsConfig, ConfigError, ConfigFile, ExplicitPath, FirstMatch, Jsonifiable, StaticDir, TomlCodec, UpwardWalk, XdgConfigLive, XdgConfigResolver, XdgSavePath, taplo, tombi } from "xdg-effect";
|
|
5
5
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
-
import {
|
|
6
|
+
import { isAbsolute, resolve } from "node:path";
|
|
7
7
|
import { Octokit } from "@octokit/rest";
|
|
8
8
|
class ResolveError extends Data.TaggedError("ResolveError") {
|
|
9
9
|
}
|
|
@@ -704,16 +704,11 @@ const RulesetSchema = Schema.Union(BranchRulesetSchema, TagRulesetSchema).annota
|
|
|
704
704
|
identifier: "Ruleset",
|
|
705
705
|
title: "Repository ruleset",
|
|
706
706
|
description: "A set of rules to apply when specified conditions are met",
|
|
707
|
-
jsonSchema: {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
links: {
|
|
713
|
-
key: "https://github.com/spencerbeggs/reposets/blob/main/docs/rulesets.md"
|
|
714
|
-
}
|
|
715
|
-
})
|
|
716
|
-
}
|
|
707
|
+
jsonSchema: taplo({
|
|
708
|
+
links: {
|
|
709
|
+
key: "https://github.com/spencerbeggs/reposets/blob/main/docs/rulesets.md"
|
|
710
|
+
}
|
|
711
|
+
})
|
|
717
712
|
});
|
|
718
713
|
function buildRulesetPayload(ruleset) {
|
|
719
714
|
const rules = [];
|
|
@@ -1372,64 +1367,101 @@ const CONFIG_FILENAME = "reposets.config.toml";
|
|
|
1372
1367
|
const CREDENTIALS_FILENAME = "reposets.credentials.toml";
|
|
1373
1368
|
const ReposetsConfigFile = ConfigFile.Tag("reposets/Config");
|
|
1374
1369
|
const ReposetsCredentialsFile = ConfigFile.Tag("reposets/Credentials");
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
config
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
})
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
})
|
|
1391
|
-
|
|
1370
|
+
function validateConfigRefs(config) {
|
|
1371
|
+
const errors = [];
|
|
1372
|
+
const definedSettings = new Set(Object.keys(config.settings));
|
|
1373
|
+
const definedSecrets = new Set(Object.keys(config.secrets));
|
|
1374
|
+
const definedVariables = new Set(Object.keys(config.variables));
|
|
1375
|
+
const definedRulesets = new Set(Object.keys(config.rulesets));
|
|
1376
|
+
const definedEnvironments = new Set(Object.keys(config.environments));
|
|
1377
|
+
for (const [groupName, group] of Object.entries(config.groups)){
|
|
1378
|
+
if (group.settings) {
|
|
1379
|
+
for (const ref of group.settings)if (!definedSettings.has(ref)) errors.push(`group '${groupName}': unknown settings group '${ref}'`);
|
|
1380
|
+
}
|
|
1381
|
+
if (group.rulesets) {
|
|
1382
|
+
for (const ref of group.rulesets)if (!definedRulesets.has(ref)) errors.push(`group '${groupName}': unknown ruleset '${ref}'`);
|
|
1383
|
+
}
|
|
1384
|
+
if (group.environments) {
|
|
1385
|
+
for (const ref of group.environments)if (!definedEnvironments.has(ref)) errors.push(`group '${groupName}': unknown environment '${ref}'`);
|
|
1386
|
+
}
|
|
1387
|
+
if (group.secrets) {
|
|
1388
|
+
if (group.secrets.actions) {
|
|
1389
|
+
for (const ref of group.secrets.actions)if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
|
|
1390
|
+
}
|
|
1391
|
+
if (group.secrets.dependabot) {
|
|
1392
|
+
for (const ref of group.secrets.dependabot)if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
|
|
1393
|
+
}
|
|
1394
|
+
if (group.secrets.codespaces) {
|
|
1395
|
+
for (const ref of group.secrets.codespaces)if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': unknown secrets group '${ref}'`);
|
|
1396
|
+
}
|
|
1397
|
+
if (group.secrets.environments) for (const [envName, secretGroups] of Object.entries(group.secrets.environments)){
|
|
1398
|
+
if (!definedEnvironments.has(envName)) errors.push(`group '${groupName}': unknown environment '${envName}' in secrets.environments`);
|
|
1399
|
+
for (const ref of secretGroups)if (!definedSecrets.has(ref)) errors.push(`group '${groupName}': in secrets.environments.'${envName}': unknown secrets group '${ref}'`);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (group.variables) {
|
|
1403
|
+
if (group.variables.actions) {
|
|
1404
|
+
for (const ref of group.variables.actions)if (!definedVariables.has(ref)) errors.push(`group '${groupName}': unknown variables group '${ref}'`);
|
|
1405
|
+
}
|
|
1406
|
+
if (group.variables.environments) for (const [envName, varGroups] of Object.entries(group.variables.environments)){
|
|
1407
|
+
if (!definedEnvironments.has(envName)) errors.push(`group '${groupName}': unknown environment '${envName}' in variables.environments`);
|
|
1408
|
+
for (const ref of varGroups)if (!definedVariables.has(ref)) errors.push(`group '${groupName}': in variables.environments.'${envName}': unknown variables group '${ref}'`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1392
1411
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
strategy: FirstMatch,
|
|
1399
|
-
resolvers: [
|
|
1400
|
-
UpwardWalk({
|
|
1401
|
-
filename: CREDENTIALS_FILENAME
|
|
1402
|
-
}),
|
|
1403
|
-
XdgConfig({
|
|
1404
|
-
filename: CREDENTIALS_FILENAME
|
|
1405
|
-
})
|
|
1406
|
-
],
|
|
1407
|
-
defaultPath: XdgSavePath(CREDENTIALS_FILENAME)
|
|
1408
|
-
}).pipe(Layer.provide(xdgLayer));
|
|
1409
|
-
const ConfigFilesLive = Layer.mergeAll(xdgLayer, credentialsLayer);
|
|
1410
|
-
function resolveConfigFlag(configFlag) {
|
|
1411
|
-
if (Option.isNone(configFlag)) return;
|
|
1412
|
-
const flag = configFlag.value;
|
|
1413
|
-
if (existsSync(flag) && statSync(flag).isDirectory()) return join(flag, CONFIG_FILENAME);
|
|
1414
|
-
return flag;
|
|
1412
|
+
if (errors.length > 0) return Effect.fail(new ConfigError({
|
|
1413
|
+
operation: "validate",
|
|
1414
|
+
reason: errors.join("\n")
|
|
1415
|
+
}));
|
|
1416
|
+
return Effect.succeed(config);
|
|
1415
1417
|
}
|
|
1416
|
-
function
|
|
1417
|
-
const
|
|
1418
|
-
if (
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
if (0 === sources.length) return Effect.fail(new ConfigError({
|
|
1424
|
-
operation: "discover",
|
|
1425
|
-
reason: "No config file found"
|
|
1418
|
+
function makeConfigFilesLive(configFlag) {
|
|
1419
|
+
const configResolvers = [];
|
|
1420
|
+
if (Option.isSome(configFlag)) {
|
|
1421
|
+
const flag = configFlag.value;
|
|
1422
|
+
if (existsSync(flag) && statSync(flag).isDirectory()) configResolvers.push(StaticDir({
|
|
1423
|
+
dir: flag,
|
|
1424
|
+
filename: CONFIG_FILENAME
|
|
1426
1425
|
}));
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1426
|
+
else configResolvers.push(ExplicitPath(flag));
|
|
1427
|
+
}
|
|
1428
|
+
configResolvers.push(UpwardWalk({
|
|
1429
|
+
filename: CONFIG_FILENAME
|
|
1430
|
+
}), XdgConfigResolver({
|
|
1431
|
+
filename: CONFIG_FILENAME
|
|
1432
|
+
}));
|
|
1433
|
+
return XdgConfigLive.multi({
|
|
1434
|
+
app: new AppDirsConfig({
|
|
1435
|
+
namespace: "reposets"
|
|
1436
|
+
}),
|
|
1437
|
+
configs: [
|
|
1438
|
+
{
|
|
1439
|
+
tag: ReposetsConfigFile,
|
|
1440
|
+
schema: ConfigSchema,
|
|
1441
|
+
codec: TomlCodec,
|
|
1442
|
+
strategy: FirstMatch,
|
|
1443
|
+
resolvers: configResolvers,
|
|
1444
|
+
validate: validateConfigRefs
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
tag: ReposetsCredentialsFile,
|
|
1448
|
+
schema: CredentialsSchema,
|
|
1449
|
+
codec: TomlCodec,
|
|
1450
|
+
strategy: FirstMatch,
|
|
1451
|
+
resolvers: [
|
|
1452
|
+
UpwardWalk({
|
|
1453
|
+
filename: CREDENTIALS_FILENAME
|
|
1454
|
+
}),
|
|
1455
|
+
XdgConfigResolver({
|
|
1456
|
+
filename: CREDENTIALS_FILENAME
|
|
1457
|
+
})
|
|
1458
|
+
],
|
|
1459
|
+
defaultPath: XdgSavePath(CREDENTIALS_FILENAME)
|
|
1460
|
+
}
|
|
1461
|
+
]
|
|
1431
1462
|
});
|
|
1432
1463
|
}
|
|
1464
|
+
const ConfigFilesLive = makeConfigFilesLive(Option.none());
|
|
1433
1465
|
class OnePasswordClient extends Context.Tag("OnePasswordClient")() {
|
|
1434
1466
|
}
|
|
1435
1467
|
const OnePasswordClientLive = Layer.succeed(OnePasswordClient, {
|
|
@@ -2657,4 +2689,4 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
|
|
|
2657
2689
|
}
|
|
2658
2690
|
};
|
|
2659
2691
|
}));
|
|
2660
|
-
export { BypassActorSchema, CleanupSchema, CleanupScopeSchema, ConfigFilesLive, ConfigSchema, CredentialProfileSchema, CredentialResolver, CredentialResolverLive, CredentialsSchema, GitHubApiError, GitHubClient, GitHubClientLive, GitHubClientTest, GroupSchema, LogLevelSchema, OnePasswordClient, OnePasswordClientLive, OnePasswordClientTest, OnePasswordError, ReposetsConfigFile, ReposetsCredentialsFile, ResolveError, ResolveSectionSchema, ResolvedRefSchema, RulesetSchema, SecretGroupSchema, SyncEngine, SyncEngineLive, SyncError, SyncLogger, SyncLoggerLive, VariableGroupSchema, buildRulesetPayload, encryptSecret,
|
|
2692
|
+
export { BypassActorSchema, CONFIG_FILENAME, CREDENTIALS_FILENAME, CleanupSchema, CleanupScopeSchema, ConfigFilesLive, ConfigSchema, CredentialProfileSchema, CredentialResolver, CredentialResolverLive, CredentialsSchema, GitHubApiError, GitHubClient, GitHubClientLive, GitHubClientTest, GroupSchema, LogLevelSchema, OnePasswordClient, OnePasswordClientLive, OnePasswordClientTest, OnePasswordError, ReposetsConfigFile, ReposetsCredentialsFile, ResolveError, ResolveSectionSchema, ResolvedRefSchema, RulesetSchema, SecretGroupSchema, SyncEngine, SyncEngineLive, SyncError, SyncLogger, SyncLoggerLive, VariableGroupSchema, buildRulesetPayload, encryptSecret, makeConfigFilesLive, validateConfigRefs };
|
package/bin/reposets.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, Options } from "@effect/cli";
|
|
3
3
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
4
|
-
import {
|
|
4
|
+
import { Effect, Layer, LogLevel, Logger, Option } from "effect";
|
|
5
5
|
import { AppDirs } from "xdg-effect";
|
|
6
6
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
7
|
import { parse } from "smol-toml";
|
|
9
|
-
import {
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { CredentialResolverLive, SyncEngineLive, ReposetsCredentialsFile, SyncLoggerLive, OnePasswordClientLive, GitHubClientLive, SyncEngine, makeConfigFilesLive, ReposetsConfigFile } from "../500.js";
|
|
10
10
|
const EMPTY_CREDENTIALS = {
|
|
11
11
|
profiles: {}
|
|
12
12
|
};
|
|
@@ -26,11 +26,11 @@ const createCommand = Command.make("create", {
|
|
|
26
26
|
yield* appDirs.ensureConfig;
|
|
27
27
|
const credentialsFile = yield* ReposetsCredentialsFile;
|
|
28
28
|
const creds = yield* credentialsFile.loadOrDefault(EMPTY_CREDENTIALS);
|
|
29
|
-
if (creds.profiles[profile]) return void (yield*
|
|
29
|
+
if (creds.profiles[profile]) return void (yield* Effect.logError(`Profile '${profile}' already exists. Delete it first.`));
|
|
30
30
|
const newProfile = {};
|
|
31
31
|
if ("Some" === githubToken._tag) newProfile.github_token = githubToken.value;
|
|
32
32
|
if ("Some" === opToken._tag) newProfile.op_service_account_token = opToken.value;
|
|
33
|
-
if (!newProfile.github_token && !newProfile.op_service_account_token) return void (yield*
|
|
33
|
+
if (!newProfile.github_token && !newProfile.op_service_account_token) return void (yield* Effect.logError("Provide at least --github-token or --op-token."));
|
|
34
34
|
yield* credentialsFile.update((current)=>({
|
|
35
35
|
profiles: {
|
|
36
36
|
...current.profiles,
|
|
@@ -40,19 +40,19 @@ const createCommand = Command.make("create", {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}), EMPTY_CREDENTIALS);
|
|
43
|
-
yield*
|
|
44
|
-
})).pipe(Command.withDescription("Add a credential profile"));
|
|
43
|
+
yield* Effect.log(`Created profile '${profile}'.`);
|
|
44
|
+
}).pipe(Effect.provide(makeConfigFilesLive(Option.none())))).pipe(Command.withDescription("Add a credential profile"));
|
|
45
45
|
const listCredsCommand = Command.make("list", {}, ()=>Effect.gen(function*() {
|
|
46
46
|
const credentialsFile = yield* ReposetsCredentialsFile;
|
|
47
47
|
const creds = yield* credentialsFile.loadOrDefault(EMPTY_CREDENTIALS);
|
|
48
|
-
if (0 === Object.keys(creds.profiles).length) return void (yield*
|
|
48
|
+
if (0 === Object.keys(creds.profiles).length) return void (yield* Effect.log("No credential profiles configured."));
|
|
49
49
|
for (const [name, profile] of Object.entries(creds.profiles)){
|
|
50
|
-
yield*
|
|
51
|
-
if (profile.github_token) yield*
|
|
52
|
-
if (profile.op_service_account_token) yield*
|
|
53
|
-
yield*
|
|
50
|
+
yield* Effect.log(`[${name}]`);
|
|
51
|
+
if (profile.github_token) yield* Effect.log(` github_token: ${redactToken(profile.github_token)}`);
|
|
52
|
+
if (profile.op_service_account_token) yield* Effect.log(` op_service_account_token: ${redactToken(profile.op_service_account_token)}`);
|
|
53
|
+
yield* Effect.log("");
|
|
54
54
|
}
|
|
55
|
-
})).pipe(Command.withDescription("List profiles (tokens redacted)"));
|
|
55
|
+
}).pipe(Effect.provide(makeConfigFilesLive(Option.none())))).pipe(Command.withDescription("List profiles (tokens redacted)"));
|
|
56
56
|
const deleteCommand = Command.make("delete", {
|
|
57
57
|
profile: profileOption
|
|
58
58
|
}, ({ profile })=>Effect.gen(function*() {
|
|
@@ -60,13 +60,13 @@ const deleteCommand = Command.make("delete", {
|
|
|
60
60
|
yield* appDirs.ensureConfig;
|
|
61
61
|
const credentialsFile = yield* ReposetsCredentialsFile;
|
|
62
62
|
const creds = yield* credentialsFile.loadOrDefault(EMPTY_CREDENTIALS);
|
|
63
|
-
if (!creds.profiles[profile]) return void (yield*
|
|
63
|
+
if (!creds.profiles[profile]) return void (yield* Effect.logError(`Profile '${profile}' not found.`));
|
|
64
64
|
const { [profile]: _, ...remainingProfiles } = creds.profiles;
|
|
65
65
|
yield* credentialsFile.save({
|
|
66
66
|
profiles: remainingProfiles
|
|
67
67
|
});
|
|
68
|
-
yield*
|
|
69
|
-
})).pipe(Command.withDescription("Remove a profile"));
|
|
68
|
+
yield* Effect.log(`Deleted profile '${profile}'.`);
|
|
69
|
+
}).pipe(Effect.provide(makeConfigFilesLive(Option.none())))).pipe(Command.withDescription("Remove a profile"));
|
|
70
70
|
const credentialsCommand = Command.make("credentials").pipe(Command.withDescription("Manage credential profiles"), Command.withSubcommands([
|
|
71
71
|
createCommand,
|
|
72
72
|
listCredsCommand,
|
|
@@ -138,23 +138,24 @@ const doctorCommand = Command.make("doctor", {
|
|
|
138
138
|
config: configOption
|
|
139
139
|
}, ({ config })=>Effect.gen(function*() {
|
|
140
140
|
const configFile = yield* ReposetsConfigFile;
|
|
141
|
-
const discoverResult = yield* Effect.either(
|
|
142
|
-
if ("Left" === discoverResult._tag) return void (yield*
|
|
143
|
-
const
|
|
144
|
-
|
|
141
|
+
const discoverResult = yield* Effect.either(configFile.discover);
|
|
142
|
+
if ("Left" === discoverResult._tag) return void (yield* Effect.logError("No config found. Run 'reposets init' to create one."));
|
|
143
|
+
const sources = discoverResult.right;
|
|
144
|
+
if (0 === sources.length) return void (yield* Effect.logError("No config found. Run 'reposets init' to create one."));
|
|
145
|
+
const configPath = sources[0].path;
|
|
145
146
|
let raw;
|
|
146
147
|
try {
|
|
147
148
|
const configToml = readFileSync(configPath, "utf-8");
|
|
148
149
|
raw = parse(configToml);
|
|
149
150
|
} catch (err) {
|
|
150
|
-
yield*
|
|
151
|
+
yield* Effect.logError(`TOML parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
151
152
|
return;
|
|
152
153
|
}
|
|
153
154
|
let warnings = 0;
|
|
154
155
|
for (const key of Object.keys(raw))if (!KNOWN_CONFIG_KEYS.has(key)) {
|
|
155
156
|
const suggestion = findClosestMatch(key, KNOWN_CONFIG_KEYS);
|
|
156
157
|
const hint = suggestion ? ` -- did you mean '${suggestion}'?` : "";
|
|
157
|
-
yield*
|
|
158
|
+
yield* Effect.log(`Warning: unknown top-level key '${key}'${hint}`);
|
|
158
159
|
warnings++;
|
|
159
160
|
}
|
|
160
161
|
const groups = raw.groups;
|
|
@@ -163,7 +164,7 @@ const doctorCommand = Command.make("doctor", {
|
|
|
163
164
|
for (const key of Object.keys(group))if (!KNOWN_GROUP_KEYS.has(key)) {
|
|
164
165
|
const suggestion = findClosestMatch(key, KNOWN_GROUP_KEYS);
|
|
165
166
|
const hint = suggestion ? ` -- did you mean '${suggestion}'?` : "";
|
|
166
|
-
yield*
|
|
167
|
+
yield* Effect.log(`Warning: unknown key '${key}' in groups.${groupName}${hint}`);
|
|
167
168
|
warnings++;
|
|
168
169
|
}
|
|
169
170
|
}
|
|
@@ -177,7 +178,7 @@ const doctorCommand = Command.make("doctor", {
|
|
|
177
178
|
for (const key of Object.keys(cleanupObj))if (!KNOWN_CLEANUP_KEYS.has(key)) {
|
|
178
179
|
const suggestion = findClosestMatch(key, KNOWN_CLEANUP_KEYS);
|
|
179
180
|
const hint = suggestion ? ` -- did you mean '${suggestion}'?` : "";
|
|
180
|
-
yield*
|
|
181
|
+
yield* Effect.log(`Warning: unknown key '${key}' in ${prefix}${hint}`);
|
|
181
182
|
warnings++;
|
|
182
183
|
}
|
|
183
184
|
const secrets = cleanupObj.secrets;
|
|
@@ -185,7 +186,7 @@ const doctorCommand = Command.make("doctor", {
|
|
|
185
186
|
for (const key of Object.keys(secrets))if (!KNOWN_CLEANUP_SECRETS_KEYS.has(key)) {
|
|
186
187
|
const suggestion = findClosestMatch(key, KNOWN_CLEANUP_SECRETS_KEYS);
|
|
187
188
|
const hint = suggestion ? ` -- did you mean '${suggestion}'?` : "";
|
|
188
|
-
yield*
|
|
189
|
+
yield* Effect.log(`Warning: unknown key '${key}' in ${prefix}.secrets${hint}`);
|
|
189
190
|
warnings++;
|
|
190
191
|
}
|
|
191
192
|
}
|
|
@@ -194,21 +195,21 @@ const doctorCommand = Command.make("doctor", {
|
|
|
194
195
|
for (const key of Object.keys(variables))if (!KNOWN_CLEANUP_VARIABLES_KEYS.has(key)) {
|
|
195
196
|
const suggestion = findClosestMatch(key, KNOWN_CLEANUP_VARIABLES_KEYS);
|
|
196
197
|
const hint = suggestion ? ` -- did you mean '${suggestion}'?` : "";
|
|
197
|
-
yield*
|
|
198
|
+
yield* Effect.log(`Warning: unknown key '${key}' in ${prefix}.variables${hint}`);
|
|
198
199
|
warnings++;
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
|
-
yield*
|
|
203
|
-
yield*
|
|
204
|
-
yield*
|
|
205
|
-
yield*
|
|
206
|
-
yield*
|
|
207
|
-
yield*
|
|
208
|
-
yield*
|
|
209
|
-
if (0 === warnings) yield*
|
|
210
|
-
else yield*
|
|
211
|
-
})).pipe(Command.withDescription("Deep config diagnostics with typo detection"));
|
|
203
|
+
yield* Effect.log("Schema validation: passed");
|
|
204
|
+
yield* Effect.log("\nRequired fine-grained token permissions:");
|
|
205
|
+
yield* Effect.log(" Repository permissions > Administration (Read and write) -- settings sync");
|
|
206
|
+
yield* Effect.log(" Repository permissions > Secrets (Read and write) -- Actions secrets");
|
|
207
|
+
yield* Effect.log(" Repository permissions > Variables (Read and write) -- Actions variables");
|
|
208
|
+
yield* Effect.log(" Repository permissions > Environments (Read and write) -- environment sync");
|
|
209
|
+
yield* Effect.log(" Account permissions > GPG keys (Read and write) -- secrets encryption key");
|
|
210
|
+
if (0 === warnings) yield* Effect.log("\nNo unknown keys detected.");
|
|
211
|
+
else yield* Effect.log(`\n${warnings} warning(s) found.`);
|
|
212
|
+
}).pipe(Effect.provide(makeConfigFilesLive(config)))).pipe(Command.withDescription("Deep config diagnostics with typo detection"));
|
|
212
213
|
const projectOption = Options.boolean("project").pipe(Options.withDescription("Create config in current directory instead of XDG/home location"), Options.withDefault(false));
|
|
213
214
|
const CONFIG_TEMPLATE = `# reposets configuration
|
|
214
215
|
# See: https://github.com/spencerbeggs/reposets
|
|
@@ -289,15 +290,15 @@ const initCommand = Command.make("init", {
|
|
|
289
290
|
});
|
|
290
291
|
const configPath = join(targetDir, CONFIG_FILE);
|
|
291
292
|
const credsPath = join(targetDir, CREDENTIALS_FILE);
|
|
292
|
-
if (existsSync(configPath)) yield*
|
|
293
|
+
if (existsSync(configPath)) yield* Effect.log(`Config already exists: ${configPath}`);
|
|
293
294
|
else {
|
|
294
295
|
writeFileSync(configPath, CONFIG_TEMPLATE);
|
|
295
|
-
yield*
|
|
296
|
+
yield* Effect.log(`Created: ${configPath}`);
|
|
296
297
|
}
|
|
297
|
-
if (existsSync(credsPath)) yield*
|
|
298
|
+
if (existsSync(credsPath)) yield* Effect.log(`Credentials already exists: ${credsPath}`);
|
|
298
299
|
else {
|
|
299
300
|
writeFileSync(credsPath, CREDENTIALS_TEMPLATE);
|
|
300
|
-
yield*
|
|
301
|
+
yield* Effect.log(`Created: ${credsPath}`);
|
|
301
302
|
}
|
|
302
303
|
if (project) {
|
|
303
304
|
const gitignorePath = join(targetDir, ".gitignore");
|
|
@@ -305,35 +306,37 @@ const initCommand = Command.make("init", {
|
|
|
305
306
|
const content = readFileSync(gitignorePath, "utf-8");
|
|
306
307
|
if (!content.includes(CREDENTIALS_FILE)) {
|
|
307
308
|
appendFileSync(gitignorePath, `\n${CREDENTIALS_FILE}\n`);
|
|
308
|
-
yield*
|
|
309
|
+
yield* Effect.log(`Added ${CREDENTIALS_FILE} to .gitignore`);
|
|
309
310
|
}
|
|
310
311
|
} else {
|
|
311
312
|
writeFileSync(gitignorePath, `${CREDENTIALS_FILE}\n`);
|
|
312
|
-
yield*
|
|
313
|
+
yield* Effect.log(`Created .gitignore with ${CREDENTIALS_FILE}`);
|
|
313
314
|
}
|
|
314
315
|
} else {
|
|
315
316
|
const gitignorePath = join(targetDir, ".gitignore");
|
|
316
317
|
if (!existsSync(gitignorePath)) {
|
|
317
318
|
writeFileSync(gitignorePath, `${CREDENTIALS_FILE}\n`);
|
|
318
|
-
yield*
|
|
319
|
+
yield* Effect.log(`Created .gitignore in ${targetDir}`);
|
|
319
320
|
}
|
|
320
321
|
}
|
|
321
|
-
yield*
|
|
322
|
-
})).pipe(Command.withDescription("Scaffold config files"));
|
|
322
|
+
yield* Effect.log("\nDone! Edit your config and credentials files to get started.");
|
|
323
|
+
}).pipe(Effect.provide(makeConfigFilesLive(Option.none())))).pipe(Command.withDescription("Scaffold config files"));
|
|
323
324
|
const list_configOption = Options.file("config").pipe(Options.withDescription("Path to config directory or reposets.config.toml file"), Options.optional);
|
|
324
325
|
const listCommand = Command.make("list", {
|
|
325
326
|
config: list_configOption
|
|
326
327
|
}, ({ config })=>Effect.gen(function*() {
|
|
327
328
|
const configFile = yield* ReposetsConfigFile;
|
|
328
|
-
const
|
|
329
|
+
const sources = yield* configFile.discover;
|
|
330
|
+
if (0 === sources.length) return void (yield* Effect.logError("No config file found."));
|
|
331
|
+
const parsedConfig = sources[0].value;
|
|
329
332
|
const defaultOwner = parsedConfig.owner ?? "(not set)";
|
|
330
|
-
yield*
|
|
333
|
+
yield* Effect.log(`Default owner: ${defaultOwner}\n`);
|
|
331
334
|
for (const [groupName, group] of Object.entries(parsedConfig.groups)){
|
|
332
335
|
const owner = group.owner ?? parsedConfig.owner ?? "(not set)";
|
|
333
|
-
yield*
|
|
334
|
-
for (const repo of group.repos)yield*
|
|
335
|
-
if (group.settings?.length) yield*
|
|
336
|
-
if (group.environments?.length) yield*
|
|
336
|
+
yield* Effect.log(`[${groupName}] (owner: ${owner})`);
|
|
337
|
+
for (const repo of group.repos)yield* Effect.log(` - ${owner}/${repo}`);
|
|
338
|
+
if (group.settings?.length) yield* Effect.log(` settings: ${group.settings.join(", ")}`);
|
|
339
|
+
if (group.environments?.length) yield* Effect.log(` environments: ${group.environments.join(", ")}`);
|
|
337
340
|
if (group.secrets) {
|
|
338
341
|
const parts = [];
|
|
339
342
|
if (group.secrets.actions?.length) parts.push(`actions:[${group.secrets.actions.join(",")}]`);
|
|
@@ -342,7 +345,7 @@ const listCommand = Command.make("list", {
|
|
|
342
345
|
if (group.secrets.environments) {
|
|
343
346
|
for (const [envName, envGroups] of Object.entries(group.secrets.environments))if (envGroups.length) parts.push(`environments.${envName}:[${envGroups.join(",")}]`);
|
|
344
347
|
}
|
|
345
|
-
if (parts.length) yield*
|
|
348
|
+
if (parts.length) yield* Effect.log(` secrets: ${parts.join(", ")}`);
|
|
346
349
|
}
|
|
347
350
|
if (group.variables) {
|
|
348
351
|
const parts = [];
|
|
@@ -350,13 +353,13 @@ const listCommand = Command.make("list", {
|
|
|
350
353
|
if (group.variables.environments) {
|
|
351
354
|
for (const [envName, envGroups] of Object.entries(group.variables.environments))if (envGroups.length) parts.push(`environments.${envName}:[${envGroups.join(",")}]`);
|
|
352
355
|
}
|
|
353
|
-
if (parts.length) yield*
|
|
356
|
+
if (parts.length) yield* Effect.log(` variables: ${parts.join(", ")}`);
|
|
354
357
|
}
|
|
355
|
-
if (group.rulesets?.length) yield*
|
|
356
|
-
if (group.credentials) yield*
|
|
357
|
-
yield*
|
|
358
|
+
if (group.rulesets?.length) yield* Effect.log(` rulesets: ${group.rulesets.join(", ")}`);
|
|
359
|
+
if (group.credentials) yield* Effect.log(` credentials: ${group.credentials}`);
|
|
360
|
+
yield* Effect.log("");
|
|
358
361
|
}
|
|
359
|
-
})).pipe(Command.withDescription("Show config summary"));
|
|
362
|
+
}).pipe(Effect.provide(makeConfigFilesLive(config)))).pipe(Command.withDescription("Show config summary"));
|
|
360
363
|
const sync_configOption = Options.file("config").pipe(Options.withDescription("Path to config directory or reposets.config.toml file"), Options.optional);
|
|
361
364
|
const groupOption = Options.text("group").pipe(Options.withDescription("Sync only a specific repo group"), Options.optional);
|
|
362
365
|
const repoOption = Options.text("repo").pipe(Options.withDescription("Sync only a specific repo"), Options.optional);
|
|
@@ -377,7 +380,10 @@ const syncCommand = Command.make("sync", {
|
|
|
377
380
|
logLevel: logLevelOption
|
|
378
381
|
}, ({ config, group, repo, dryRun, noCleanup, logLevel: logLevelFlag })=>Effect.gen(function*() {
|
|
379
382
|
const configFile = yield* ReposetsConfigFile;
|
|
380
|
-
const
|
|
383
|
+
const sources = yield* configFile.discover;
|
|
384
|
+
if (0 === sources.length) return void (yield* Effect.logError("No config file found."));
|
|
385
|
+
const parsedConfig = sources[0].value;
|
|
386
|
+
const configDir = dirname(sources[0].path);
|
|
381
387
|
const credentialsFile = yield* ReposetsCredentialsFile;
|
|
382
388
|
const credentials = yield* credentialsFile.loadOrDefault({
|
|
383
389
|
profiles: {}
|
|
@@ -385,7 +391,7 @@ const syncCommand = Command.make("sync", {
|
|
|
385
391
|
const profileNames = Object.keys(credentials.profiles);
|
|
386
392
|
const defaultProfile = 1 === profileNames.length ? profileNames[0] : void 0;
|
|
387
393
|
const token = defaultProfile ? credentials.profiles[defaultProfile]?.github_token : void 0;
|
|
388
|
-
if (!token) return void (yield*
|
|
394
|
+
if (!token) return void (yield* Effect.logError("No GitHub token found. Run 'reposets credentials create' first."));
|
|
389
395
|
const logLevel = "Some" === logLevelFlag._tag ? logLevelFlag.value : parsedConfig.log_level;
|
|
390
396
|
const githubLayer = GitHubClientLive(token);
|
|
391
397
|
const opLayer = OnePasswordClientLive;
|
|
@@ -397,7 +403,7 @@ const syncCommand = Command.make("sync", {
|
|
|
397
403
|
const engineLayer = Layer.provideMerge(SyncEngineLive, Layer.merge(Layer.merge(githubLayer, resolverLayer), loggerLayer));
|
|
398
404
|
const groupFilter = "Some" === group._tag ? group.value : void 0;
|
|
399
405
|
const repoFilter = "Some" === repo._tag ? repo.value : void 0;
|
|
400
|
-
if (dryRun && "silent" !== logLevel) yield*
|
|
406
|
+
if (dryRun && "silent" !== logLevel) yield* Effect.log("DRY RUN \u2014 no changes will be made\n");
|
|
401
407
|
yield* Effect.provide(Effect.gen(function*() {
|
|
402
408
|
const engine = yield* SyncEngine;
|
|
403
409
|
yield* engine.syncAll(parsedConfig, credentials, {
|
|
@@ -408,82 +414,51 @@ const syncCommand = Command.make("sync", {
|
|
|
408
414
|
configDir
|
|
409
415
|
});
|
|
410
416
|
}), engineLayer);
|
|
411
|
-
})).pipe(Command.withDescription("Sync repos with GitHub"));
|
|
417
|
+
}).pipe(Effect.provide(makeConfigFilesLive(config)))).pipe(Command.withDescription("Sync repos with GitHub"));
|
|
412
418
|
const validate_configOption = Options.file("config").pipe(Options.withDescription("Path to config directory or reposets.config.toml file"), Options.optional);
|
|
413
419
|
const validateCommand = Command.make("validate", {
|
|
414
420
|
config: validate_configOption
|
|
415
421
|
}, ({ config })=>Effect.gen(function*() {
|
|
416
422
|
const configFile = yield* ReposetsConfigFile;
|
|
417
423
|
let hasErrors = false;
|
|
418
|
-
const configResult = yield* Effect.either(
|
|
419
|
-
if ("Left" === configResult._tag) return void (yield*
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
hasErrors = true;
|
|
426
|
-
}
|
|
427
|
-
const allSecretRefs = [
|
|
428
|
-
...group.secrets?.actions ?? [],
|
|
429
|
-
...group.secrets?.dependabot ?? [],
|
|
430
|
-
...group.secrets?.codespaces ?? []
|
|
431
|
-
];
|
|
432
|
-
for (const ref of allSecretRefs)if (!parsedConfig.secrets?.[ref]) {
|
|
433
|
-
yield* Console.error(`Group '${groupName}': references unknown secrets group '${ref}'`);
|
|
434
|
-
hasErrors = true;
|
|
435
|
-
}
|
|
436
|
-
for (const ref of group.variables?.actions ?? [])if (!parsedConfig.variables?.[ref]) {
|
|
437
|
-
yield* Console.error(`Group '${groupName}': references unknown variables group '${ref}'`);
|
|
438
|
-
hasErrors = true;
|
|
439
|
-
}
|
|
440
|
-
for (const ref of group.rulesets ?? [])if (!parsedConfig.rulesets?.[ref]) {
|
|
441
|
-
yield* Console.error(`Group '${groupName}': references unknown ruleset '${ref}'`);
|
|
442
|
-
hasErrors = true;
|
|
443
|
-
}
|
|
444
|
-
for (const ref of group.environments ?? [])if (!parsedConfig.environments?.[ref]) {
|
|
445
|
-
yield* Console.error(`Group '${groupName}': references unknown environment '${ref}'`);
|
|
446
|
-
hasErrors = true;
|
|
447
|
-
}
|
|
448
|
-
if (group.secrets?.environments) {
|
|
449
|
-
for (const [envName, groupRefs] of Object.entries(group.secrets.environments))for (const ref of groupRefs)if (!parsedConfig.secrets?.[ref]) {
|
|
450
|
-
yield* Console.error(`Group '${groupName}': secrets.environments.${envName} references unknown secrets group '${ref}'`);
|
|
451
|
-
hasErrors = true;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
if (group.variables?.environments) {
|
|
455
|
-
for (const [envName, groupRefs] of Object.entries(group.variables.environments))for (const ref of groupRefs)if (!parsedConfig.variables?.[ref]) {
|
|
456
|
-
yield* Console.error(`Group '${groupName}': variables.environments.${envName} references unknown variables group '${ref}'`);
|
|
457
|
-
hasErrors = true;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
424
|
+
const configResult = yield* Effect.either(configFile.discover);
|
|
425
|
+
if ("Left" === configResult._tag) return void (yield* Effect.logError(`Config validation failed: ${configResult.left.message}`));
|
|
426
|
+
const sources = configResult.right;
|
|
427
|
+
if (0 === sources.length) return void (yield* Effect.logError("No config file found."));
|
|
428
|
+
yield* Effect.log("Config schema: valid");
|
|
429
|
+
const parsedConfig = sources[0].value;
|
|
430
|
+
const configDir = dirname(sources[0].path);
|
|
461
431
|
for (const [groupName, group] of Object.entries(parsedConfig.secrets))if ("file" in group) for (const [entryName, filePath] of Object.entries(group.file)){
|
|
462
432
|
const fullPath = join(configDir, filePath);
|
|
463
433
|
if (!existsSync(fullPath)) {
|
|
464
|
-
yield*
|
|
434
|
+
yield* Effect.logError(`secrets.${groupName}.file.${entryName}: file not found: ${fullPath}`);
|
|
465
435
|
hasErrors = true;
|
|
466
436
|
}
|
|
467
437
|
}
|
|
468
438
|
for (const [groupName, group] of Object.entries(parsedConfig.variables))if ("file" in group) for (const [entryName, filePath] of Object.entries(group.file)){
|
|
469
439
|
const fullPath = join(configDir, filePath);
|
|
470
440
|
if (!existsSync(fullPath)) {
|
|
471
|
-
yield*
|
|
441
|
+
yield* Effect.logError(`variables.${groupName}.file.${entryName}: file not found: ${fullPath}`);
|
|
472
442
|
hasErrors = true;
|
|
473
443
|
}
|
|
474
444
|
}
|
|
475
445
|
const credentialsFile = yield* ReposetsCredentialsFile;
|
|
476
446
|
const credsResult = yield* Effect.either(credentialsFile.load);
|
|
477
|
-
if ("Left" === credsResult._tag) yield*
|
|
447
|
+
if ("Left" === credsResult._tag) yield* Effect.log("Credentials file: not found (optional)");
|
|
478
448
|
else {
|
|
479
|
-
yield*
|
|
449
|
+
yield* Effect.log("Credentials schema: valid");
|
|
480
450
|
for (const [groupName, group] of Object.entries(parsedConfig.groups))if (group.credentials && !credsResult.right.profiles[group.credentials]) {
|
|
481
|
-
yield*
|
|
451
|
+
yield* Effect.logError(`Group '${groupName}': references unknown credentials profile '${group.credentials}'`);
|
|
482
452
|
hasErrors = true;
|
|
483
453
|
}
|
|
484
454
|
}
|
|
485
|
-
if (!hasErrors) yield*
|
|
486
|
-
})).pipe(Command.withDescription("Validate config without API calls"));
|
|
455
|
+
if (!hasErrors) yield* Effect.log("\nAll checks passed.");
|
|
456
|
+
}).pipe(Effect.provide(makeConfigFilesLive(config)))).pipe(Command.withDescription("Validate config without API calls"));
|
|
457
|
+
const CliLogger = Logger.replace(Logger.defaultLogger, Logger.make(({ logLevel, message })=>{
|
|
458
|
+
const text = "string" == typeof message ? message : String(message);
|
|
459
|
+
if (LogLevel.greaterThanEqual(logLevel, LogLevel.Error)) globalThis.console.error(text);
|
|
460
|
+
else globalThis.console.log(text);
|
|
461
|
+
}));
|
|
487
462
|
const rootCommand = Command.make("reposets").pipe(Command.withSubcommands([
|
|
488
463
|
syncCommand,
|
|
489
464
|
listCommand,
|
|
@@ -496,5 +471,5 @@ const cli = Command.run(rootCommand, {
|
|
|
496
471
|
name: "reposets",
|
|
497
472
|
version: "0.0.0"
|
|
498
473
|
});
|
|
499
|
-
const program = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(
|
|
474
|
+
const program = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(NodeContext.layer), Effect.provide(CliLogger));
|
|
500
475
|
NodeRuntime.runMain(program);
|
package/index.d.ts
CHANGED
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
|
|
10
10
|
import { AppDirs } from 'xdg-effect';
|
|
11
11
|
import { ConfigFile } from 'xdg-effect';
|
|
12
|
-
import
|
|
12
|
+
import { ConfigFileService } from 'xdg-effect';
|
|
13
13
|
import { Context } from 'effect';
|
|
14
14
|
import { Effect } from 'effect';
|
|
15
15
|
import { FileSystem } from '@effect/platform/FileSystem';
|
|
16
|
-
import { Layer } from 'effect';
|
|
16
|
+
import { Layer } from 'effect/Layer';
|
|
17
|
+
import { Layer as Layer_2 } from 'effect';
|
|
17
18
|
import { Option } from 'effect';
|
|
18
19
|
import { Ref } from 'effect';
|
|
19
20
|
import { Schema } from 'effect';
|
|
@@ -112,9 +113,15 @@ export declare const CleanupScopeSchema: Schema.Union<[typeof Schema.Boolean, Sc
|
|
|
112
113
|
|
|
113
114
|
export declare type Config = typeof ConfigSchema.Type;
|
|
114
115
|
|
|
116
|
+
export declare const CONFIG_FILENAME = "reposets.config.toml";
|
|
117
|
+
|
|
115
118
|
export { ConfigFile }
|
|
116
119
|
|
|
117
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Default ConfigFiles layer with no --config flag override.
|
|
122
|
+
* Used by CLI entrypoint when no flag is provided.
|
|
123
|
+
*/
|
|
124
|
+
export declare const ConfigFilesLive: Layer<AppDirs | ConfigFileService<any> | XdgResolver, never, FileSystem>;
|
|
118
125
|
|
|
119
126
|
export declare const ConfigSchema: Schema.Struct<{
|
|
120
127
|
owner: Schema.optional<Schema.SchemaClass<string, string, never>>;
|
|
@@ -478,7 +485,7 @@ export declare class CredentialResolver extends CredentialResolver_base {
|
|
|
478
485
|
|
|
479
486
|
declare const CredentialResolver_base: Context.TagClass<CredentialResolver, "CredentialResolver", CredentialResolverService>;
|
|
480
487
|
|
|
481
|
-
export declare const CredentialResolverLive:
|
|
488
|
+
export declare const CredentialResolverLive: Layer_2.Layer<CredentialResolver, never, OnePasswordClient>;
|
|
482
489
|
|
|
483
490
|
declare interface CredentialResolverService {
|
|
484
491
|
readonly resolveAll: (profile: CredentialProfile, basePath: string) => Effect.Effect<Map<string, string>, ResolveError>;
|
|
@@ -486,6 +493,8 @@ declare interface CredentialResolverService {
|
|
|
486
493
|
|
|
487
494
|
export declare type Credentials = typeof CredentialsSchema.Type;
|
|
488
495
|
|
|
496
|
+
export declare const CREDENTIALS_FILENAME = "reposets.credentials.toml";
|
|
497
|
+
|
|
489
498
|
export declare const CredentialsSchema: Schema.Struct<{
|
|
490
499
|
profiles: Schema.optionalWith<Schema.Record$<typeof Schema.String, Schema.Struct<{
|
|
491
500
|
github_token: Schema.SchemaClass<string, string, never>;
|
|
@@ -527,7 +536,7 @@ export declare class GitHubClient extends GitHubClient_base {
|
|
|
527
536
|
|
|
528
537
|
declare const GitHubClient_base: Context.TagClass<GitHubClient, "GitHubClient", GitHubClientService>;
|
|
529
538
|
|
|
530
|
-
export declare function GitHubClientLive(token: string):
|
|
539
|
+
export declare function GitHubClientLive(token: string): Layer_2.Layer<GitHubClient>;
|
|
531
540
|
|
|
532
541
|
declare interface GitHubClientService {
|
|
533
542
|
readonly getOwnerType: (owner: string) => Effect.Effect<OwnerType, GitHubApiError>;
|
|
@@ -553,7 +562,7 @@ declare interface GitHubClientService {
|
|
|
553
562
|
}
|
|
554
563
|
|
|
555
564
|
export declare function GitHubClientTest(): {
|
|
556
|
-
layer:
|
|
565
|
+
layer: Layer_2.Layer<GitHubClient>;
|
|
557
566
|
calls: () => RecordedCall[];
|
|
558
567
|
};
|
|
559
568
|
|
|
@@ -636,35 +645,31 @@ export declare const GroupSchema: Schema.Struct<{
|
|
|
636
645
|
}>>;
|
|
637
646
|
}>;
|
|
638
647
|
|
|
639
|
-
/**
|
|
640
|
-
* Load config using the ConfigFileService. When a --config flag is provided,
|
|
641
|
-
* uses loadFrom with the resolved path. Otherwise uses discover to get both
|
|
642
|
-
* the config value and the file path (for configDir derivation).
|
|
643
|
-
*
|
|
644
|
-
* Returns config and configDir where configDir is the directory containing
|
|
645
|
-
* the config file.
|
|
646
|
-
*/
|
|
647
|
-
export declare function loadConfigWithDir(configFile: ConfigFileService<Config>, configFlag: Option.Option<string>): Effect.Effect<{
|
|
648
|
-
config: Config;
|
|
649
|
-
configDir: string;
|
|
650
|
-
}, XdgConfigError>;
|
|
651
|
-
|
|
652
648
|
export declare type LogLevel = typeof LogLevelSchema.Type;
|
|
653
649
|
|
|
654
650
|
export declare const LogLevelSchema: Schema.Literal<["silent", "info", "verbose", "debug"]>;
|
|
655
651
|
|
|
652
|
+
/**
|
|
653
|
+
* Creates a live Layer providing both config and credentials file services.
|
|
654
|
+
* When configFlag is Some and points to a directory, prepends StaticDir resolver.
|
|
655
|
+
* When configFlag is Some and points to a file, prepends ExplicitPath resolver.
|
|
656
|
+
* Always includes UpwardWalk + XdgConfigResolver as fallback resolvers.
|
|
657
|
+
* Passes validateConfigRefs as the validate callback on the config spec.
|
|
658
|
+
*/
|
|
659
|
+
export declare function makeConfigFilesLive(configFlag: Option.Option<string>): Layer<AppDirs | ConfigFileService<any> | XdgResolver, never, FileSystem>;
|
|
660
|
+
|
|
656
661
|
export declare class OnePasswordClient extends OnePasswordClient_base {
|
|
657
662
|
}
|
|
658
663
|
|
|
659
664
|
declare const OnePasswordClient_base: Context.TagClass<OnePasswordClient, "OnePasswordClient", OnePasswordClientService>;
|
|
660
665
|
|
|
661
|
-
export declare const OnePasswordClientLive:
|
|
666
|
+
export declare const OnePasswordClientLive: Layer_2.Layer<OnePasswordClient, never, never>;
|
|
662
667
|
|
|
663
668
|
declare interface OnePasswordClientService {
|
|
664
669
|
readonly resolve: (reference: string, serviceAccountToken: string) => Effect.Effect<string, OnePasswordError>;
|
|
665
670
|
}
|
|
666
671
|
|
|
667
|
-
export declare function OnePasswordClientTest(stubs: Record<string, string>):
|
|
672
|
+
export declare function OnePasswordClientTest(stubs: Record<string, string>): Layer_2.Layer<OnePasswordClient>;
|
|
668
673
|
|
|
669
674
|
export declare class OnePasswordError extends OnePasswordError_base<{
|
|
670
675
|
readonly message: string;
|
|
@@ -682,7 +687,7 @@ export declare interface RecordedCall {
|
|
|
682
687
|
args: Record<string, unknown>;
|
|
683
688
|
}
|
|
684
689
|
|
|
685
|
-
export declare const ReposetsConfigFile: Tag<ConfigFileService<{
|
|
690
|
+
export declare const ReposetsConfigFile: Tag<ConfigFileService< {
|
|
686
691
|
readonly owner?: string | undefined;
|
|
687
692
|
readonly log_level: "debug" | "info" | "silent" | "verbose";
|
|
688
693
|
readonly settings: {
|
|
@@ -991,7 +996,7 @@ readonly preserve: readonly string[];
|
|
|
991
996
|
} | undefined;
|
|
992
997
|
};
|
|
993
998
|
};
|
|
994
|
-
}>, ConfigFileService<{
|
|
999
|
+
}>, ConfigFileService< {
|
|
995
1000
|
readonly owner?: string | undefined;
|
|
996
1001
|
readonly log_level: "debug" | "info" | "silent" | "verbose";
|
|
997
1002
|
readonly settings: {
|
|
@@ -1302,7 +1307,7 @@ readonly preserve: readonly string[];
|
|
|
1302
1307
|
};
|
|
1303
1308
|
}>>;
|
|
1304
1309
|
|
|
1305
|
-
export declare const ReposetsCredentialsFile: Tag<ConfigFileService<{
|
|
1310
|
+
export declare const ReposetsCredentialsFile: Tag<ConfigFileService< {
|
|
1306
1311
|
readonly profiles: {
|
|
1307
1312
|
readonly [x: string]: {
|
|
1308
1313
|
readonly github_token: string;
|
|
@@ -1322,7 +1327,7 @@ readonly [x: string]: unknown;
|
|
|
1322
1327
|
} | undefined;
|
|
1323
1328
|
};
|
|
1324
1329
|
};
|
|
1325
|
-
}>, ConfigFileService<{
|
|
1330
|
+
}>, ConfigFileService< {
|
|
1326
1331
|
readonly profiles: {
|
|
1327
1332
|
readonly [x: string]: {
|
|
1328
1333
|
readonly github_token: string;
|
|
@@ -1344,12 +1349,6 @@ readonly [x: string]: unknown;
|
|
|
1344
1349
|
};
|
|
1345
1350
|
}>>;
|
|
1346
1351
|
|
|
1347
|
-
/**
|
|
1348
|
-
* Resolves a --config flag to a file path. If the flag points to a directory,
|
|
1349
|
-
* appends the config filename. If omitted, returns undefined.
|
|
1350
|
-
*/
|
|
1351
|
-
export declare function resolveConfigFlag(configFlag: Option.Option<string>): string | undefined;
|
|
1352
|
-
|
|
1353
1352
|
export declare type ResolvedRef = typeof ResolvedRefSchema.Type;
|
|
1354
1353
|
|
|
1355
1354
|
export declare const ResolvedRefSchema: Schema.Struct<{
|
|
@@ -1613,7 +1612,7 @@ export declare class SyncEngine extends SyncEngine_base {
|
|
|
1613
1612
|
|
|
1614
1613
|
declare const SyncEngine_base: Context.TagClass<SyncEngine, "SyncEngine", SyncEngineService>;
|
|
1615
1614
|
|
|
1616
|
-
export declare const SyncEngineLive:
|
|
1615
|
+
export declare const SyncEngineLive: Layer_2.Layer<SyncEngine, never, CredentialResolver | GitHubClient | SyncLogger>;
|
|
1617
1616
|
|
|
1618
1617
|
declare interface SyncEngineService {
|
|
1619
1618
|
readonly syncAll: (config: Config, credentials: Credentials, options: SyncOptions) => Effect.Effect<void, SyncError>;
|
|
@@ -1639,7 +1638,7 @@ export declare interface SyncLoggerConfig {
|
|
|
1639
1638
|
readonly output?: Ref.Ref<string[]>;
|
|
1640
1639
|
}
|
|
1641
1640
|
|
|
1642
|
-
export declare function SyncLoggerLive(config: SyncLoggerConfig):
|
|
1641
|
+
export declare function SyncLoggerLive(config: SyncLoggerConfig): Layer_2.Layer<SyncLogger>;
|
|
1643
1642
|
|
|
1644
1643
|
declare interface SyncLoggerService {
|
|
1645
1644
|
readonly groupStart: (name: string, repoCount: number) => Effect.Effect<void>;
|
|
@@ -1661,6 +1660,15 @@ declare interface SyncOptions {
|
|
|
1661
1660
|
readonly configDir: string;
|
|
1662
1661
|
}
|
|
1663
1662
|
|
|
1663
|
+
/**
|
|
1664
|
+
* Validates all internal cross-references in a parsed config. Checks that
|
|
1665
|
+
* every group's settings, secrets, variables, rulesets, and environments
|
|
1666
|
+
* references point to defined top-level sections, and that environment-scoped
|
|
1667
|
+
* secret/variable groups reference defined environments. Collects ALL errors
|
|
1668
|
+
* into a single ConfigError.
|
|
1669
|
+
*/
|
|
1670
|
+
export declare function validateConfigRefs(config: Config): Effect.Effect<Config, XdgConfigError>;
|
|
1671
|
+
|
|
1664
1672
|
export declare type VariableGroup = typeof VariableGroupSchema.Type;
|
|
1665
1673
|
|
|
1666
1674
|
export declare const VariableGroupSchema: Schema.Union<[Schema.Struct<{
|
package/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { BypassActorSchema, CleanupSchema, CleanupScopeSchema, ConfigFilesLive, ConfigSchema, CredentialProfileSchema, CredentialResolver, CredentialResolverLive, CredentialsSchema, GitHubApiError, GitHubClient, GitHubClientLive, GitHubClientTest, GroupSchema, LogLevelSchema, OnePasswordClient, OnePasswordClientLive, OnePasswordClientTest, OnePasswordError, ReposetsConfigFile, ReposetsCredentialsFile, ResolveError, ResolveSectionSchema, ResolvedRefSchema, RulesetSchema, SecretGroupSchema, SyncEngine, SyncEngineLive, SyncError, SyncLogger, SyncLoggerLive, VariableGroupSchema, buildRulesetPayload, encryptSecret,
|
|
1
|
+
export { BypassActorSchema, CONFIG_FILENAME, CREDENTIALS_FILENAME, CleanupSchema, CleanupScopeSchema, ConfigFilesLive, ConfigSchema, CredentialProfileSchema, CredentialResolver, CredentialResolverLive, CredentialsSchema, GitHubApiError, GitHubClient, GitHubClientLive, GitHubClientTest, GroupSchema, LogLevelSchema, OnePasswordClient, OnePasswordClientLive, OnePasswordClientTest, OnePasswordError, ReposetsConfigFile, ReposetsCredentialsFile, ResolveError, ResolveSectionSchema, ResolvedRefSchema, RulesetSchema, SecretGroupSchema, SyncEngine, SyncEngineLive, SyncError, SyncLogger, SyncLoggerLive, VariableGroupSchema, buildRulesetPayload, encryptSecret, makeConfigFilesLive, validateConfigRefs } from "./500.js";
|
|
2
2
|
export { AppDirs, ConfigError as XdgConfigError, ConfigFile } from "xdg-effect";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reposets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "CLI tool to sync GitHub repo settings, secrets and rulesets across personal repositories",
|
|
6
6
|
"keywords": [
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"effect": "^3.21.1",
|
|
55
55
|
"smol-toml": ">=1.6.1",
|
|
56
56
|
"tweetnacl": "^1.0.3",
|
|
57
|
-
"xdg-effect": "^0.
|
|
57
|
+
"xdg-effect": "^1.0.0"
|
|
58
58
|
},
|
|
59
59
|
"engines": {
|
|
60
60
|
"node": ">=20.0.0"
|