@lkangd/cc-env 1.1.0 → 1.1.2

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 (72) hide show
  1. package/LICENSE +15 -0
  2. package/dist/cli.js +3 -4
  3. package/package.json +8 -1
  4. package/.claude/settings.json +0 -6
  5. package/.claude/settings.local.json +0 -8
  6. package/.nvmrc +0 -1
  7. package/CHANGELOG.md +0 -66
  8. package/docs/product-specs/index.draft.md +0 -106
  9. package/docs/product-specs/index.md +0 -911
  10. package/docs/product-specs/optional.md +0 -42
  11. package/docs/references/claude-code-env.md +0 -224
  12. package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +0 -1331
  13. package/docs/superpowers/plans/2026-04-24-cc-env.md +0 -1666
  14. package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +0 -1432
  15. package/docs/superpowers/specs/2026-04-24-cc-env-design.md +0 -438
  16. package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +0 -181
  17. package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +0 -78
  18. package/src/cli.ts +0 -339
  19. package/src/commands/init.ts +0 -139
  20. package/src/commands/preset/create.ts +0 -96
  21. package/src/commands/preset/delete.ts +0 -62
  22. package/src/commands/preset/show.ts +0 -51
  23. package/src/commands/restore.ts +0 -150
  24. package/src/commands/run.ts +0 -158
  25. package/src/core/errors.ts +0 -13
  26. package/src/core/find-claude.ts +0 -70
  27. package/src/core/format.ts +0 -29
  28. package/src/core/fs.ts +0 -18
  29. package/src/core/gitignore.ts +0 -26
  30. package/src/core/logger.ts +0 -11
  31. package/src/core/mask.ts +0 -17
  32. package/src/core/paths.ts +0 -41
  33. package/src/core/process-env.ts +0 -11
  34. package/src/core/schema.ts +0 -55
  35. package/src/core/spawn.ts +0 -36
  36. package/src/flows/init-flow.ts +0 -61
  37. package/src/flows/preset-create-flow.ts +0 -129
  38. package/src/flows/restore-flow.ts +0 -144
  39. package/src/ink/init-app.tsx +0 -110
  40. package/src/ink/preset-create-app.tsx +0 -451
  41. package/src/ink/preset-delete-app.tsx +0 -114
  42. package/src/ink/preset-show-app.tsx +0 -76
  43. package/src/ink/restore-app.tsx +0 -230
  44. package/src/ink/run-preset-select-app.tsx +0 -83
  45. package/src/ink/summary.tsx +0 -91
  46. package/src/services/claude-settings-env-service.ts +0 -72
  47. package/src/services/history-service.ts +0 -48
  48. package/src/services/preset-service.ts +0 -72
  49. package/src/services/project-env-service.ts +0 -128
  50. package/src/services/project-state-service.ts +0 -31
  51. package/src/services/settings-env-service.ts +0 -40
  52. package/src/services/shell-env-service.ts +0 -112
  53. package/src/types.d.ts +0 -19
  54. package/tests/cli/help.test.ts +0 -133
  55. package/tests/cli/init.test.ts +0 -76
  56. package/tests/cli/restore.test.ts +0 -172
  57. package/tests/commands/create.test.ts +0 -263
  58. package/tests/commands/output.test.ts +0 -119
  59. package/tests/commands/run.test.ts +0 -218
  60. package/tests/core/gitignore.test.ts +0 -98
  61. package/tests/core/paths.test.ts +0 -24
  62. package/tests/core/schema-mask.test.ts +0 -182
  63. package/tests/core/spawn.test.ts +0 -47
  64. package/tests/flows/init-flow.test.ts +0 -40
  65. package/tests/flows/preset-create-flow.test.ts +0 -225
  66. package/tests/flows/restore-flow.test.ts +0 -157
  67. package/tests/integration/init-restore.test.ts +0 -406
  68. package/tests/services/claude-shell.test.ts +0 -183
  69. package/tests/services/storage.test.ts +0 -143
  70. package/tsconfig.build.json +0 -9
  71. package/tsconfig.json +0 -22
  72. package/vitest.config.ts +0 -8
