@chriscode/hush 4.2.0 → 5.0.1

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.
Files changed (78) hide show
  1. package/dist/cli.js +58 -29
  2. package/dist/commands/check.d.ts +3 -3
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +30 -33
  5. package/dist/commands/decrypt.d.ts +2 -2
  6. package/dist/commands/decrypt.d.ts.map +1 -1
  7. package/dist/commands/decrypt.js +52 -55
  8. package/dist/commands/edit.d.ts +2 -2
  9. package/dist/commands/edit.d.ts.map +1 -1
  10. package/dist/commands/edit.js +10 -12
  11. package/dist/commands/encrypt.d.ts +2 -2
  12. package/dist/commands/encrypt.d.ts.map +1 -1
  13. package/dist/commands/encrypt.js +27 -29
  14. package/dist/commands/expansions.d.ts +2 -2
  15. package/dist/commands/expansions.d.ts.map +1 -1
  16. package/dist/commands/expansions.js +46 -44
  17. package/dist/commands/has.d.ts +2 -2
  18. package/dist/commands/has.d.ts.map +1 -1
  19. package/dist/commands/has.js +12 -15
  20. package/dist/commands/init.d.ts +2 -2
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +107 -87
  23. package/dist/commands/inspect.d.ts +2 -2
  24. package/dist/commands/inspect.d.ts.map +1 -1
  25. package/dist/commands/inspect.js +14 -16
  26. package/dist/commands/keys.d.ts +2 -1
  27. package/dist/commands/keys.d.ts.map +1 -1
  28. package/dist/commands/keys.js +47 -49
  29. package/dist/commands/list.d.ts +2 -2
  30. package/dist/commands/list.d.ts.map +1 -1
  31. package/dist/commands/list.js +11 -14
  32. package/dist/commands/migrate.d.ts +7 -0
  33. package/dist/commands/migrate.d.ts.map +1 -0
  34. package/dist/commands/migrate.js +117 -0
  35. package/dist/commands/push.d.ts +2 -2
  36. package/dist/commands/push.d.ts.map +1 -1
  37. package/dist/commands/push.js +41 -45
  38. package/dist/commands/resolve.d.ts +2 -2
  39. package/dist/commands/resolve.d.ts.map +1 -1
  40. package/dist/commands/resolve.js +25 -28
  41. package/dist/commands/run.d.ts +2 -2
  42. package/dist/commands/run.d.ts.map +1 -1
  43. package/dist/commands/run.js +35 -39
  44. package/dist/commands/set.d.ts +2 -2
  45. package/dist/commands/set.d.ts.map +1 -1
  46. package/dist/commands/set.js +61 -70
  47. package/dist/commands/skill.d.ts +2 -2
  48. package/dist/commands/skill.d.ts.map +1 -1
  49. package/dist/commands/skill.js +186 -487
  50. package/dist/commands/status.d.ts +2 -2
  51. package/dist/commands/status.d.ts.map +1 -1
  52. package/dist/commands/status.js +52 -55
  53. package/dist/commands/template.d.ts +2 -2
  54. package/dist/commands/template.d.ts.map +1 -1
  55. package/dist/commands/template.js +36 -39
  56. package/dist/commands/trace.d.ts +2 -2
  57. package/dist/commands/trace.d.ts.map +1 -1
  58. package/dist/commands/trace.js +16 -19
  59. package/dist/config/loader.js +3 -3
  60. package/dist/context.d.ts +3 -0
  61. package/dist/context.d.ts.map +1 -0
  62. package/dist/context.js +59 -0
  63. package/dist/core/parse.js +3 -3
  64. package/dist/core/sops.js +9 -9
  65. package/dist/core/template.d.ts +2 -2
  66. package/dist/core/template.d.ts.map +1 -1
  67. package/dist/core/template.js +11 -12
  68. package/dist/lib/age.js +9 -9
  69. package/dist/lib/fs.d.ts +25 -0
  70. package/dist/lib/fs.d.ts.map +1 -0
  71. package/dist/lib/fs.js +36 -0
  72. package/dist/lib/onepassword.d.ts.map +1 -1
  73. package/dist/lib/onepassword.js +41 -4
  74. package/dist/types.d.ts +91 -0
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +4 -4
  77. package/dist/utils/version-check.js +5 -5
  78. package/package.json +3 -2
