@lkangd/cc-env 1.0.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.
Files changed (111) hide show
  1. package/.claude/settings.json +6 -0
  2. package/.claude/settings.local.json +3 -0
  3. package/.nvmrc +1 -0
  4. package/dist/cli.js +266 -0
  5. package/dist/commands/debug.js +17 -0
  6. package/dist/commands/init.js +64 -0
  7. package/dist/commands/preset/create.js +61 -0
  8. package/dist/commands/preset/delete.js +25 -0
  9. package/dist/commands/preset/edit.js +15 -0
  10. package/dist/commands/preset/list.js +16 -0
  11. package/dist/commands/preset/show.js +16 -0
  12. package/dist/commands/restore.js +65 -0
  13. package/dist/commands/run.js +80 -0
  14. package/dist/core/errors.js +11 -0
  15. package/dist/core/find-claude.js +64 -0
  16. package/dist/core/format.js +23 -0
  17. package/dist/core/fs.js +12 -0
  18. package/dist/core/gitignore.js +23 -0
  19. package/dist/core/lock.js +25 -0
  20. package/dist/core/logger.js +8 -0
  21. package/dist/core/mask.js +13 -0
  22. package/dist/core/paths.js +32 -0
  23. package/dist/core/process-env.js +4 -0
  24. package/dist/core/schema.js +38 -0
  25. package/dist/core/spawn.js +26 -0
  26. package/dist/flows/init-flow.js +35 -0
  27. package/dist/flows/preset-create-flow.js +80 -0
  28. package/dist/flows/restore-flow.js +75 -0
  29. package/dist/ink/init-app.js +54 -0
  30. package/dist/ink/preset-create-app.js +271 -0
  31. package/dist/ink/preset-delete-app.js +47 -0
  32. package/dist/ink/preset-list-app.js +27 -0
  33. package/dist/ink/preset-show-app.js +27 -0
  34. package/dist/ink/restore-app.js +102 -0
  35. package/dist/ink/run-preset-select-app.js +31 -0
  36. package/dist/ink/summary.js +28 -0
  37. package/dist/services/claude-settings-env-service.js +55 -0
  38. package/dist/services/config-service.js +26 -0
  39. package/dist/services/history-service.js +39 -0
  40. package/dist/services/preset-service.js +61 -0
  41. package/dist/services/project-env-service.js +90 -0
  42. package/dist/services/project-state-service.js +26 -0
  43. package/dist/services/runtime-env-service.js +13 -0
  44. package/dist/services/settings-env-service.js +36 -0
  45. package/dist/services/shell-env-service.js +77 -0
  46. package/docs/product-specs/index.draft.md +106 -0
  47. package/docs/product-specs/index.md +911 -0
  48. package/docs/product-specs/optional.md +42 -0
  49. package/docs/references/claude-code-env.md +224 -0
  50. package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +1331 -0
  51. package/docs/superpowers/plans/2026-04-24-cc-env.md +1666 -0
  52. package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +1432 -0
  53. package/docs/superpowers/specs/2026-04-24-cc-env-design.md +438 -0
  54. package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +181 -0
  55. package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +78 -0
  56. package/package.json +55 -0
  57. package/src/cli.ts +337 -0
  58. package/src/commands/init.ts +139 -0
  59. package/src/commands/preset/create.ts +96 -0
  60. package/src/commands/preset/delete.ts +62 -0
  61. package/src/commands/preset/show.ts +51 -0
  62. package/src/commands/restore.ts +150 -0
  63. package/src/commands/run.ts +158 -0
  64. package/src/core/errors.ts +13 -0
  65. package/src/core/find-claude.ts +70 -0
  66. package/src/core/format.ts +29 -0
  67. package/src/core/fs.ts +18 -0
  68. package/src/core/gitignore.ts +26 -0
  69. package/src/core/logger.ts +11 -0
  70. package/src/core/mask.ts +17 -0
  71. package/src/core/paths.ts +41 -0
  72. package/src/core/process-env.ts +11 -0
  73. package/src/core/schema.ts +55 -0
  74. package/src/core/spawn.ts +36 -0
  75. package/src/flows/init-flow.ts +61 -0
  76. package/src/flows/preset-create-flow.ts +129 -0
  77. package/src/flows/restore-flow.ts +144 -0
  78. package/src/ink/init-app.tsx +110 -0
  79. package/src/ink/preset-create-app.tsx +451 -0
  80. package/src/ink/preset-delete-app.tsx +114 -0
  81. package/src/ink/preset-show-app.tsx +76 -0
  82. package/src/ink/restore-app.tsx +230 -0
  83. package/src/ink/run-preset-select-app.tsx +83 -0
  84. package/src/ink/summary.tsx +91 -0
  85. package/src/services/claude-settings-env-service.ts +72 -0
  86. package/src/services/history-service.ts +48 -0
  87. package/src/services/preset-service.ts +72 -0
  88. package/src/services/project-env-service.ts +128 -0
  89. package/src/services/project-state-service.ts +31 -0
  90. package/src/services/settings-env-service.ts +40 -0
  91. package/src/services/shell-env-service.ts +112 -0
  92. package/src/types.d.ts +19 -0
  93. package/tests/cli/help.test.ts +133 -0
  94. package/tests/cli/init.test.ts +76 -0
  95. package/tests/cli/restore.test.ts +172 -0
  96. package/tests/commands/create.test.ts +263 -0
  97. package/tests/commands/output.test.ts +119 -0
  98. package/tests/commands/run.test.ts +218 -0
  99. package/tests/core/gitignore.test.ts +98 -0
  100. package/tests/core/paths.test.ts +24 -0
  101. package/tests/core/schema-mask.test.ts +182 -0
  102. package/tests/core/spawn.test.ts +47 -0
  103. package/tests/flows/init-flow.test.ts +40 -0
  104. package/tests/flows/preset-create-flow.test.ts +225 -0
  105. package/tests/flows/restore-flow.test.ts +157 -0
  106. package/tests/integration/init-restore.test.ts +406 -0
  107. package/tests/services/claude-shell.test.ts +183 -0
  108. package/tests/services/storage.test.ts +143 -0
  109. package/tsconfig.build.json +9 -0
  110. package/tsconfig.json +22 -0
  111. package/vitest.config.ts +8 -0
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,61 @@
1
+ export type InitFlowStep = 'keys' | 'confirm' | 'done'
2
+
3
+ export type InitFlowState = {
4
+ step: InitFlowStep
5
+ availableKeys: string[]
6
+ requiredKeys: string[]
7
+ selectedKeys: string[]
8
+ }
9
+
10
+ export type InitFlowAction =
11
+ | { type: 'toggle-key'; key: string }
12
+ | { type: 'continue' }
13
+ | { type: 'confirm' }
14
+
15
+ export function createInitFlowState(
16
+ availableKeys: string[],
17
+ requiredKeys: string[],
18
+ ): InitFlowState {
19
+ return {
20
+ step: 'keys',
21
+ availableKeys,
22
+ requiredKeys,
23
+ selectedKeys: requiredKeys,
24
+ }
25
+ }
26
+
27
+ export function advanceInitFlow(
28
+ state: InitFlowState,
29
+ action: InitFlowAction,
30
+ ): InitFlowState {
31
+ if (state.step === 'keys' && action.type === 'toggle-key') {
32
+ if (state.requiredKeys.includes(action.key)) {
33
+ return state
34
+ }
35
+
36
+ const selectedKeys = state.selectedKeys.includes(action.key)
37
+ ? state.selectedKeys.filter((key) => key !== action.key)
38
+ : [...state.selectedKeys, action.key]
39
+
40
+ return {
41
+ ...state,
42
+ selectedKeys,
43
+ }
44
+ }
45
+
46
+ if (state.step === 'keys' && action.type === 'continue') {
47
+ return {
48
+ ...state,
49
+ step: 'confirm',
50
+ }
51
+ }
52
+
53
+ if (state.step === 'confirm' && action.type === 'confirm') {
54
+ return {
55
+ ...state,
56
+ step: 'done',
57
+ }
58
+ }
59
+
60
+ return state
61
+ }
@@ -0,0 +1,129 @@
1
+ import type { EnvMap } from '../core/schema.js'
2
+
3
+ export type PresetCreateSource = 'file' | 'manual'
4
+ export type PresetCreateDestination = 'global' | 'project'
5
+
6
+ export type PresetCreateStep =
7
+ | 'source'
8
+ | 'filePath'
9
+ | 'keys'
10
+ | 'manualInput'
11
+ | 'name'
12
+ | 'destination'
13
+ | 'confirm'
14
+ | 'done'
15
+
16
+ export type PresetCreateFlowState = {
17
+ step: PresetCreateStep
18
+ source?: PresetCreateSource
19
+ filePath?: string
20
+ env: EnvMap
21
+ allKeys: string[]
22
+ selectedKeys: string[]
23
+ presetName: string
24
+ destination?: PresetCreateDestination
25
+ error?: string | undefined
26
+ }
27
+
28
+ export type PresetCreateFlowResult = Pick<
29
+ PresetCreateFlowState,
30
+ 'source' | 'env' | 'selectedKeys' | 'presetName' | 'destination'
31
+ > & {
32
+ filePath?: string | undefined
33
+ }
34
+
35
+ export type PresetCreateFlowAction =
36
+ | { type: 'select-source'; source: PresetCreateSource }
37
+ | { type: 'set-file-path'; filePath: string }
38
+ | { type: 'set-error'; error: string }
39
+ | { type: 'select-keys'; keys: string[]; env: EnvMap }
40
+ | { type: 'add-manual-pair'; key: string; value: string }
41
+ | { type: 'finish-manual-input' }
42
+ | { type: 'set-name'; name: string }
43
+ | { type: 'select-destination'; destination: PresetCreateDestination }
44
+ | { type: 'confirm' }
45
+
46
+ export function createPresetCreateFlowState(): PresetCreateFlowState {
47
+ return {
48
+ step: 'source',
49
+ env: {},
50
+ allKeys: [],
51
+ selectedKeys: [],
52
+ presetName: '',
53
+ }
54
+ }
55
+
56
+ export function advancePresetCreateFlow(
57
+ state: PresetCreateFlowState,
58
+ action: PresetCreateFlowAction,
59
+ ): PresetCreateFlowState {
60
+ switch (state.step) {
61
+ case 'source':
62
+ if (action.type !== 'select-source') return state
63
+ return {
64
+ ...state,
65
+ step: action.source === 'file' ? 'filePath' : 'manualInput',
66
+ source: action.source,
67
+ }
68
+
69
+ case 'filePath':
70
+ if (action.type === 'set-error') {
71
+ return { ...state, error: action.error }
72
+ }
73
+ if (action.type !== 'set-file-path') return state
74
+ return {
75
+ ...state,
76
+ step: 'keys',
77
+ filePath: action.filePath,
78
+ error: undefined,
79
+ }
80
+
81
+ case 'keys':
82
+ if (action.type !== 'select-keys') return state
83
+ return {
84
+ ...state,
85
+ step: 'name',
86
+ selectedKeys: action.keys,
87
+ env: action.env,
88
+ }
89
+
90
+ case 'manualInput':
91
+ if (action.type === 'add-manual-pair') {
92
+ const hasKey = state.selectedKeys.includes(action.key)
93
+ return {
94
+ ...state,
95
+ env: { ...state.env, [action.key]: action.value },
96
+ selectedKeys: hasKey ? state.selectedKeys : [...state.selectedKeys, action.key],
97
+ error: undefined,
98
+ }
99
+ }
100
+ if (action.type === 'set-error') {
101
+ return { ...state, error: action.error }
102
+ }
103
+ if (action.type !== 'finish-manual-input') return state
104
+ return { ...state, step: 'name' }
105
+
106
+ case 'name':
107
+ if (action.type !== 'set-name') return state
108
+ return {
109
+ ...state,
110
+ step: 'destination',
111
+ presetName: action.name,
112
+ }
113
+
114
+ case 'destination':
115
+ if (action.type !== 'select-destination') return state
116
+ return {
117
+ ...state,
118
+ step: 'confirm',
119
+ destination: action.destination,
120
+ }
121
+
122
+ case 'confirm':
123
+ if (action.type !== 'confirm') return state
124
+ return { ...state, step: 'done' }
125
+
126
+ case 'done':
127
+ return state
128
+ }
129
+ }
@@ -0,0 +1,144 @@
1
+ import type { HistoryRecord } from '../core/schema.js'
2
+
3
+ export type RestoreFlowStep = 'record' | 'target' | 'confirm' | 'done'
4
+
5
+ type BaseRestoreFlowState = {
6
+ records: HistoryRecord[]
7
+ selectedTimestamp?: string
8
+ }
9
+
10
+ export type RestoreFlowState =
11
+ | ({ step: 'record' } & BaseRestoreFlowState)
12
+ | ({ step: 'target'; selectedTimestamp: string } & BaseRestoreFlowState)
13
+ | ({ step: 'confirm'; selectedTimestamp: string } & BaseRestoreFlowState & (
14
+ | {
15
+ targetType: 'settings'
16
+ }
17
+ | {
18
+ targetType: 'preset'
19
+ targetName: string
20
+ }
21
+ ))
22
+ | ({ step: 'done'; selectedTimestamp: string } & BaseRestoreFlowState & (
23
+ | {
24
+ targetType: 'settings'
25
+ }
26
+ | {
27
+ targetType: 'preset'
28
+ targetName: string
29
+ }
30
+ ))
31
+
32
+ export type RestoreFlowAction =
33
+ | {
34
+ type: 'select-record'
35
+ timestamp: string
36
+ }
37
+ | {
38
+ type: 'select-target'
39
+ targetType: 'settings' | 'preset'
40
+ targetName?: string
41
+ }
42
+ | {
43
+ type: 'confirm'
44
+ }
45
+
46
+ export function createRestoreFlowState(records: HistoryRecord[]): RestoreFlowState {
47
+ return {
48
+ step: 'record',
49
+ records,
50
+ }
51
+ }
52
+
53
+ export function advanceRestoreFlow(
54
+ state: RestoreFlowState,
55
+ action: RestoreFlowAction,
56
+ ): RestoreFlowState {
57
+ switch (state.step) {
58
+ case 'record': {
59
+ if (action.type !== 'select-record') {
60
+ return state
61
+ }
62
+
63
+ const selectedRecord = state.records.find(
64
+ (record) => record.timestamp === action.timestamp,
65
+ )
66
+
67
+ if (!selectedRecord) {
68
+ return state
69
+ }
70
+
71
+ if (selectedRecord.action === 'init') {
72
+ return {
73
+ ...state,
74
+ step: 'confirm',
75
+ selectedTimestamp: action.timestamp,
76
+ } as RestoreFlowState
77
+ }
78
+
79
+ return {
80
+ ...state,
81
+ step: 'target',
82
+ selectedTimestamp: action.timestamp,
83
+ }
84
+ }
85
+
86
+ case 'target':
87
+ if (action.type !== 'select-target' || !state.selectedTimestamp) {
88
+ return state
89
+ }
90
+
91
+ if (action.targetType === 'preset' && !action.targetName) {
92
+ return state
93
+ }
94
+
95
+ if (action.targetType === 'settings') {
96
+ return {
97
+ ...state,
98
+ step: 'confirm',
99
+ targetType: 'settings',
100
+ }
101
+ }
102
+
103
+ const targetName = action.targetName as string
104
+
105
+ return {
106
+ ...state,
107
+ step: 'confirm',
108
+ targetType: 'preset',
109
+ targetName,
110
+ }
111
+
112
+ case 'confirm':
113
+ if (action.type !== 'confirm' || !state.selectedTimestamp) {
114
+ return state
115
+ }
116
+
117
+ const selectedRecord = state.records.find(
118
+ (record) => record.timestamp === state.selectedTimestamp,
119
+ )
120
+
121
+ if (selectedRecord?.action === 'init') {
122
+ return {
123
+ ...state,
124
+ step: 'done',
125
+ } as RestoreFlowState
126
+ }
127
+
128
+ if (!state.targetType) {
129
+ return state
130
+ }
131
+
132
+ if (state.targetType === 'preset' && !state.targetName) {
133
+ return state
134
+ }
135
+
136
+ return {
137
+ ...state,
138
+ step: 'done',
139
+ }
140
+
141
+ case 'done':
142
+ return state
143
+ }
144
+ }
@@ -0,0 +1,110 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import { Box, Text, useApp, useInput } from 'ink'
3
+
4
+ import {
5
+ advanceInitFlow,
6
+ createInitFlowState,
7
+ } from '../flows/init-flow.js'
8
+
9
+ export function InitApp({
10
+ keys = [],
11
+ requiredKeys = [],
12
+ sourceFiles = [],
13
+ onSubmit,
14
+ }: {
15
+ keys?: string[]
16
+ requiredKeys?: string[]
17
+ sourceFiles?: string[]
18
+ onSubmit?: (result: { confirmed: boolean; selectedKeys: string[] }) => void
19
+ }) {
20
+ const { exit } = useApp()
21
+ const [cursor, setCursor] = useState(0)
22
+ const [flowState, setFlowState] = useState(() =>
23
+ createInitFlowState(keys, requiredKeys),
24
+ )
25
+
26
+ useEffect(() => {
27
+ if (!onSubmit) {
28
+ return
29
+ }
30
+
31
+ if (keys.length === 0) {
32
+ onSubmit({ confirmed: false, selectedKeys: [] })
33
+ exit()
34
+ }
35
+ }, [exit, keys.length, onSubmit])
36
+
37
+ useInput((input, key) => {
38
+ if (!onSubmit) {
39
+ return
40
+ }
41
+
42
+ if (key.upArrow || input === 'k') {
43
+ setCursor((c) => Math.max(0, c - 1))
44
+ return
45
+ }
46
+
47
+ if (key.downArrow || input === 'j') {
48
+ setCursor((c) => Math.min(keys.length - 1, c + 1))
49
+ return
50
+ }
51
+
52
+ if (input === ' ') {
53
+ const targetKey = keys[cursor]
54
+ if (targetKey) {
55
+ setFlowState((prev) =>
56
+ advanceInitFlow(prev, { type: 'toggle-key', key: targetKey }),
57
+ )
58
+ }
59
+ return
60
+ }
61
+
62
+ if (key.return) {
63
+ onSubmit({ confirmed: true, selectedKeys: flowState.selectedKeys })
64
+ exit()
65
+ return
66
+ }
67
+
68
+ if (key.escape || input.toLowerCase() === 'q') {
69
+ onSubmit({ confirmed: false, selectedKeys: [] })
70
+ exit()
71
+ }
72
+ })
73
+
74
+ return (
75
+ <Box flexDirection="column">
76
+ <Text bold>Select env keys to migrate into managed shell config</Text>
77
+ {sourceFiles.length > 0 ? (
78
+ <Box flexDirection="column">
79
+ <Text dimColor>Source:</Text>
80
+ {sourceFiles.map((file) => (
81
+ <Text key={file} color="cyan"> {file}</Text>
82
+ ))}
83
+ </Box>
84
+ ) : null}
85
+ <Text dimColor>↑/k ↓/j navigate · space toggle · enter confirm · q cancel</Text>
86
+ <Box flexDirection="column" marginTop={1}>
87
+ {keys.map((key, i) => {
88
+ const isRequired = requiredKeys.includes(key)
89
+ const isSelected = flowState.selectedKeys.includes(key)
90
+ const isCursor = i === cursor
91
+ const checkbox = isSelected ? '[x]' : '[ ]'
92
+
93
+ return (
94
+ <Box key={key}>
95
+ <Text>{isCursor ? '❯ ' : ' '}</Text>
96
+ <Text color={isSelected ? 'green' : ''}>{checkbox}</Text>
97
+ <Text> {key}</Text>
98
+ {isRequired ? <Text dimColor> (required)</Text> : null}
99
+ </Box>
100
+ )
101
+ })}
102
+ </Box>
103
+ <Box marginTop={1}>
104
+ <Text dimColor>
105
+ {flowState.selectedKeys.length} of {keys.length} selected
106
+ </Text>
107
+ </Box>
108
+ </Box>
109
+ )
110
+ }