@@ -1,150 +0,0 @@
1
- import React from 'react'
2
- import { Box, Text } from 'ink'
3
-
4
- import { CliError } from '../core/errors.js'
5
- import type { EnvMap, HistoryRecord, Preset } from '../core/schema.js'
6
- import type { ClaudeSettingsSource } from '../services/claude-settings-env-service.js'
7
- import type { ShellWriteRecord } from '../services/shell-env-service.js'
8
-
9
- const h = React.createElement
10
-
11
- type HistoryService = {
12
- list: () => Promise<HistoryRecord[]>
13
- }
14
-
15
- type ClaudeSettingsEnvService = {
16
- read: () => Promise<ClaudeSettingsSource[]>
17
- write: (sources: Array<{ path: string; env: EnvMap }>) => Promise<void>
18
- }
19
-
20
- type ShellEnvService = {
21
- removeKeys: (shellWrites: ShellWriteRecord[], keys: string[]) => Promise<void>
22
- }
23
-
24
- type SettingsEnvService = {
25
- read: () => Promise<EnvMap>
26
- write: (env: EnvMap) => Promise<unknown>
27
- }
28
-
29
- type PresetService = {
30
- read: (name: string) => Promise<Preset>
31
- write: (preset: Preset) => Promise<unknown>
32
- }
33
-
34
- type RestoreFlowResult = {
35
- confirmed?: boolean
36
- timestamp?: string
37
- targetType?: 'settings' | 'preset'
38
- targetName?: string
39
- }
40
-
41
- export function createRestoreCommand({
42
- historyService,
43
- claudeSettingsEnvService,
44
- shellEnvService,
45
- settingsEnvService,
46
- presetService,
47
- renderFlow,
48
- renderEnvSummary,
49
- }: {
50
- historyService: HistoryService
51
- claudeSettingsEnvService: ClaudeSettingsEnvService
52
- shellEnvService: ShellEnvService
53
- settingsEnvService: SettingsEnvService
54
- presetService: PresetService
55
- renderFlow: (context: {
56
- records: HistoryRecord[]
57
- yes: boolean
58
- }) => Promise<RestoreFlowResult | void> | RestoreFlowResult | void
59
- renderEnvSummary: (props: {
60
- title: string
61
- env: EnvMap
62
- fromFiles?: string[]
63
- toFiles?: string[]
64
- footer?: React.ReactNode
65
- }) => Promise<void>
66
- }) {
67
- return async function restore({ yes = false }: { yes?: boolean } = {}): Promise<void> {
68
- const records = await historyService.list()
69
- const result = await renderFlow({ records, yes })
70
-
71
- if (!result?.confirmed) {
72
- return
73
- }
74
-
75
- const record = records.find((entry) => entry.timestamp === result.timestamp)
76
-
77
- if (!record) {
78
- throw new CliError('Restore record not found')
79
- }
80
-
81
- if (record.action === 'init') {
82
- const mergedBackup = Object.fromEntries(
83
- record.sources.flatMap((s) => Object.entries(s.backup)),
84
- )
85
-
86
- const current = await claudeSettingsEnvService.read()
87
- await shellEnvService.removeKeys(record.shellWrites, record.migratedKeys)
88
- await claudeSettingsEnvService.write(
89
- current.map((source) => ({
90
- path: source.path,
91
- env: {
92
- ...source.env,
93
- ...(record.sources.find((s) => s.file === source.path)?.backup ?? {}),
94
- },
95
- })),
96
- )
97
-
98
- await renderEnvSummary({
99
- title: 'Restored',
100
- env: mergedBackup,
101
- fromFiles: record.shellWrites.map((sw) => sw.filePath),
102
- toFiles: record.sources.map((s) => s.file),
103
- footer: h(Box, { flexDirection: 'column' },
104
- h(Text, { color: 'green' }, 'Restore complete'),
105
- h(Text, { bold: true, color: 'green' }, 'Please restart your terminal for the restored environment variables to take effect.'),
106
- ),
107
- })
108
- return
109
- }
110
-
111
- if (result.targetType === 'settings') {
112
- const currentSettings = await settingsEnvService.read()
113
- await settingsEnvService.write({
114
- ...currentSettings,
115
- ...record.backup,
116
- })
117
-
118
- await renderEnvSummary({
119
- title: 'Restored to settings',
120
- env: record.backup,
121
- footer: h(Box, { flexDirection: 'column' },
122
- h(Text, { color: 'green' }, 'Restore complete'),
123
- h(Text, { bold: true, color: 'green' }, 'Please restart your terminal for the restored environment variables to take effect.'),
124
- ),
125
- })
126
- return
127
- }
128
-
129
- const presetName = result.targetName ?? record.targetName
130
- const preset = await presetService.read(presetName)
131
-
132
- await presetService.write({
133
- ...preset,
134
- updatedAt: new Date().toISOString(),
135
- env: {
136
- ...preset.env,
137
- ...record.backup,
138
- },
139
- })
140
-
141
- await renderEnvSummary({
142
- title: `Restored to preset ${presetName}`,
143
- env: record.backup,
144
- footer: h(Box, { flexDirection: 'column' },
145
- h(Text, { color: 'green' }, 'Restore complete'),
146
- h(Text, { bold: true, color: 'green' }, 'Please restart your terminal for the restored environment variables to take effect.'),
147
- ),
148
- })
149
- }
150
- }
@@ -1,158 +0,0 @@
1
- import { CliError } from '../core/errors.js'
2
- import { formatRunEnvBlock } from '../core/format.js'
3
- import type { EnvMap } from '../core/schema.js'
4
-
5
- const requiredInitKeys = [
6
- 'ANTHROPIC_AUTH_TOKEN',
7
- 'ANTHROPIC_BASE_URL',
8
- 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
9
- 'ANTHROPIC_DEFAULT_OPUS_MODEL',
10
- 'ANTHROPIC_DEFAULT_SONNET_MODEL',
11
- 'ANTHROPIC_REASONING_MODEL',
12
- ] as const
13
-
14
- type PresetSelectItem = {
15
- name: string
16
- env: EnvMap
17
- source: 'global' | 'project'
18
- }
19
-
20
- type ClaudeSettingsEnvService = {
21
- read: () => Promise<Array<{ path: string; exists: boolean; env: EnvMap }>>
22
- }
23
-
24
- type PresetService = {
25
- listNames: () => Promise<string[]>
26
- read: (name: string) => Promise<{ env: EnvMap }>
27
- }
28
-
29
- type ProjectEnvService = {
30
- readWithMeta: () => Promise<{ env: EnvMap; name?: string | undefined }>
31
- }
32
-
33
- type ProjectStateService = {
34
- getLastPreset: (cwd: string) => Promise<{ presetName: string; source: 'global' | 'project' } | undefined>
35
- saveLastPreset: (cwd: string, ref: { presetName: string; source: 'global' | 'project' }) => Promise<void>
36
- }
37
-
38
- type FindClaude = () => string
39
-
40
- type RenderPresetSelect = (input: {
41
- presets: Array<PresetSelectItem>
42
- defaultIndex: number
43
- }) => Promise<PresetSelectItem | undefined>
44
-
45
- export function createRunCommand({
46
- claudeSettingsEnvService,
47
- presetService,
48
- projectEnvService,
49
- projectStateService,
50
- findClaude,
51
- renderPresetSelect,
52
- spawnCommand,
53
- stdout = process.stdout,
54
- }: {
55
- claudeSettingsEnvService: ClaudeSettingsEnvService
56
- presetService: PresetService
57
- projectEnvService: ProjectEnvService
58
- projectStateService: ProjectStateService
59
- findClaude: FindClaude
60
- renderPresetSelect: RenderPresetSelect
61
- spawnCommand: (command: string, args: string[], env: NodeJS.ProcessEnv) => Promise<void>
62
- stdout?: Pick<NodeJS.WriteStream, 'write'>
63
- }) {
64
- return async function run({
65
- args = [],
66
- dryRun = false,
67
- yes = false,
68
- cwd,
69
- }: {
70
- args?: string[]
71
- dryRun?: boolean
72
- yes?: boolean
73
- cwd: string
74
- }): Promise<void> {
75
- // Step 0: Check settings files for init-managed keys
76
- const sources = await claudeSettingsEnvService.read()
77
- const mergedSettingsEnv = sources.reduce<EnvMap>(
78
- (acc, s) => ({ ...acc, ...s.env }),
79
- {} as EnvMap,
80
- )
81
- const staleKeys = requiredInitKeys.filter((k) => k in mergedSettingsEnv)
82
- if (staleKeys.length > 0) {
83
- throw new CliError(
84
- `Found init-managed keys in Claude settings:\n\n ${staleKeys.join(', \n ')}. \n\n Run "cc-env init" first.`,
85
- )
86
- }
87
-
88
- // Step 1: Collect all presets (project + global)
89
- const names = await presetService.listNames()
90
- const globalPresets = await Promise.all(
91
- names.map((name) =>
92
- presetService.read(name).then((p) => ({ name, env: p.env, source: 'global' as const })),
93
- ),
94
- )
95
- const { env: projectEnv, name: projectName } = await projectEnvService.readWithMeta()
96
- const projectPreset =
97
- Object.keys(projectEnv).length > 0
98
- ? [{ name: projectName ?? 'project', env: projectEnv, source: 'project' as const }]
99
- : []
100
-
101
- const presets = [...projectPreset, ...globalPresets]
102
- if (presets.length === 0) {
103
- throw new CliError('No presets found. Create one with "cc-env preset create".')
104
- }
105
-
106
- // Step 2: Determine default selection
107
- const savedRef = await projectStateService.getLastPreset(cwd)
108
- let defaultIndex = 0
109
- if (savedRef) {
110
- const idx = presets.findIndex(
111
- (p) => p.name === savedRef.presetName && p.source === savedRef.source,
112
- )
113
- if (idx >= 0) defaultIndex = idx
114
- } else if (projectPreset.length > 0) {
115
- defaultIndex = 0
116
- }
117
-
118
- // Step 3: Select preset (interactive or auto)
119
- let selected: PresetSelectItem | undefined
120
- if (yes) {
121
- selected = presets[defaultIndex]
122
- } else {
123
- selected = await renderPresetSelect({ presets, defaultIndex })
124
- }
125
- if (!selected) return
126
-
127
- // Step 4: Save selection
128
- await projectStateService.saveLastPreset(cwd, {
129
- presetName: selected.name,
130
- source: selected.source,
131
- })
132
-
133
- // Step 5: Resolve claude command
134
- let command: string
135
- let claudeArgs: string[]
136
- if (args.length > 0 && args[0] === 'claude') {
137
- command = 'claude'
138
- claudeArgs = args.slice(1)
139
- } else {
140
- command = findClaude()
141
- claudeArgs = args
142
- }
143
-
144
- // Step 6: Print env vars
145
- const presetKeys = new Set(Object.keys(selected.env))
146
- const envBlock = formatRunEnvBlock(selected.env, presetKeys)
147
- stdout.write(`Using preset: ${selected.name} (${selected.source})\n${envBlock}\n\n`)
148
-
149
- if (dryRun) {
150
- const preview = [command, ...claudeArgs].join(' ')
151
- stdout.write(`Would run: ${preview}\n`)
152
- return
153
- }
154
-
155
- // Step 7: Spawn
156
- await spawnCommand(command, claudeArgs, { ...process.env, ...selected.env })
157
- }
158
- }
@@ -1,13 +0,0 @@
1
- export class CliError extends Error {
2
- readonly exitCode: number
3
-
4
- constructor(message: string, exitCode = 1) {
5
- super(message)
6
- this.name = 'CliError'
7
- this.exitCode = exitCode
8
- }
9
- }
10
-
11
- export function invalidUsage(message: string): CliError {
12
- return new CliError(message, 2)
13
- }
@@ -1,70 +0,0 @@
1
- import { execSync } from 'node:child_process'
2
- import { existsSync, readFileSync, realpathSync } from 'node:fs'
3
- import { join } from 'node:path'
4
-
5
- import { CliError } from './errors.js'
6
-
7
- function resolveToJsFile(filePath: string): string {
8
- try {
9
- const realPath = realpathSync(filePath)
10
- if (realPath.endsWith('.js')) return realPath
11
- if (existsSync(realPath)) {
12
- const content = readFileSync(realPath, 'utf-8')
13
- if (
14
- content.startsWith('#!/usr/bin/env node') ||
15
- /^#!.*\/node$/m.test(content) ||
16
- content.includes('require(') ||
17
- content.includes('import ')
18
- ) {
19
- return realPath
20
- }
21
- }
22
-
23
- for (const candidate of [
24
- realPath + '.js',
25
- realPath.replace(/\/bin\//, '/lib/') + '.js',
26
- realPath.replace(/\/\.bin\//, '/lib/bin/') + '.js',
27
- ]) {
28
- if (existsSync(candidate)) return candidate
29
- }
30
-
31
- return realPath
32
- } catch {
33
- return filePath
34
- }
35
- }
36
-
37
- export function findClaudeExecutable(): string {
38
- try {
39
- let claudePath = execSync('which claude', { encoding: 'utf-8' }).trim()
40
- const aliasMatch = claudePath.match(/:\s*aliased to\s+(.+)$/)
41
- if (aliasMatch?.[1]) claudePath = aliasMatch[1]
42
-
43
- if (existsSync(claudePath)) {
44
- const content = readFileSync(claudePath, 'utf-8')
45
- if (content.startsWith('#!/bin/bash')) {
46
- const execMatch = content.match(/exec\s+"([^"]+)"/)
47
- if (execMatch?.[1]) return resolveToJsFile(execMatch[1])
48
- }
49
- }
50
-
51
- return resolveToJsFile(claudePath)
52
- } catch {}
53
-
54
- const home = process.env.HOME ?? process.cwd()
55
- const localWrapper = join(home, '.claude', 'local', 'claude')
56
- if (existsSync(localWrapper)) {
57
- const content = readFileSync(localWrapper, 'utf-8')
58
- if (content.startsWith('#!/bin/bash')) {
59
- const execMatch = content.match(/exec\s+"([^"]+)"/)
60
- if (execMatch?.[1]) return resolveToJsFile(execMatch[1])
61
- }
62
- }
63
-
64
- const localBin = join(home, '.claude', 'local', 'node_modules', '.bin', 'claude')
65
- if (existsSync(localBin)) return resolveToJsFile(localBin)
66
-
67
- throw new CliError(
68
- 'Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code',
69
- )
70
- }
@@ -1,29 +0,0 @@
1
- import { maskValue } from './mask.js'
2
- import type { EnvMap } from './schema.js'
3
-
4
- export function formatEnvBlock(env: EnvMap): string {
5
- return Object.entries(env)
6
- .sort(([left], [right]) => left.localeCompare(right))
7
- .map(([key, value]) => `${key}=${maskValue(key, value)}`)
8
- .join('\n')
9
- }
10
-
11
- export function formatRunEnvBlock(env: EnvMap, presetKeys: ReadonlySet<string>): string {
12
- const entries = Object.entries(env).sort(([a], [b]) => a.localeCompare(b))
13
- const presetEntries = entries.filter(([key]) => presetKeys.has(key))
14
- const otherCount = entries.length - presetEntries.length
15
-
16
- const lines = presetEntries.map(([key, value]) => `${key}=${maskValue(key, value)}`)
17
- if (otherCount > 0) {
18
- lines.push(`+${otherCount} other env vars applied`)
19
- }
20
-
21
- return lines.join('\n')
22
- }
23
-
24
- export function formatRestorePreview(env: EnvMap): string {
25
- return Object.entries(env)
26
- .sort(([left], [right]) => left.localeCompare(right))
27
- .map(([key, value]) => `${key}=${value}`)
28
- .join('\n')
29
- }
package/src/core/fs.ts DELETED
@@ -1,18 +0,0 @@
1
- import { mkdir, rename, writeFile } from 'node:fs/promises'
2
- import { basename, dirname, join } from 'node:path'
3
-
4
- export async function ensureParentDir(filePath: string): Promise<void> {
5
- await mkdir(dirname(filePath), { recursive: true })
6
- }
7
-
8
- export async function atomicWriteFile(filePath: string, content: string): Promise<void> {
9
- const parentDir = dirname(filePath)
10
- const tempFilePath = join(
11
- parentDir,
12
- `.${basename(filePath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`,
13
- )
14
-
15
- await ensureParentDir(filePath)
16
- await writeFile(tempFilePath, content, 'utf8')
17
- await rename(tempFilePath, filePath)
18
- }
@@ -1,26 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
- import { readFile, writeFile } from 'node:fs/promises'
3
- import { join } from 'node:path'
4
-
5
- export function isGitRepo(dir: string): boolean {
6
- return existsSync(join(dir, '.git'))
7
- }
8
-
9
- export async function ensureGitignoreEntry(dir: string, entry: string): Promise<void> {
10
- if (!isGitRepo(dir)) return
11
-
12
- const gitignorePath = join(dir, '.gitignore')
13
- let content = ''
14
-
15
- try {
16
- content = await readFile(gitignorePath, 'utf8')
17
- } catch {
18
- // .gitignore doesn't exist — will create it
19
- }
20
-
21
- const lines = content.split('\n')
22
- if (lines.some((line) => line === entry || line === `${entry}/`)) return
23
-
24
- const separator = content.length > 0 && !content.endsWith('\n') ? '\n' : ''
25
- await writeFile(gitignorePath, `${content}${separator}${entry}\n`, 'utf8')
26
- }
@@ -1,11 +0,0 @@
1
- import pino from 'pino'
2
-
3
- import { ensureParentDir } from './fs.js'
4
- import { resolveLogPath } from './paths.js'
5
-
6
- export async function createLogger(globalRoot: string) {
7
- const logPath = resolveLogPath(globalRoot)
8
- await ensureParentDir(logPath)
9
-
10
- return pino(pino.destination(logPath))
11
- }
package/src/core/mask.ts DELETED
@@ -1,17 +0,0 @@
1
- const sensitiveKeyPattern = /(_TOKEN|_KEY|_SECRET|_PASSWORD)$/i
2
-
3
- export function isSensitiveKey(key: string): boolean {
4
- return sensitiveKeyPattern.test(key)
5
- }
6
-
7
- export function maskValue(key: string, value: string): string {
8
- if (!isSensitiveKey(key)) {
9
- return value
10
- }
11
-
12
- if (value.length <= 8) {
13
- return '*'.repeat(value.length)
14
- }
15
-
16
- return `${value.slice(0, 9)}********`
17
- }
package/src/core/paths.ts DELETED
@@ -1,41 +0,0 @@
1
- import { join } from 'node:path'
2
-
3
- export function resolveGlobalRoot(globalRoot?: string): string {
4
- return globalRoot ?? join(process.env.HOME ?? process.cwd(), '.cc-env')
5
- }
6
-
7
- export function resolveClaudeSettingsPath(homeDir = process.env.HOME ?? process.cwd()): string {
8
- return join(homeDir, '.claude', 'settings.json')
9
- }
10
-
11
- export function resolveClaudeSettingsLocalPath(homeDir = process.env.HOME ?? process.cwd()): string {
12
- return join(homeDir, '.claude', 'settings.local.json')
13
- }
14
-
15
- export function resolveProjectSettingsPath(cwd = process.cwd()): string {
16
- return join(cwd, '.claude', 'settings.json')
17
- }
18
-
19
- export function resolveProjectSettingsLocalPath(cwd = process.cwd()): string {
20
- return join(cwd, '.claude', 'settings.local.json')
21
- }
22
-
23
- export function resolveShellConfigPaths(homeDir = process.env.HOME ?? process.cwd()) {
24
- return {
25
- zsh: join(homeDir, '.zshrc'),
26
- bash: join(homeDir, '.bashrc'),
27
- fish: join(homeDir, '.config', 'fish', 'config.fish'),
28
- }
29
- }
30
-
31
- export function resolvePresetPath(globalRoot: string, name: string): string {
32
- return join(globalRoot, 'presets', `${name}.json`)
33
- }
34
-
35
- export function resolveHistoryPath(globalRoot: string, timestamp: string): string {
36
- return join(globalRoot, 'history', `${timestamp.replaceAll(':', '-')}.json`)
37
- }
38
-
39
- export function resolveLogPath(globalRoot: string): string {
40
- return join(globalRoot, 'logs', 'app.log')
41
- }
@@ -1,11 +0,0 @@
1
- import { envMapSchema, type EnvMap } from './schema.js'
2
-
3
- export function toProcessEnvMap(input: Record<string, unknown>): EnvMap {
4
- return envMapSchema.parse(
5
- Object.fromEntries(
6
- Object.entries(input).filter(
7
- ([key, value]) => typeof value === 'string' && /^[A-Z0-9_]+$/.test(key),
8
- ),
9
- ),
10
- )
11
- }
@@ -1,55 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- const envKeySchema = z.string().regex(/^[A-Z0-9_]+$/)
4
-
5
- export const envMapSchema = z.record(
6
- envKeySchema,
7
- z.unknown()
8
- .refine((value) => value === null || typeof value !== 'object')
9
- .transform((value) => String(value)),
10
- )
11
-
12
- export const presetSchema = z.object({
13
- name: z.string(),
14
- createdAt: z.string().datetime({ offset: true }),
15
- updatedAt: z.string().datetime({ offset: true }),
16
- env: envMapSchema,
17
- })
18
-
19
- const shellWriteSchema = z.object({
20
- shell: z.enum(['zsh', 'bash', 'fish']),
21
- filePath: z.string(),
22
- env: envMapSchema,
23
- })
24
-
25
- const sourceEntrySchema = z.object({
26
- file: z.string(),
27
- backup: envMapSchema,
28
- })
29
-
30
- const initHistorySchema = z.object({
31
- timestamp: z.string().datetime({ offset: true }),
32
- action: z.literal('init'),
33
- migratedKeys: z.array(envKeySchema),
34
- sources: z.array(sourceEntrySchema),
35
- shellWrites: z.array(shellWriteSchema),
36
- })
37
-
38
- const restoreHistorySchema = z.object({
39
- timestamp: z.string().datetime({ offset: true }),
40
- action: z.literal('restore'),
41
- backup: envMapSchema,
42
- targetType: z.enum(['settings', 'preset']),
43
- targetName: z.string(),
44
- })
45
-
46
- export const historySchema = z.discriminatedUnion('action', [
47
- initHistorySchema,
48
- restoreHistorySchema,
49
- ])
50
-
51
- export type EnvMap = z.infer<typeof envMapSchema>
52
- export type SourceEntry = z.infer<typeof sourceEntrySchema>
53
- export type Preset = z.infer<typeof presetSchema>
54
- export type InitHistoryRecord = z.infer<typeof initHistorySchema>
55
- export type HistoryRecord = z.infer<typeof historySchema>
package/src/core/spawn.ts DELETED
@@ -1,36 +0,0 @@
1
- import spawn from 'cross-spawn'
2
-
3
- import { CliError } from './errors.js'
4
-
5
- export function spawnCommand(
6
- command: string,
7
- args: string[],
8
- env: NodeJS.ProcessEnv,
9
- ): Promise<void> {
10
- return new Promise((resolve, reject) => {
11
- const child = spawn(command, args, {
12
- env,
13
- stdio: 'inherit',
14
- })
15
-
16
- child.once('error', reject)
17
- child.once('close', (exitCode: number | null, signal: NodeJS.Signals | null) => {
18
- if (signal) {
19
- reject(new CliError(`Command terminated by signal ${signal}`))
20
- return
21
- }
22
-
23
- if (exitCode === null) {
24
- reject(new CliError('Command terminated without an exit code'))
25
- return
26
- }
27
-
28
- if (exitCode !== 0) {
29
- reject(new CliError(`Command exited with code ${exitCode}`, exitCode))
30
- return
31
- }
32
-
33
- resolve()
34
- })
35
- })
36
- }