@@ -0,0 +1,117 @@
1
+ import { join } from 'node:path';
2
+ import pc from 'picocolors';
3
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
4
+ const FILE_MIGRATIONS = [
5
+ { from: '.env.encrypted', to: '.hush.encrypted' },
6
+ { from: '.env.development.encrypted', to: '.hush.development.encrypted' },
7
+ { from: '.env.production.encrypted', to: '.hush.production.encrypted' },
8
+ { from: '.env.local.encrypted', to: '.hush.local.encrypted' },
9
+ ];
10
+ const SOURCE_MIGRATIONS = {
11
+ '.env': '.hush',
12
+ '.env.development': '.hush.development',
13
+ '.env.production': '.hush.production',
14
+ '.env.local': '.hush.local',
15
+ };
16
+ function getMigrationFiles(ctx, root) {
17
+ return FILE_MIGRATIONS.map(({ from, to }) => ({
18
+ from,
19
+ to,
20
+ exists: ctx.fs.existsSync(join(root, from)),
21
+ }));
22
+ }
23
+ function migrateConfig(ctx, root, dryRun) {
24
+ const projectInfo = ctx.config.findProjectRoot(root);
25
+ if (!projectInfo)
26
+ return false;
27
+ const configPath = projectInfo.configPath;
28
+ const content = ctx.fs.readFileSync(configPath, 'utf-8');
29
+ const config = parseYaml(content);
30
+ let modified = false;
31
+ const sources = config.sources;
32
+ if (sources) {
33
+ for (const [oldValue, newValue] of Object.entries(SOURCE_MIGRATIONS)) {
34
+ for (const [key, value] of Object.entries(sources)) {
35
+ if (value === oldValue) {
36
+ if (!dryRun) {
37
+ sources[key] = newValue;
38
+ }
39
+ modified = true;
40
+ }
41
+ }
42
+ }
43
+ }
44
+ if (modified && !dryRun) {
45
+ const schemaComment = content.startsWith('#') ? content.split('\n')[0] + '\n' : '';
46
+ const newContent = schemaComment + stringifyYaml(config, { indent: 2 });
47
+ ctx.fs.writeFileSync(configPath, newContent, 'utf-8');
48
+ }
49
+ return modified;
50
+ }
51
+ export async function migrateCommand(ctx, options) {
52
+ const { root, dryRun } = options;
53
+ ctx.logger.log(pc.blue('Hush v4 → v5 Migration\n'));
54
+ if (dryRun) {
55
+ ctx.logger.log(pc.yellow('DRY RUN - no changes will be made\n'));
56
+ }
57
+ const migrations = getMigrationFiles(ctx, root);
58
+ const filesToMigrate = migrations.filter(m => m.exists);
59
+ if (filesToMigrate.length === 0) {
60
+ ctx.logger.log(pc.dim('No v4 encrypted files found (.env.encrypted, etc.)'));
61
+ ctx.logger.log(pc.dim('Already on v5 or no encrypted files exist.\n'));
62
+ const configNeedsMigration = migrateConfig(ctx, root, true);
63
+ if (configNeedsMigration) {
64
+ ctx.logger.log(pc.yellow('hush.yaml contains v4 source paths that need updating.\n'));
65
+ if (!dryRun) {
66
+ migrateConfig(ctx, root, false);
67
+ ctx.logger.log(pc.green('Updated hush.yaml source paths to v5 format.\n'));
68
+ }
69
+ }
70
+ return;
71
+ }
72
+ ctx.logger.log(pc.bold('Files to migrate:'));
73
+ for (const { from, to, exists } of migrations) {
74
+ if (exists) {
75
+ ctx.logger.log(` ${pc.yellow(from)} → ${pc.green(to)}`);
76
+ }
77
+ else {
78
+ ctx.logger.log(pc.dim(` ${from} (not found, skipping)`));
79
+ }
80
+ }
81
+ ctx.logger.log('');
82
+ if (dryRun) {
83
+ ctx.logger.log(pc.dim('Run without --dry-run to apply changes.'));
84
+ return;
85
+ }
86
+ let migratedCount = 0;
87
+ for (const { from, to, exists } of migrations) {
88
+ if (!exists)
89
+ continue;
90
+ const fromPath = join(root, from);
91
+ const toPath = join(root, to);
92
+ if (ctx.fs.existsSync(toPath)) {
93
+ ctx.logger.log(pc.yellow(` Skipping ${from}: ${to} already exists`));
94
+ continue;
95
+ }
96
+ const content = ctx.fs.readFileSync(fromPath, 'utf-8');
97
+ ctx.fs.writeFileSync(toPath, content);
98
+ ctx.fs.unlinkSync(fromPath);
99
+ ctx.logger.log(pc.green(` Migrated ${from} → ${to}`));
100
+ migratedCount++;
101
+ }
102
+ const configUpdated = migrateConfig(ctx, root, false);
103
+ if (configUpdated) {
104
+ ctx.logger.log(pc.green(' Updated hush.yaml source paths'));
105
+ }
106
+ ctx.logger.log('');
107
+ if (migratedCount > 0 || configUpdated) {
108
+ ctx.logger.log(pc.green(pc.bold(`Migration complete.`)));
109
+ ctx.logger.log(pc.dim('\nNext steps:'));
110
+ ctx.logger.log(pc.dim(' 1. git add .hush.encrypted .hush.*.encrypted hush.yaml'));
111
+ ctx.logger.log(pc.dim(' 2. git rm .env.encrypted .env.*.encrypted (if tracked)'));
112
+ ctx.logger.log(pc.dim(' 3. git commit -m "chore: migrate to Hush v5 format"'));
113
+ }
114
+ else {
115
+ ctx.logger.log(pc.dim('No changes made.'));
116
+ }
117
+ }
@@ -1,3 +1,3 @@
1
- import type { PushOptions } from '../types.js';
2
- export declare function pushCommand(options: PushOptions): Promise<void>;
1
+ import type { PushOptions, HushContext } from '../types.js';
2
+ export declare function pushCommand(ctx: HushContext, options: PushOptions): Promise<void>;
3
3
  //# sourceMappingURL=push.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/commands/push.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAU,WAAW,EAAqC,MAAM,aAAa,CAAC;AAgH1F,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6GrE"}
