reposets 0.2.1 → 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 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, XdgConfig, XdgConfigLive, XdgSavePath, taplo, tombi } from "xdg-effect";
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 { dirname, isAbsolute, join, resolve } from "node:path";
6
+ import { isAbsolute, resolve } from "node:path";
7
7
  import { Octokit } from "@octokit/rest";
8
8
  class ResolveError extends Data.TaggedError("ResolveError") {
9
9
  }
@@ -1367,64 +1367,101 @@ const CONFIG_FILENAME = "reposets.config.toml";
1367
1367
  const CREDENTIALS_FILENAME = "reposets.credentials.toml";
1368
1368
  const ReposetsConfigFile = ConfigFile.Tag("reposets/Config");
1369
1369
  const ReposetsCredentialsFile = ConfigFile.Tag("reposets/Credentials");
1370
- const xdgLayer = XdgConfigLive({
1371
- app: new AppDirsConfig({
1372
- namespace: "reposets"
1373
- }),
1374
- config: {
1375
- tag: ReposetsConfigFile,
1376
- schema: ConfigSchema,
1377
- codec: TomlCodec,
1378
- strategy: FirstMatch,
1379
- resolvers: [
1380
- UpwardWalk({
1381
- filename: CONFIG_FILENAME
1382
- }),
1383
- XdgConfig({
1384
- filename: CONFIG_FILENAME
1385
- })
1386
- ]
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
+ }
1387
1411
  }
1388
- });
1389
- const credentialsLayer = ConfigFile.Live({
1390
- tag: ReposetsCredentialsFile,
1391
- schema: CredentialsSchema,
1392
- codec: TomlCodec,
1393
- strategy: FirstMatch,
1394
- resolvers: [
1395
- UpwardWalk({
1396
- filename: CREDENTIALS_FILENAME
1397
- }),
1398
- XdgConfig({
1399
- filename: CREDENTIALS_FILENAME
1400
- })
1401
- ],
1402
- defaultPath: XdgSavePath(CREDENTIALS_FILENAME)
1403
- }).pipe(Layer.provide(xdgLayer));
1404
- const ConfigFilesLive = Layer.mergeAll(xdgLayer, credentialsLayer);
1405
- function resolveConfigFlag(configFlag) {
1406
- if (Option.isNone(configFlag)) return;
1407
- const flag = configFlag.value;
1408
- if (existsSync(flag) && statSync(flag).isDirectory()) return join(flag, CONFIG_FILENAME);
1409
- 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);
1410
1417
  }
1411
- function loadConfigWithDir(configFile, configFlag) {
1412
- const resolved = resolveConfigFlag(configFlag);
1413
- if (resolved) return Effect.map(configFile.loadFrom(resolved), (config)=>({
1414
- config,
1415
- configDir: dirname(resolved)
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
1416
1425
  }));
1417
- return Effect.flatMap(configFile.discover, (sources)=>{
1418
- if (0 === sources.length) return Effect.fail(new ConfigError({
1419
- operation: "discover",
1420
- reason: "No config file found"
1421
- }));
1422
- return Effect.succeed({
1423
- config: sources[0].value,
1424
- configDir: dirname(sources[0].path)
1425
- });
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
+ ]
1426
1462
  });
1427
1463
  }
1464
+ const ConfigFilesLive = makeConfigFilesLive(Option.none());
1428
1465
  class OnePasswordClient extends Context.Tag("OnePasswordClient")() {
1429
1466
  }
1430
1467
  const OnePasswordClientLive = Layer.succeed(OnePasswordClient, {
@@ -2652,4 +2689,4 @@ const SyncEngineLive = Layer.effect(SyncEngine, Effect.gen(function*() {
2652
2689
  }
2653
2690
  };
2654
2691
  }));
