@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.
- package/.claude/settings.json +6 -0
- package/.claude/settings.local.json +3 -0
- package/.nvmrc +1 -0
- package/dist/cli.js +266 -0
- package/dist/commands/debug.js +17 -0
- package/dist/commands/init.js +64 -0
- package/dist/commands/preset/create.js +61 -0
- package/dist/commands/preset/delete.js +25 -0
- package/dist/commands/preset/edit.js +15 -0
- package/dist/commands/preset/list.js +16 -0
- package/dist/commands/preset/show.js +16 -0
- package/dist/commands/restore.js +65 -0
- package/dist/commands/run.js +80 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/find-claude.js +64 -0
- package/dist/core/format.js +23 -0
- package/dist/core/fs.js +12 -0
- package/dist/core/gitignore.js +23 -0
- package/dist/core/lock.js +25 -0
- package/dist/core/logger.js +8 -0
- package/dist/core/mask.js +13 -0
- package/dist/core/paths.js +32 -0
- package/dist/core/process-env.js +4 -0
- package/dist/core/schema.js +38 -0
- package/dist/core/spawn.js +26 -0
- package/dist/flows/init-flow.js +35 -0
- package/dist/flows/preset-create-flow.js +80 -0
- package/dist/flows/restore-flow.js +75 -0
- package/dist/ink/init-app.js +54 -0
- package/dist/ink/preset-create-app.js +271 -0
- package/dist/ink/preset-delete-app.js +47 -0
- package/dist/ink/preset-list-app.js +27 -0
- package/dist/ink/preset-show-app.js +27 -0
- package/dist/ink/restore-app.js +102 -0
- package/dist/ink/run-preset-select-app.js +31 -0
- package/dist/ink/summary.js +28 -0
- package/dist/services/claude-settings-env-service.js +55 -0
- package/dist/services/config-service.js +26 -0
- package/dist/services/history-service.js +39 -0
- package/dist/services/preset-service.js +61 -0
- package/dist/services/project-env-service.js +90 -0
- package/dist/services/project-state-service.js +26 -0
- package/dist/services/runtime-env-service.js +13 -0
- package/dist/services/settings-env-service.js +36 -0
- package/dist/services/shell-env-service.js +77 -0
- package/docs/product-specs/index.draft.md +106 -0
- package/docs/product-specs/index.md +911 -0
- package/docs/product-specs/optional.md +42 -0
- package/docs/references/claude-code-env.md +224 -0
- package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +1331 -0
- package/docs/superpowers/plans/2026-04-24-cc-env.md +1666 -0
- package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +1432 -0
- package/docs/superpowers/specs/2026-04-24-cc-env-design.md +438 -0
- package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +181 -0
- package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +78 -0
- package/package.json +55 -0
- package/src/cli.ts +337 -0
- package/src/commands/init.ts +139 -0
- package/src/commands/preset/create.ts +96 -0
- package/src/commands/preset/delete.ts +62 -0
- package/src/commands/preset/show.ts +51 -0
- package/src/commands/restore.ts +150 -0
- package/src/commands/run.ts +158 -0
- package/src/core/errors.ts +13 -0
- package/src/core/find-claude.ts +70 -0
- package/src/core/format.ts +29 -0
- package/src/core/fs.ts +18 -0
- package/src/core/gitignore.ts +26 -0
- package/src/core/logger.ts +11 -0
- package/src/core/mask.ts +17 -0
- package/src/core/paths.ts +41 -0
- package/src/core/process-env.ts +11 -0
- package/src/core/schema.ts +55 -0
- package/src/core/spawn.ts +36 -0
- package/src/flows/init-flow.ts +61 -0
- package/src/flows/preset-create-flow.ts +129 -0
- package/src/flows/restore-flow.ts +144 -0
- package/src/ink/init-app.tsx +110 -0
- package/src/ink/preset-create-app.tsx +451 -0
- package/src/ink/preset-delete-app.tsx +114 -0
- package/src/ink/preset-show-app.tsx +76 -0
- package/src/ink/restore-app.tsx +230 -0
- package/src/ink/run-preset-select-app.tsx +83 -0
- package/src/ink/summary.tsx +91 -0
- package/src/services/claude-settings-env-service.ts +72 -0
- package/src/services/history-service.ts +48 -0
- package/src/services/preset-service.ts +72 -0
- package/src/services/project-env-service.ts +128 -0
- package/src/services/project-state-service.ts +31 -0
- package/src/services/settings-env-service.ts +40 -0
- package/src/services/shell-env-service.ts +112 -0
- package/src/types.d.ts +19 -0
- package/tests/cli/help.test.ts +133 -0
- package/tests/cli/init.test.ts +76 -0
- package/tests/cli/restore.test.ts +172 -0
- package/tests/commands/create.test.ts +263 -0
- package/tests/commands/output.test.ts +119 -0
- package/tests/commands/run.test.ts +218 -0
- package/tests/core/gitignore.test.ts +98 -0
- package/tests/core/paths.test.ts +24 -0
- package/tests/core/schema-mask.test.ts +182 -0
- package/tests/core/spawn.test.ts +47 -0
- package/tests/flows/init-flow.test.ts +40 -0
- package/tests/flows/preset-create-flow.test.ts +225 -0
- package/tests/flows/restore-flow.test.ts +157 -0
- package/tests/integration/init-restore.test.ts +406 -0
- package/tests/services/claude-shell.test.ts +183 -0
- package/tests/services/storage.test.ts +143 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { CliError } from './errors.js';
|
|
5
|
+
function resolveToJsFile(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
const realPath = realpathSync(filePath);
|
|
8
|
+
if (realPath.endsWith('.js'))
|
|
9
|
+
return realPath;
|
|
10
|
+
if (existsSync(realPath)) {
|
|
11
|
+
const content = readFileSync(realPath, 'utf-8');
|
|
12
|
+
if (content.startsWith('#!/usr/bin/env node') ||
|
|
13
|
+
/^#!.*\/node$/m.test(content) ||
|
|
14
|
+
content.includes('require(') ||
|
|
15
|
+
content.includes('import ')) {
|
|
16
|
+
return realPath;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const candidate of [
|
|
20
|
+
realPath + '.js',
|
|
21
|
+
realPath.replace(/\/bin\//, '/lib/') + '.js',
|
|
22
|
+
realPath.replace(/\/\.bin\//, '/lib/bin/') + '.js',
|
|
23
|
+
]) {
|
|
24
|
+
if (existsSync(candidate))
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
return realPath;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return filePath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function findClaudeExecutable() {
|
|
34
|
+
try {
|
|
35
|
+
let claudePath = execSync('which claude', { encoding: 'utf-8' }).trim();
|
|
36
|
+
const aliasMatch = claudePath.match(/:\s*aliased to\s+(.+)$/);
|
|
37
|
+
if (aliasMatch?.[1])
|
|
38
|
+
claudePath = aliasMatch[1];
|
|
39
|
+
if (existsSync(claudePath)) {
|
|
40
|
+
const content = readFileSync(claudePath, 'utf-8');
|
|
41
|
+
if (content.startsWith('#!/bin/bash')) {
|
|
42
|
+
const execMatch = content.match(/exec\s+"([^"]+)"/);
|
|
43
|
+
if (execMatch?.[1])
|
|
44
|
+
return resolveToJsFile(execMatch[1]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return resolveToJsFile(claudePath);
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
const home = process.env.HOME ?? process.cwd();
|
|
51
|
+
const localWrapper = join(home, '.claude', 'local', 'claude');
|
|
52
|
+
if (existsSync(localWrapper)) {
|
|
53
|
+
const content = readFileSync(localWrapper, 'utf-8');
|
|
54
|
+
if (content.startsWith('#!/bin/bash')) {
|
|
55
|
+
const execMatch = content.match(/exec\s+"([^"]+)"/);
|
|
56
|
+
if (execMatch?.[1])
|
|
57
|
+
return resolveToJsFile(execMatch[1]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const localBin = join(home, '.claude', 'local', 'node_modules', '.bin', 'claude');
|
|
61
|
+
if (existsSync(localBin))
|
|
62
|
+
return resolveToJsFile(localBin);
|
|
63
|
+
throw new CliError('Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code');
|
|
64
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { maskValue } from './mask.js';
|
|
2
|
+
export function formatEnvBlock(env) {
|
|
3
|
+
return Object.entries(env)
|
|
4
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
5
|
+
.map(([key, value]) => `${key}=${maskValue(key, value)}`)
|
|
6
|
+
.join('\n');
|
|
7
|
+
}
|
|
8
|
+
export function formatRunEnvBlock(env, presetKeys) {
|
|
9
|
+
const entries = Object.entries(env).sort(([a], [b]) => a.localeCompare(b));
|
|
10
|
+
const presetEntries = entries.filter(([key]) => presetKeys.has(key));
|
|
11
|
+
const otherCount = entries.length - presetEntries.length;
|
|
12
|
+
const lines = presetEntries.map(([key, value]) => `${key}=${maskValue(key, value)}`);
|
|
13
|
+
if (otherCount > 0) {
|
|
14
|
+
lines.push(`+${otherCount} other env vars applied`);
|
|
15
|
+
}
|
|
16
|
+
return lines.join('\n');
|
|
17
|
+
}
|
|
18
|
+
export function formatRestorePreview(env) {
|
|
19
|
+
return Object.entries(env)
|
|
20
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
21
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
22
|
+
.join('\n');
|
|
23
|
+
}
|
package/dist/core/fs.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { mkdir, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
export async function ensureParentDir(filePath) {
|
|
4
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
5
|
+
}
|
|
6
|
+
export async function atomicWriteFile(filePath, content) {
|
|
7
|
+
const parentDir = dirname(filePath);
|
|
8
|
+
const tempFilePath = join(parentDir, `.${basename(filePath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
9
|
+
await ensureParentDir(filePath);
|
|
10
|
+
await writeFile(tempFilePath, content, 'utf8');
|
|
11
|
+
await rename(tempFilePath, filePath);
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function isGitRepo(dir) {
|
|
5
|
+
return existsSync(join(dir, '.git'));
|
|
6
|
+
}
|
|
7
|
+
export async function ensureGitignoreEntry(dir, entry) {
|
|
8
|
+
if (!isGitRepo(dir))
|
|
9
|
+
return;
|
|
10
|
+
const gitignorePath = join(dir, '.gitignore');
|
|
11
|
+
let content = '';
|
|
12
|
+
try {
|
|
13
|
+
content = await readFile(gitignorePath, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// .gitignore doesn't exist — will create it
|
|
17
|
+
}
|
|
18
|
+
const lines = content.split('\n');
|
|
19
|
+
if (lines.some((line) => line === entry || line === `${entry}/`))
|
|
20
|
+
return;
|
|
21
|
+
const separator = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
|
|
22
|
+
await writeFile(gitignorePath, `${content}${separator}${entry}\n`, 'utf8');
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
import lockfile from 'proper-lockfile';
|
|
3
|
+
import { ensureParentDir } from './fs.js';
|
|
4
|
+
export async function withFileLock(filePath, run) {
|
|
5
|
+
await ensureParentDir(filePath);
|
|
6
|
+
const handle = await open(filePath, 'a+');
|
|
7
|
+
try {
|
|
8
|
+
const release = await lockfile.lock(filePath, {
|
|
9
|
+
realpath: false,
|
|
10
|
+
retries: {
|
|
11
|
+
retries: 3,
|
|
12
|
+
factor: 1,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
try {
|
|
16
|
+
return await run();
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await release();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await handle.close();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { ensureParentDir } from './fs.js';
|
|
3
|
+
import { resolveLogPath } from './paths.js';
|
|
4
|
+
export async function createLogger(globalRoot) {
|
|
5
|
+
const logPath = resolveLogPath(globalRoot);
|
|
6
|
+
await ensureParentDir(logPath);
|
|
7
|
+
return pino(pino.destination(logPath));
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const sensitiveKeyPattern = /(_TOKEN|_KEY|_SECRET|_PASSWORD)$/i;
|
|
2
|
+
export function isSensitiveKey(key) {
|
|
3
|
+
return sensitiveKeyPattern.test(key);
|
|
4
|
+
}
|
|
5
|
+
export function maskValue(key, value) {
|
|
6
|
+
if (!isSensitiveKey(key)) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
if (value.length <= 8) {
|
|
10
|
+
return '*'.repeat(value.length);
|
|
11
|
+
}
|
|
12
|
+
return `${value.slice(0, 9)}********`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
export function resolveGlobalRoot(globalRoot) {
|
|
3
|
+
return globalRoot ?? join(process.env.HOME ?? process.cwd(), '.cc-env');
|
|
4
|
+
}
|
|
5
|
+
export function resolveClaudeSettingsPath(homeDir = process.env.HOME ?? process.cwd()) {
|
|
6
|
+
return join(homeDir, '.claude', 'settings.json');
|
|
7
|
+
}
|
|
8
|
+
export function resolveClaudeSettingsLocalPath(homeDir = process.env.HOME ?? process.cwd()) {
|
|
9
|
+
return join(homeDir, '.claude', 'settings.local.json');
|
|
10
|
+
}
|
|
11
|
+
export function resolveProjectSettingsPath(cwd = process.cwd()) {
|
|
12
|
+
return join(cwd, '.claude', 'settings.json');
|
|
13
|
+
}
|
|
14
|
+
export function resolveProjectSettingsLocalPath(cwd = process.cwd()) {
|
|
15
|
+
return join(cwd, '.claude', 'settings.local.json');
|
|
16
|
+
}
|
|
17
|
+
export function resolveShellConfigPaths(homeDir = process.env.HOME ?? process.cwd()) {
|
|
18
|
+
return {
|
|
19
|
+
zsh: join(homeDir, '.zshrc'),
|
|
20
|
+
bash: join(homeDir, '.bashrc'),
|
|
21
|
+
fish: join(homeDir, '.config', 'fish', 'config.fish'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function resolvePresetPath(globalRoot, name) {
|
|
25
|
+
return join(globalRoot, 'presets', `${name}.json`);
|
|
26
|
+
}
|
|
27
|
+
export function resolveHistoryPath(globalRoot, timestamp) {
|
|
28
|
+
return join(globalRoot, 'history', `${timestamp.replaceAll(':', '-')}.json`);
|
|
29
|
+
}
|
|
30
|
+
export function resolveLogPath(globalRoot) {
|
|
31
|
+
return join(globalRoot, 'logs', 'app.log');
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const envKeySchema = z.string().regex(/^[A-Z0-9_]+$/);
|
|
3
|
+
export const envMapSchema = z.record(envKeySchema, z.unknown()
|
|
4
|
+
.refine((value) => value === null || typeof value !== 'object')
|
|
5
|
+
.transform((value) => String(value)));
|
|
6
|
+
export const presetSchema = z.object({
|
|
7
|
+
name: z.string(),
|
|
8
|
+
createdAt: z.string().datetime({ offset: true }),
|
|
9
|
+
updatedAt: z.string().datetime({ offset: true }),
|
|
10
|
+
env: envMapSchema,
|
|
11
|
+
});
|
|
12
|
+
const shellWriteSchema = z.object({
|
|
13
|
+
shell: z.enum(['zsh', 'bash', 'fish']),
|
|
14
|
+
filePath: z.string(),
|
|
15
|
+
env: envMapSchema,
|
|
16
|
+
});
|
|
17
|
+
const sourceEntrySchema = z.object({
|
|
18
|
+
file: z.string(),
|
|
19
|
+
backup: envMapSchema,
|
|
20
|
+
});
|
|
21
|
+
const initHistorySchema = z.object({
|
|
22
|
+
timestamp: z.string().datetime({ offset: true }),
|
|
23
|
+
action: z.literal('init'),
|
|
24
|
+
migratedKeys: z.array(envKeySchema),
|
|
25
|
+
sources: z.array(sourceEntrySchema),
|
|
26
|
+
shellWrites: z.array(shellWriteSchema),
|
|
27
|
+
});
|
|
28
|
+
const restoreHistorySchema = z.object({
|
|
29
|
+
timestamp: z.string().datetime({ offset: true }),
|
|
30
|
+
action: z.literal('restore'),
|
|
31
|
+
backup: envMapSchema,
|
|
32
|
+
targetType: z.enum(['settings', 'preset']),
|
|
33
|
+
targetName: z.string(),
|
|
34
|
+
});
|
|
35
|
+
export const historySchema = z.discriminatedUnion('action', [
|
|
36
|
+
initHistorySchema,
|
|
37
|
+
restoreHistorySchema,
|
|
38
|
+
]);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
2
|
+
import { CliError } from './errors.js';
|
|
3
|
+
export function spawnCommand(command, args, env) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = spawn(command, args, {
|
|
6
|
+
env,
|
|
7
|
+
stdio: 'inherit',
|
|
8
|
+
});
|
|
9
|
+
child.once('error', reject);
|
|
10
|
+
child.once('close', (exitCode, signal) => {
|
|
11
|
+
if (signal) {
|
|
12
|
+
reject(new CliError(`Command terminated by signal ${signal}`));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (exitCode === null) {
|
|
16
|
+
reject(new CliError('Command terminated without an exit code'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (exitCode !== 0) {
|
|
20
|
+
reject(new CliError(`Command exited with code ${exitCode}`, exitCode));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
resolve();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function createInitFlowState(availableKeys, requiredKeys) {
|
|
2
|
+
return {
|
|
3
|
+
step: 'keys',
|
|
4
|
+
availableKeys,
|
|
5
|
+
requiredKeys,
|
|
6
|
+
selectedKeys: requiredKeys,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function advanceInitFlow(state, action) {
|
|
10
|
+
if (state.step === 'keys' && action.type === 'toggle-key') {
|
|
11
|
+
if (state.requiredKeys.includes(action.key)) {
|
|
12
|
+
return state;
|
|
13
|
+
}
|
|
14
|
+
const selectedKeys = state.selectedKeys.includes(action.key)
|
|
15
|
+
? state.selectedKeys.filter((key) => key !== action.key)
|
|
16
|
+
: [...state.selectedKeys, action.key];
|
|
17
|
+
return {
|
|
18
|
+
...state,
|
|
19
|
+
selectedKeys,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (state.step === 'keys' && action.type === 'continue') {
|
|
23
|
+
return {
|
|
24
|
+
...state,
|
|
25
|
+
step: 'confirm',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (state.step === 'confirm' && action.type === 'confirm') {
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
step: 'done',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export function createPresetCreateFlowState() {
|
|
2
|
+
return {
|
|
3
|
+
step: 'source',
|
|
4
|
+
env: {},
|
|
5
|
+
allKeys: [],
|
|
6
|
+
selectedKeys: [],
|
|
7
|
+
presetName: '',
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function advancePresetCreateFlow(state, action) {
|
|
11
|
+
switch (state.step) {
|
|
12
|
+
case 'source':
|
|
13
|
+
if (action.type !== 'select-source')
|
|
14
|
+
return state;
|
|
15
|
+
return {
|
|
16
|
+
...state,
|
|
17
|
+
step: action.source === 'file' ? 'filePath' : 'manualInput',
|
|
18
|
+
source: action.source,
|
|
19
|
+
};
|
|
20
|
+
case 'filePath':
|
|
21
|
+
if (action.type === 'set-error') {
|
|
22
|
+
return { ...state, error: action.error };
|
|
23
|
+
}
|
|
24
|
+
if (action.type !== 'set-file-path')
|
|
25
|
+
return state;
|
|
26
|
+
return {
|
|
27
|
+
...state,
|
|
28
|
+
step: 'keys',
|
|
29
|
+
filePath: action.filePath,
|
|
30
|
+
error: undefined,
|
|
31
|
+
};
|
|
32
|
+
case 'keys':
|
|
33
|
+
if (action.type !== 'select-keys')
|
|
34
|
+
return state;
|
|
35
|
+
return {
|
|
36
|
+
...state,
|
|
37
|
+
step: 'name',
|
|
38
|
+
selectedKeys: action.keys,
|
|
39
|
+
env: action.env,
|
|
40
|
+
};
|
|
41
|
+
case 'manualInput':
|
|
42
|
+
if (action.type === 'add-manual-pair') {
|
|
43
|
+
const hasKey = state.selectedKeys.includes(action.key);
|
|
44
|
+
return {
|
|
45
|
+
...state,
|
|
46
|
+
env: { ...state.env, [action.key]: action.value },
|
|
47
|
+
selectedKeys: hasKey ? state.selectedKeys : [...state.selectedKeys, action.key],
|
|
48
|
+
error: undefined,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (action.type === 'set-error') {
|
|
52
|
+
return { ...state, error: action.error };
|
|
53
|
+
}
|
|
54
|
+
if (action.type !== 'finish-manual-input')
|
|
55
|
+
return state;
|
|
56
|
+
return { ...state, step: 'name' };
|
|
57
|
+
case 'name':
|
|
58
|
+
if (action.type !== 'set-name')
|
|
59
|
+
return state;
|
|
60
|
+
return {
|
|
61
|
+
...state,
|
|
62
|
+
step: 'destination',
|
|
63
|
+
presetName: action.name,
|
|
64
|
+
};
|
|
65
|
+
case 'destination':
|
|
66
|
+
if (action.type !== 'select-destination')
|
|
67
|
+
return state;
|
|
68
|
+
return {
|
|
69
|
+
...state,
|
|
70
|
+
step: 'confirm',
|
|
71
|
+
destination: action.destination,
|
|
72
|
+
};
|
|
73
|
+
case 'confirm':
|
|
74
|
+
if (action.type !== 'confirm')
|
|
75
|
+
return state;
|
|
76
|
+
return { ...state, step: 'done' };
|
|
77
|
+
case 'done':
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function createRestoreFlowState(records) {
|
|
2
|
+
return {
|
|
3
|
+
step: 'record',
|
|
4
|
+
records,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function advanceRestoreFlow(state, action) {
|
|
8
|
+
switch (state.step) {
|
|
9
|
+
case 'record': {
|
|
10
|
+
if (action.type !== 'select-record') {
|
|
11
|
+
return state;
|
|
12
|
+
}
|
|
13
|
+
const selectedRecord = state.records.find((record) => record.timestamp === action.timestamp);
|
|
14
|
+
if (!selectedRecord) {
|
|
15
|
+
return state;
|
|
16
|
+
}
|
|
17
|
+
if (selectedRecord.action === 'init') {
|
|
18
|
+
return {
|
|
19
|
+
...state,
|
|
20
|
+
step: 'confirm',
|
|
21
|
+
selectedTimestamp: action.timestamp,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
...state,
|
|
26
|
+
step: 'target',
|
|
27
|
+
selectedTimestamp: action.timestamp,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
case 'target':
|
|
31
|
+
if (action.type !== 'select-target' || !state.selectedTimestamp) {
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
if (action.targetType === 'preset' && !action.targetName) {
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
if (action.targetType === 'settings') {
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
step: 'confirm',
|
|
41
|
+
targetType: 'settings',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const targetName = action.targetName;
|
|
45
|
+
return {
|
|
46
|
+
...state,
|
|
47
|
+
step: 'confirm',
|
|
48
|
+
targetType: 'preset',
|
|
49
|
+
targetName,
|
|
50
|
+
};
|
|
51
|
+
case 'confirm':
|
|
52
|
+
if (action.type !== 'confirm' || !state.selectedTimestamp) {
|
|
53
|
+
return state;
|
|
54
|
+
}
|
|
55
|
+
const selectedRecord = state.records.find((record) => record.timestamp === state.selectedTimestamp);
|
|
56
|
+
if (selectedRecord?.action === 'init') {
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
step: 'done',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!state.targetType) {
|
|
63
|
+
return state;
|
|
64
|
+
}
|
|
65
|
+
if (state.targetType === 'preset' && !state.targetName) {
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
...state,
|
|
70
|
+
step: 'done',
|
|
71
|
+
};
|
|
72
|
+
case 'done':
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import { advanceInitFlow, createInitFlowState, } from '../flows/init-flow.js';
|
|
5
|
+
export function InitApp({ keys = [], requiredKeys = [], sourceFiles = [], onSubmit, }) {
|
|
6
|
+
const { exit } = useApp();
|
|
7
|
+
const [cursor, setCursor] = useState(0);
|
|
8
|
+
const [flowState, setFlowState] = useState(() => createInitFlowState(keys, requiredKeys));
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!onSubmit) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (keys.length === 0) {
|
|
14
|
+
onSubmit({ confirmed: false, selectedKeys: [] });
|
|
15
|
+
exit();
|
|
16
|
+
}
|
|
17
|
+
}, [exit, keys.length, onSubmit]);
|
|
18
|
+
useInput((input, key) => {
|
|
19
|
+
if (!onSubmit) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (key.upArrow || input === 'k') {
|
|
23
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (key.downArrow || input === 'j') {
|
|
27
|
+
setCursor((c) => Math.min(keys.length - 1, c + 1));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (input === ' ') {
|
|
31
|
+
const targetKey = keys[cursor];
|
|
32
|
+
if (targetKey) {
|
|
33
|
+
setFlowState((prev) => advanceInitFlow(prev, { type: 'toggle-key', key: targetKey }));
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (key.return) {
|
|
38
|
+
onSubmit({ confirmed: true, selectedKeys: flowState.selectedKeys });
|
|
39
|
+
exit();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.escape || input.toLowerCase() === 'q') {
|
|
43
|
+
onSubmit({ confirmed: false, selectedKeys: [] });
|
|
44
|
+
exit();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Select env keys to migrate into managed shell config" }), sourceFiles.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Source:" }), sourceFiles.map((file) => (_jsxs(Text, { color: "cyan", children: [" ", file] }, file)))] })) : null, _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u00B7 space toggle \u00B7 enter confirm \u00B7 q cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: keys.map((key, i) => {
|
|
48
|
+
const isRequired = requiredKeys.includes(key);
|
|
49
|
+
const isSelected = flowState.selectedKeys.includes(key);
|
|
50
|
+
const isCursor = i === cursor;
|
|
51
|
+
const checkbox = isSelected ? '[x]' : '[ ]';
|
|
52
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: isCursor ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? 'green' : '', children: checkbox }), _jsxs(Text, { children: [" ", key] }), isRequired ? _jsx(Text, { dimColor: true, children: " (required)" }) : null] }, key));
|
|
53
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [flowState.selectedKeys.length, " of ", keys.length, " selected"] }) })] }));
|
|
54
|
+
}
|