1
+ {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/commands/push.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAU,WAAW,EAAqC,WAAW,EAAE,MAAM,aAAa,CAAC;AAiHvG,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6GvF"}
@@ -1,26 +1,22 @@
1
- import { execSync } from 'node:child_process';
2
- import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
3
1
  import { join } from 'node:path';
4
2
  import pc from 'picocolors';
5
- import { loadConfig } from '../config/loader.js';
6
3
  import { filterVarsForTarget } from '../core/filter.js';
7
4
  import { interpolateVars } from '../core/interpolate.js';
8
5
  import { mergeVars } from '../core/merge.js';
9
6
  import { parseEnvContent } from '../core/parse.js';
10
- import { decrypt as sopsDecrypt } from '../core/sops.js';
11
7
  import { loadLocalTemplates, resolveTemplateVars } from '../core/template.js';
12
- function pushWorkerSecret(key, value, targetDir, dryRun, verbose) {
8
+ function pushWorkerSecret(ctx, key, value, targetDir, dryRun, verbose) {
13
9
  if (dryRun) {
14
10
  if (verbose) {
15
- console.log(pc.green(` + ${key}`));
11
+ ctx.logger.log(pc.green(` + ${key}`));
16
12
  }
17
13
  else {
18
- console.log(pc.dim(` [dry-run] ${key}`));
14
+ ctx.logger.log(pc.dim(` [dry-run] ${key}`));
19
15
  }
20
16
  return true;
21
17
  }
22
18
  try {
23
- execSync(`echo "${value}" | wrangler secret put ${key}`, {
19
+ ctx.exec.execSync(`echo "${value}" | wrangler secret put ${key}`, {
24
20
  cwd: targetDir,
25
21
  stdio: ['pipe', 'pipe', 'pipe'],
26
22
  shell: '/bin/bash',
@@ -29,18 +25,18 @@ function pushWorkerSecret(key, value, targetDir, dryRun, verbose) {
29
25
  }
30
26
  catch (error) {
31
27
  const err = error;
32
- console.error(pc.red(` Failed: ${key} - ${err.stderr || err.message}`));
28
+ ctx.logger.error(pc.red(` Failed: ${key} - ${err.stderr || err.message}`));
33
29
  return false;
34
30
  }
35
31
  }
36
- function pushPagesSecrets(vars, projectName, targetDir, dryRun, verbose) {
32
+ function pushPagesSecrets(ctx, vars, projectName, targetDir, dryRun, verbose) {
37
33
  if (dryRun) {
38
34
  for (const { key } of vars) {
39
35
  if (verbose) {
40
- console.log(pc.green(` + ${key}`));
36
+ ctx.logger.log(pc.green(` + ${key}`));
41
37
  }
42
38
  else {
43
- console.log(pc.dim(` [dry-run] ${key}`));
39
+ ctx.logger.log(pc.dim(` [dry-run] ${key}`));
44
40
  }
45
41
  }
46
42
  return { success: vars.length, failed: 0 };
@@ -51,26 +47,26 @@ function pushPagesSecrets(vars, projectName, targetDir, dryRun, verbose) {
51
47
  }
52
48
  const tempFile = join(targetDir, '.hush-secrets-temp.json');
53
49
  try {
54
- writeFileSync(tempFile, JSON.stringify(secretsJson, null, 2));
55
- execSync(`wrangler pages secret bulk "${tempFile}" --project-name "${projectName}"`, {
50
+ ctx.fs.writeFileSync(tempFile, JSON.stringify(secretsJson, null, 2));
51
+ ctx.exec.execSync(`wrangler pages secret bulk "${tempFile}" --project-name "${projectName}"`, {
56
52
  cwd: targetDir,
57
53
  stdio: ['pipe', 'pipe', 'pipe'],
58
54
  shell: '/bin/bash',
59
55
  });
60
56
  for (const { key } of vars) {
61
- console.log(pc.green(` ${key}`));
57
+ ctx.logger.log(pc.green(` ${key}`));
62
58
  }
63
59
  return { success: vars.length, failed: 0 };
64
60
  }
65
61
  catch (error) {
66
62
  const err = error;
67
63
  const stderrStr = err.stderr instanceof Buffer ? err.stderr.toString() : (err.stderr || err.message || 'Unknown error');
68
- console.error(pc.red(` Failed to push secrets: ${stderrStr}`));
64
+ ctx.logger.error(pc.red(` Failed to push secrets: ${stderrStr}`));
69
65
  return { success: 0, failed: vars.length };
70
66
  }
71
67
  finally {
72
- if (existsSync(tempFile)) {
73
- unlinkSync(tempFile);
68
+ if (ctx.fs.existsSync(tempFile)) {
69
+ ctx.fs.unlinkSync(tempFile);
74
70
  }
75
71
  }
76
72
  }
@@ -103,30 +99,30 @@ function getPagesProject(target) {
103
99
  }
104
100
  throw new Error(`Target "${target.name}" is not configured for Cloudflare Pages`);
105
101
  }
106
- export async function pushCommand(options) {
102
+ export async function pushCommand(ctx, options) {
107
103
  const { root, dryRun, verbose, target: targetFilter } = options;
108
- const config = loadConfig(root);
109
- console.log(pc.blue('Pushing production secrets to Cloudflare...'));
104
+ const config = ctx.config.loadConfig(root);
105
+ ctx.logger.log(pc.blue('Pushing production secrets to Cloudflare...'));
110
106
  if (dryRun) {
111
- console.log(pc.yellow('(dry-run mode)'));
107
+ ctx.logger.log(pc.yellow('(dry-run mode)'));
112
108
  if (verbose) {
113
- console.log(pc.dim('(verbose output enabled)'));
109
+ ctx.logger.log(pc.dim('(verbose output enabled)'));
114
110
  }
115
111
  }
116
112
  const sharedEncrypted = join(root, config.sources.shared + '.encrypted');
117
113
  const prodEncrypted = join(root, config.sources.production + '.encrypted');
118
114
  const varSources = [];
119
- if (existsSync(sharedEncrypted)) {
120
- const content = sopsDecrypt(sharedEncrypted);
115
+ if (ctx.fs.existsSync(sharedEncrypted)) {
116
+ const content = ctx.sops.decrypt(sharedEncrypted);
121
117
  varSources.push(parseEnvContent(content));
122
118
  }
123
- if (existsSync(prodEncrypted)) {
124
- const content = sopsDecrypt(prodEncrypted);
119
+ if (ctx.fs.existsSync(prodEncrypted)) {
120
+ const content = ctx.sops.decrypt(prodEncrypted);
125
121
  varSources.push(parseEnvContent(content));
126
122
  }
127
123
  if (varSources.length === 0) {
128
- console.error(pc.red('No encrypted files found'));
129
- process.exit(1);
124
+ ctx.logger.error(pc.red('No encrypted files found'));
125
+ ctx.process.exit(1);
130
126
  }
131
127
  const merged = mergeVars(...varSources);
132
128
  const interpolated = interpolateVars(merged);
@@ -139,47 +135,47 @@ export async function pushCommand(options) {
139
135
  pushableTargets = getTargetsWithPush(config, targetFilter);
140
136
  }
141
137
  catch (error) {
142
- console.error(pc.red(error.message));
143
- process.exit(1);
138
+ ctx.logger.error(pc.red(error.message));
139
+ ctx.process.exit(1);
144
140
  }
145
141
  if (pushableTargets.length === 0) {
146
- console.error(pc.red('No targets configured for push'));
147
- console.error(pc.dim('Add format: wrangler or push_to: { type: cloudflare-pages, project: ... } to a target'));
148
- process.exit(1);
142
+ ctx.logger.error(pc.red('No targets configured for push'));
143
+ ctx.logger.error(pc.dim('Add format: wrangler or push_to: { type: cloudflare-pages, project: ... } to a target'));
144
+ ctx.process.exit(1);
149
145
  }
150
146
  for (const target of pushableTargets) {
151
147
  const targetDir = join(root, target.path);
152
148
  const pushType = getPushType(target);
153
149
  let filtered = filterVarsForTarget(interpolated, target);
154
- const localTemplate = loadLocalTemplates(targetDir, 'production');
150
+ const localTemplate = loadLocalTemplates(targetDir, 'production', ctx.fs);
155
151
  if (localTemplate.hasTemplate) {
156
- const templateVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
152
+ const templateVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: ctx.process.env });
157
153
  filtered = mergeVars(filtered, templateVars);
158
154
  }
159
155
  if (filtered.length === 0) {
160
- console.log(pc.dim(`\n${target.name} - no matching vars, skipped`));
156
+ ctx.logger.log(pc.dim(`\n${target.name} - no matching vars, skipped`));
161
157
  continue;
162
158
  }
163
159
  const typeLabel = pushType === 'cloudflare-pages' ? 'Pages' : 'Workers';
164
160
  if (dryRun && verbose) {
165
- console.log(pc.blue(`\n[DRY RUN] Would push to ${target.name} (${typeLabel}, ${target.path}/):`));
161
+ ctx.logger.log(pc.blue(`\n[DRY RUN] Would push to ${target.name} (${typeLabel}, ${target.path}/):`));
166
162
  }
167
163
  else {
168
- console.log(pc.blue(`\n${target.name} (${typeLabel}, ${target.path}/)`));
164
+ ctx.logger.log(pc.blue(`\n${target.name} (${typeLabel}, ${target.path}/)`));
169
165
  }
170
166
  let success = 0;
171
167
  let failed = 0;
172
168
  if (pushType === 'cloudflare-pages') {
173
169
  const projectName = getPagesProject(target);
174
- const result = pushPagesSecrets(filtered, projectName, targetDir, dryRun, verbose);
170
+ const result = pushPagesSecrets(ctx, filtered, projectName, targetDir, dryRun, verbose);
175
171
  success = result.success;
176
172
  failed = result.failed;
177
173
  }
178
174
  else {
179
175
  for (const { key, value } of filtered) {
180
- if (pushWorkerSecret(key, value, targetDir, dryRun, verbose)) {
176
+ if (pushWorkerSecret(ctx, key, value, targetDir, dryRun, verbose)) {
181
177
  if (!dryRun)
182
- console.log(pc.green(` ${key}`));
178
+ ctx.logger.log(pc.green(` ${key}`));
183
179
  success++;
184
180
  }
185
181
  else {
@@ -187,12 +183,12 @@ export async function pushCommand(options) {
187
183
  }
188
184
  }
189
185
  }
190
- console.log(pc.dim(` ${success} pushed, ${failed} failed`));
186
+ ctx.logger.log(pc.dim(` ${success} pushed, ${failed} failed`));
191
187
  }
192
188
  if (dryRun) {
193
- console.log(pc.yellow('\n[dry-run] No secrets were pushed'));
189
+ ctx.logger.log(pc.yellow('\n[dry-run] No secrets were pushed'));
194
190
  }
195
191
  else {
196
- console.log(pc.green('\nPush complete'));
192
+ ctx.logger.log(pc.green('\nPush complete'));
197
193
  }
198
194
  }
@@ -1,8 +1,8 @@
1
- import type { Environment } from '../types.js';
1
+ import type { Environment, HushContext } from '../types.js';
2
2
  export interface ResolveOptions {
3
3
  root: string;
4
4
  env: Environment;
5
5
  target: string;
6
6
  }
7
- export declare function resolveCommand(options: ResolveOptions): Promise<void>;
7
+ export declare function resolveCommand(ctx: HushContext, options: ResolveOptions): Promise<void>;
8
8
  //# sourceMappingURL=resolve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/commands/resolve.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAU,WAAW,EAAU,MAAM,aAAa,CAAC;AAG/D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AA6BD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmI3E"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/commands/resolve.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAU,WAAW,EAAU,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AA6BD,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmI7F"}
@@ -1,11 +1,8 @@
1
- import { existsSync } from 'node:fs';
2
1
  import { join } from 'node:path';
3
2
  import pc from 'picocolors';
4
- import { loadConfig } from '../config/loader.js';
5
3
  import { interpolateVars } from '../core/interpolate.js';
6
4
  import { mergeVars } from '../core/merge.js';
7
5
  import { parseEnvContent } from '../core/parse.js';
8
- import { decrypt as sopsDecrypt } from '../core/sops.js';
9
6
  import { loadLocalTemplates } from '../core/template.js';
10
7
  import { FORMAT_OUTPUT_FILES } from '../types.js';
11
8
  function matchesPattern(key, pattern) {
@@ -26,14 +23,14 @@ function getOutputFilename(target, env) {
26
23
  }
27
24
  return FORMAT_OUTPUT_FILES[target.format][env];
28
25
  }
29
- export async function resolveCommand(options) {
26
+ export async function resolveCommand(ctx, options) {
30
27
  const { root, env, target: targetName } = options;
31
- const config = loadConfig(root);
28
+ const config = ctx.config.loadConfig(root);
32
29
  const target = config.targets.find(t => t.name === targetName);
33
30
  if (!target) {
34
- console.error(pc.red(`Target not found: ${targetName}`));
35
- console.error(pc.dim('Available targets: ' + config.targets.map(t => t.name).join(', ')));
36
- process.exit(1);
31
+ ctx.logger.error(`Target not found: ${targetName}`);
32
+ ctx.logger.error(pc.dim('Available targets: ' + config.targets.map(t => t.name).join(', ')));
33
+ ctx.process.exit(1);
37
34
  }
38
35
  const sharedEncrypted = join(root, config.sources.shared + '.encrypted');
39
36
  const envEncrypted = join(root, config.sources[env] + '.encrypted');
@@ -41,9 +38,9 @@ export async function resolveCommand(options) {
41
38
  const varsBySource = new Map();
42
39
  const allVars = new Map();
43
40
  const loadSource = (path, sourceName) => {
44
- if (!existsSync(path))
41
+ if (!ctx.fs.existsSync(path))
45
42
  return;
46
- const content = sopsDecrypt(path);
43
+ const content = ctx.sops.decrypt(path);
47
44
  const vars = parseEnvContent(content);
48
45
  const sourceVars = [];
49
46
  for (const v of vars) {
@@ -57,8 +54,8 @@ export async function resolveCommand(options) {
57
54
  loadSource(envEncrypted, config.sources[env]);
58
55
  loadSource(localEncrypted, config.sources.local);
59
56
  if (allVars.size === 0) {
60
- console.error(pc.red('No encrypted files found'));
61
- process.exit(1);
57
+ ctx.logger.error('No encrypted files found');
58
+ ctx.process.exit(1);
62
59
  }
63
60
  const merged = mergeVars(...Array.from(varsBySource.values()).map(sources => sources.map(s => ({ key: s.key, value: s.value }))));
64
61
  const interpolated = interpolateVars(merged);
@@ -84,44 +81,44 @@ export async function resolveCommand(options) {
84
81
  included.push({ key: v.key, source });
85
82
  }
86
83
  const outputFile = getOutputFilename(target, env);
87
- console.log(pc.bold(`\nTarget: ${pc.cyan(target.name)}`));
88
- console.log(`Path: ${pc.dim(target.path + '/')}`);
89
- console.log(`Format: ${pc.dim(target.format)} ${pc.dim(`(${outputFile})`)}`);
90
- console.log(`Environment: ${pc.dim(env)}`);
91
- console.log(pc.green(`\n✅ ROOT SECRETS (Matched Filters) (${included.length}):`));
84
+ ctx.logger.log(pc.bold(`\nTarget: ${pc.cyan(target.name)}`));
85
+ ctx.logger.log(`Path: ${pc.dim(target.path + '/')}`);
86
+ ctx.logger.log(`Format: ${pc.dim(target.format)} ${pc.dim(`(${outputFile})`)}`);
87
+ ctx.logger.log(`Environment: ${pc.dim(env)}`);
88
+ ctx.logger.log(pc.green(`\n✅ ROOT SECRETS (Matched Filters) (${included.length}):`));
92
89
  if (included.length === 0) {
93
- console.log(pc.dim(' (none)'));
90
+ ctx.logger.log(pc.dim(' (none)'));
94
91
  }
95
92
  else {
96
93
  const maxKeyLen = Math.max(...included.map(v => v.key.length));
97
94
  for (const v of included) {
98
- console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`(source: ${v.source})`)}`);
95
+ ctx.logger.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`(source: ${v.source})`)}`);
99
96
  }
100
97
  }
101
- console.log(pc.red(`\n🚫 EXCLUDED VARIABLES (${excluded.length}):`));
98
+ ctx.logger.log(pc.red(`\n🚫 EXCLUDED VARIABLES (${excluded.length}):`));
102
99
  if (excluded.length === 0) {
103
- console.log(pc.dim(' (none)'));
100
+ ctx.logger.log(pc.dim(' (none)'));
104
101
  }
105
102
  else {
106
103
  const maxKeyLen = Math.max(...excluded.map(v => v.key.length));
107
104
  for (const v of excluded) {
108
- console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`(matches: ${v.pattern})`)}`);
105
+ ctx.logger.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`(matches: ${v.pattern})`)}`);
109
106
  }
110
107
  }
111
108
  const targetAbsPath = join(root, target.path);
112
- const localTemplate = loadLocalTemplates(targetAbsPath, env);
109
+ const localTemplate = loadLocalTemplates(targetAbsPath, env, ctx.fs);
113
110
  if (localTemplate.hasTemplate) {
114
- console.log(pc.blue(`\n📄 TEMPLATE EXPANSIONS (${pc.dim(join(target.path, '.env'))}):`));
111
+ ctx.logger.log(pc.blue(`\n📄 TEMPLATE EXPANSIONS (${pc.dim(join(target.path, '.hush'))}):`));
115
112
  const maxKeyLen = Math.max(...localTemplate.vars.map(v => v.key.length));
116
113
  for (const v of localTemplate.vars) {
117
- console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`← ${v.value}`)}`);
114
+ ctx.logger.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`← ${v.value}`)}`);
118
115
  }
119
116
  // Calculate final merged list for clarity
120
117
  const finalKeys = new Set([
121
118
  ...included.map(v => v.key),
122
119
  ...localTemplate.vars.map(v => v.key)
123
120
  ]);
124
- console.log(pc.magenta(`\n📦 FINAL INJECTION (${finalKeys.size} total):`));
121
+ ctx.logger.log(pc.magenta(`\n📦 FINAL INJECTION (${finalKeys.size} total):`));
125
122
  const sortedKeys = Array.from(finalKeys).sort();
126
123
  for (const key of sortedKeys) {
127
124
  const isTemplate = localTemplate.vars.some(v => v.key === key);
@@ -133,8 +130,8 @@ export async function resolveCommand(options) {
133
130
  sourceInfo = pc.dim('(template)');
134
131
  else if (isRoot)
135
132
  sourceInfo = pc.dim('(root)');
136
- console.log(` ${key} ${sourceInfo}`);
133
+ ctx.logger.log(` ${key} ${sourceInfo}`);
137
134
  }
138
135
  }
139
- console.log('');
136
+ ctx.logger.log('');
140
137
  }
@@ -1,3 +1,3 @@
1
- import type { RunOptions } from '../types.js';
2
- export declare function runCommand(options: RunOptions): Promise<void>;
1
+ import type { RunOptions, HushContext } from '../types.js';
2
+ export declare function runCommand(ctx: HushContext, options: RunOptions): Promise<void>;
3
3
  //# sourceMappingURL=run.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAkD/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA8GnE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAmC,WAAW,EAAE,MAAM,aAAa,CAAC;AAkD5F,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA8GrF"}
@@ -1,32 +1,28 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
1
  import { join, resolve } from 'node:path';
4
2
  import pc from 'picocolors';
5
- import { loadConfig, findProjectRoot } from '../config/loader.js';
6
3
  import { filterVarsForTarget } from '../core/filter.js';
7
4
  import { interpolateVars, getUnresolvedVars } from '../core/interpolate.js';
8
5
  import { mergeVars } from '../core/merge.js';
9
6
  import { parseEnvContent } from '../core/parse.js';
10
- import { decrypt as sopsDecrypt } from '../core/sops.js';
11
7
  import { loadLocalTemplates, resolveTemplateVars } from '../core/template.js';
12
8
  function getEncryptedPath(sourcePath) {
13
9
  return sourcePath + '.encrypted';
14
10
  }
15
- function getDecryptedSecrets(projectRoot, env, config) {
11
+ function getDecryptedSecrets(ctx, projectRoot, env, config) {
16
12
  const sharedEncrypted = join(projectRoot, getEncryptedPath(config.sources.shared));
17
13
  const envEncrypted = join(projectRoot, getEncryptedPath(config.sources[env]));
18
14
  const localEncrypted = join(projectRoot, getEncryptedPath(config.sources.local));
19
15
  const varSources = [];
20
- if (existsSync(sharedEncrypted)) {
21
- const content = sopsDecrypt(sharedEncrypted);
16
+ if (ctx.fs.existsSync(sharedEncrypted)) {
17
+ const content = ctx.sops.decrypt(sharedEncrypted);
22
18
  varSources.push(parseEnvContent(content));
23
19
  }
24
- if (existsSync(envEncrypted)) {
25
- const content = sopsDecrypt(envEncrypted);
20
+ if (ctx.fs.existsSync(envEncrypted)) {
21
+ const content = ctx.sops.decrypt(envEncrypted);
26
22
  varSources.push(parseEnvContent(content));
27
23
  }
28
- if (existsSync(localEncrypted)) {
29
- const content = sopsDecrypt(localEncrypted);
24
+ if (ctx.fs.existsSync(localEncrypted)) {
25
+ const content = ctx.sops.decrypt(localEncrypted);
30
26
  varSources.push(parseEnvContent(content));
31
27
  }
32
28
  if (varSources.length === 0) {
@@ -46,31 +42,31 @@ function getRootSecretsAsRecord(vars) {
46
42
  }
47
43
  return record;
48
44
  }
49
- export async function runCommand(options) {
45
+ export async function runCommand(ctx, options) {
50
46
  const { root, env, target, command } = options;
51
47
  if (!command || command.length === 0) {
52
- console.error(pc.red('Usage: hush run -- <command>'));
53
- console.error(pc.dim('Example: hush run -- npm start'));
54
- console.error(pc.dim(' hush run -e production -- npm run build'));
55
- console.error(pc.dim(' hush run --target api -- wrangler dev'));
56
- process.exit(1);
48
+ ctx.logger.error('Usage: hush run -- <command>');
49
+ ctx.logger.error(pc.dim('Example: hush run -- npm start'));
50
+ ctx.logger.error(pc.dim(' hush run -e production -- npm run build'));
51
+ ctx.logger.error(pc.dim(' hush run --target api -- wrangler dev'));
52
+ ctx.process.exit(1);
57
53
  }
58
54
  const contextDir = root;
59
- const projectInfo = findProjectRoot(contextDir);
55
+ const projectInfo = ctx.config.findProjectRoot(contextDir);
60
56
  if (!projectInfo) {
61
- console.error(pc.red('No hush.yaml found in current directory or any parent directory.'));
62
- console.error(pc.dim('Run: npx hush init'));
63
- process.exit(1);
57
+ ctx.logger.error('No hush.yaml found in current directory or any parent directory.');
58
+ ctx.logger.error(pc.dim('Run: npx hush init'));
59
+ ctx.process.exit(1);
64
60
  }
65
61
  const { projectRoot } = projectInfo;
66
- const config = loadConfig(projectRoot);
67
- const rootSecrets = getDecryptedSecrets(projectRoot, env, config);
62
+ const config = ctx.config.loadConfig(projectRoot);
63
+ const rootSecrets = getDecryptedSecrets(ctx, projectRoot, env, config);
68
64
  const rootSecretsRecord = getRootSecretsAsRecord(rootSecrets);
69
- const localTemplate = loadLocalTemplates(contextDir, env);
65
+ const localTemplate = loadLocalTemplates(contextDir, env, ctx.fs);
70
66
  // 1. Resolve Template Vars
71
67
  let templateVars = [];
72
68
  if (localTemplate.hasTemplate) {
73
- templateVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
69
+ templateVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: ctx.process.env });
74
70
  }
75
71
  // 2. Resolve Target Vars
76
72
  let targetVars = [];
@@ -79,9 +75,9 @@ export async function runCommand(options) {
79
75
  ? config.targets.find(t => t.name === target)
80
76
  : config.targets.find(t => resolve(projectRoot, t.path) === resolve(contextDir));
81
77
  if (target && !targetConfig) {
82
- console.error(pc.red(`Target "${target}" not found in hush.yaml`));
83
- console.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
84
- process.exit(1);
78
+ ctx.logger.error(`Target "${target}" not found in hush.yaml`);
79
+ ctx.logger.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
80
+ ctx.process.exit(1);
85
81
  }
86
82
  if (targetConfig) {
87
83
  targetVars = filterVarsForTarget(rootSecrets, targetConfig);
@@ -89,11 +85,11 @@ export async function runCommand(options) {
89
85
  targetVars.push({ key: 'CLOUDFLARE_INCLUDE_PROCESS_ENV', value: 'true' });
90
86
  const devVarsPath = join(targetConfig.path, '.dev.vars');
91
87
  const absDevVarsPath = join(projectRoot, devVarsPath);
92
- if (existsSync(absDevVarsPath)) {
93
- console.warn(pc.yellow('\n⚠️ Wrangler Conflict Detected'));
94
- console.warn(pc.yellow(` Found .dev.vars in ${targetConfig.path}`));
95
- console.warn(pc.yellow(' Wrangler will IGNORE Hush secrets while this file exists.'));
96
- console.warn(pc.bold(` Fix: rm ${devVarsPath}\n`));
88
+ if (ctx.fs.existsSync(absDevVarsPath)) {
89
+ ctx.logger.warn('\n⚠️ Wrangler Conflict Detected');
90
+ ctx.logger.warn(` Found .dev.vars in ${targetConfig.path}`);
91
+ ctx.logger.warn(' Wrangler will IGNORE Hush secrets while this file exists.');
92
+ ctx.logger.warn(pc.bold(` Fix: rm ${devVarsPath}\n`));
97
93
  }
98
94
  }
99
95
  }
@@ -115,22 +111,22 @@ export async function runCommand(options) {
115
111
  }
116
112
  const unresolved = getUnresolvedVars(vars);
117
113
  if (unresolved.length > 0) {
118
- console.warn(pc.yellow(`Warning: ${unresolved.length} vars have unresolved references: ${unresolved.join(', ')}`));
114
+ ctx.logger.warn(`Warning: ${unresolved.length} vars have unresolved references: ${unresolved.join(', ')}`);
119
115
  }
120
116
  const childEnv = {
121
- ...process.env,
117
+ ...ctx.process.env,
122
118
  ...Object.fromEntries(vars.map(v => [v.key, v.value])),
123
119
  };
124
120
  const [cmd, ...args] = command;
125
- const result = spawnSync(cmd, args, {
121
+ const result = ctx.exec.spawnSync(cmd, args, {
126
122
  stdio: 'inherit',
127
123
  env: childEnv,
128
124
  shell: true,
129
125
  cwd: contextDir,
130
126
  });
131
127
  if (result.error) {
132
- console.error(pc.red(`Failed to execute: ${result.error.message}`));
133
- process.exit(1);
128
+ ctx.logger.error(`Failed to execute: ${result.error.message}`);
129
+ ctx.process.exit(1);
134
130
  }
135
- process.exit(result.status ?? 1);
131
+ ctx.process.exit(result.status ?? 1);
136
132
  }
@@ -1,3 +1,3 @@
1
- import type { SetOptions } from '../types.js';
2
- export declare function setCommand(options: SetOptions): Promise<void>;
1
+ import type { HushContext, SetOptions } from '../types.js';
2
+ export declare function setCommand(ctx: HushContext, options: SetOptions): Promise<void>;
3
3
  //# sourceMappingURL=set.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA2M9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
1
+ {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAoM3D,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CrF"}