2655
- 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, loadConfigWithDir, resolveConfigFlag };
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 { Console, Effect, Layer } from "effect";
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 { CredentialResolverLive, ReposetsCredentialsFile, SyncEngineLive, SyncLoggerLive, OnePasswordClientLive, GitHubClientLive, loadConfigWithDir, SyncEngine, ConfigFilesLive, ReposetsConfigFile } from "../500.js";
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* Console.error(`Profile '${profile}' already exists. Delete it first.`));
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* Console.error("Provide at least --github-token or --op-token."));
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* Console.log(`Created profile '${profile}'.`);
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* Console.log("No credential profiles configured."));
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* Console.log(`[${name}]`);
51
- if (profile.github_token) yield* Console.log(` github_token: ${redactToken(profile.github_token)}`);
52
- if (profile.op_service_account_token) yield* Console.log(` op_service_account_token: ${redactToken(profile.op_service_account_token)}`);
53
- yield* Console.log("");
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* Console.error(`Profile '${profile}' not found.`));
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* Console.log(`Deleted profile '${profile}'.`);
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(loadConfigWithDir(configFile, config));
142
- if ("Left" === discoverResult._tag) return void (yield* Console.error("No config found. Run 'reposets init' to create one."));
143
- const { configDir } = discoverResult.right;
144
- const configPath = join(configDir, "reposets.config.toml");
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* Console.error(`TOML parse error: ${err instanceof Error ? err.message : String(err)}`);
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* Console.log(`Warning: unknown top-level key '${key}'${hint}`);
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* Console.log(`Warning: unknown key '${key}' in groups.${groupName}${hint}`);
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* Console.log(`Warning: unknown key '${key}' in ${prefix}${hint}`);
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* Console.log(`Warning: unknown key '${key}' in ${prefix}.secrets${hint}`);
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* Console.log(`Warning: unknown key '${key}' in ${prefix}.variables${hint}`);
198
+ yield* Effect.log(`Warning: unknown key '${key}' in ${prefix}.variables${hint}`);
198
199
  warnings++;
199
200
  }
200
201
  }
201
202
  }
202
- yield* Console.log("Schema validation: passed");
203
- yield* Console.log("\nRequired fine-grained token permissions:");
204
- yield* Console.log(" Repository permissions > Administration (Read and write) -- settings sync");
205
- yield* Console.log(" Repository permissions > Secrets (Read and write) -- Actions secrets");
206
- yield* Console.log(" Repository permissions > Variables (Read and write) -- Actions variables");
207
- yield* Console.log(" Repository permissions > Environments (Read and write) -- environment sync");
208
- yield* Console.log(" Account permissions > GPG keys (Read and write) -- secrets encryption key");
209
- if (0 === warnings) yield* Console.log("\nNo unknown keys detected.");
210
- else yield* Console.log(`\n${warnings} warning(s) found.`);
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* Console.log(`Config already exists: ${configPath}`);
293
+ if (existsSync(configPath)) yield* Effect.log(`Config already exists: ${configPath}`);
293
294
  else {
294
295
  writeFileSync(configPath, CONFIG_TEMPLATE);
295
- yield* Console.log(`Created: ${configPath}`);
296
+ yield* Effect.log(`Created: ${configPath}`);
296
297
  }
297
- if (existsSync(credsPath)) yield* Console.log(`Credentials already exists: ${credsPath}`);
298
+ if (existsSync(credsPath)) yield* Effect.log(`Credentials already exists: ${credsPath}`);
298
299
  else {
299
300
  writeFileSync(credsPath, CREDENTIALS_TEMPLATE);
300
- yield* Console.log(`Created: ${credsPath}`);
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* Console.log(`Added ${CREDENTIALS_FILE} to .gitignore`);
309
+ yield* Effect.log(`Added ${CREDENTIALS_FILE} to .gitignore`);
309
310
  }
310
311
  } else {
311
312
  writeFileSync(gitignorePath, `${CREDENTIALS_FILE}\n`);
312
- yield* Console.log(`Created .gitignore with ${CREDENTIALS_FILE}`);
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* Console.log(`Created .gitignore in ${targetDir}`);
319
+ yield* Effect.log(`Created .gitignore in ${targetDir}`);
319
320
  }
320
321
  }
321
- yield* Console.log("\nDone! Edit your config and credentials files to get started.");
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 { config: parsedConfig } = yield* loadConfigWithDir(configFile, config);
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* Console.log(`Default owner: ${defaultOwner}\n`);
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* Console.log(`[${groupName}] (owner: ${owner})`);
334
- for (const repo of group.repos)yield* Console.log(` - ${owner}/${repo}`);
335
- if (group.settings?.length) yield* Console.log(` settings: ${group.settings.join(", ")}`);
336
- if (group.environments?.length) yield* Console.log(` environments: ${group.environments.join(", ")}`);
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* Console.log(` secrets: ${parts.join(", ")}`);
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* Console.log(` variables: ${parts.join(", ")}`);
356
+ if (parts.length) yield* Effect.log(` variables: ${parts.join(", ")}`);
354
357
  }
355
- if (group.rulesets?.length) yield* Console.log(` rulesets: ${group.rulesets.join(", ")}`);
356
- if (group.credentials) yield* Console.log(` credentials: ${group.credentials}`);
357
- yield* Console.log("");
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 { config: parsedConfig, configDir } = yield* loadConfigWithDir(configFile, config);
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* Console.error("No GitHub token found. Run 'reposets credentials create' first."));
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* Console.log("DRY RUN \u2014 no changes will be made\n");
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(loadConfigWithDir(configFile, config));
419
- if ("Left" === configResult._tag) return void (yield* Console.error(`Config validation failed: ${configResult.left.message}`));
420
- yield* Console.log("Config schema: valid");
421
- const { config: parsedConfig, configDir } = configResult.right;
422
- for (const [groupName, group] of Object.entries(parsedConfig.groups)){
423
- for (const ref of group.settings ?? [])if (!parsedConfig.settings?.[ref]) {
424
- yield* Console.error(`Group '${groupName}': references unknown settings group '${ref}'`);
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* Console.error(`secrets.${groupName}.file.${entryName}: file not found: ${fullPath}`);
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* Console.error(`variables.${groupName}.file.${entryName}: file not found: ${fullPath}`);
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* Console.log("Credentials file: not found (optional)");
447
+ if ("Left" === credsResult._tag) yield* Effect.log("Credentials file: not found (optional)");
478
448
  else {
479
- yield* Console.log("Credentials schema: valid");
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* Console.error(`Group '${groupName}': references unknown credentials profile '${group.credentials}'`);
451
+ yield* Effect.logError(`Group '${groupName}': references unknown credentials profile '${group.credentials}'`);
482
452
  hasErrors = true;
483
453
  }
484
454
  }
485
- if (!hasErrors) yield* Console.log("\nAll checks passed.");
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(ConfigFilesLive), Effect.provide(NodeContext.layer));
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 type { ConfigFileService } from 'xdg-effect';
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
- export declare const ConfigFilesLive: Layer.Layer<AppDirs | ConfigFileService<any> | XdgResolver, never, FileSystem>;
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: Layer.Layer<CredentialResolver, never, OnePasswordClient>;
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): Layer.Layer<GitHubClient>;
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: Layer.Layer<GitHubClient>;
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: Layer.Layer<OnePasswordClient, never, never>;
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>): Layer.Layer<OnePasswordClient>;
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: Layer.Layer<SyncEngine, never, CredentialResolver | GitHubClient | SyncLogger>;
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): Layer.Layer<SyncLogger>;
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, loadConfigWithDir, resolveConfigFlag } from "./500.js";
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.2.1",
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.3.3"
57
+ "xdg-effect": "^1.0.0"
58
58
  },
59
59
  "engines": {
60
60
  "node": ">=20.0.0"
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.5"
8
+ "packageVersion": "7.58.7"
9
9
  }
10
10
  ]
11
11
  }