@athsra/cli 1.0.4 → 1.1.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.
- package/README.md +34 -10
- package/package.json +3 -3
- package/src/commands/delete.ts +16 -13
- package/src/commands/get.ts +8 -5
- package/src/commands/handoff.ts +13 -3
- package/src/commands/login.ts +208 -61
- package/src/commands/logout.ts +3 -2
- package/src/commands/ls.ts +32 -10
- package/src/commands/manifest.ts +2 -2
- package/src/commands/mcp.ts +259 -7
- package/src/commands/migrate-envelopes.ts +55 -3
- package/src/commands/purge.ts +13 -10
- package/src/commands/restore.ts +10 -6
- package/src/commands/rollback.ts +12 -9
- package/src/commands/rotate-master.ts +13 -13
- package/src/commands/run.ts +6 -24
- package/src/commands/service-token.ts +15 -31
- package/src/commands/set.ts +7 -6
- package/src/commands/unset.ts +11 -8
- package/src/commands/versions.ts +7 -5
- package/src/index.ts +12 -8
- package/src/lib/auth-context.ts +77 -13
- package/src/lib/auth-proof.ts +26 -0
- package/src/lib/auto-project.ts +58 -14
- package/src/lib/client.ts +112 -19
- package/src/lib/config.ts +2 -0
- package/src/lib/device-login.ts +157 -0
- package/src/lib/env-format.ts +1 -1
- package/src/lib/envelope.ts +105 -15
- package/src/lib/identity-key.ts +21 -0
- package/src/lib/keyring.ts +25 -0
- package/src/lib/mcp-register.ts +223 -0
- package/src/lib/mcp-tools/admin.ts +267 -0
- package/src/lib/mcp-tools/args.ts +26 -0
- package/src/lib/mcp-tools/confirm.ts +21 -0
- package/src/lib/mcp-tools/defs.ts +388 -3
- package/src/lib/mcp-tools/login.ts +156 -0
- package/src/lib/mcp-tools/mask.ts +41 -0
- package/src/lib/mcp-tools/read.ts +115 -1
- package/src/lib/mcp-tools/result.ts +5 -5
- package/src/lib/mcp-tools/run.ts +101 -0
- package/src/lib/mcp-tools/write.ts +84 -5
- package/src/lib/oidc-flow.ts +43 -1
- package/src/lib/org-rewrap.ts +9 -3
- package/src/lib/service-tokens.ts +62 -0
package/src/commands/ls.ts
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import { loadAuthContext } from '../lib/auth-context.ts';
|
|
2
|
+
import { configTag, projectRef, resolveProject } from '../lib/auto-project.ts';
|
|
2
3
|
import { partitionEnv } from '../lib/env-format.ts';
|
|
3
4
|
import { readPlain } from '../lib/envelope.ts';
|
|
4
5
|
|
|
6
|
+
/** ls 자체 flag — project/config 해석 전에 args 에서 제거 (positional 오인 방지). */
|
|
7
|
+
const LS_FLAGS = ['--all', '--include-deleted', '--configs'];
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* athsra ls — active projects only
|
|
7
11
|
* athsra ls --all — active + soft-deleted (deleted 표시)
|
|
8
12
|
* athsra ls --include-deleted — alias of --all
|
|
9
|
-
* athsra ls <project>
|
|
13
|
+
* athsra ls <project>[:<env>] — keys of a project/environment (decrypt 필요)
|
|
14
|
+
* athsra ls <project> --configs — environments(config) of a project + active/deleted 상태
|
|
10
15
|
*/
|
|
11
16
|
export async function lsCmd(args: string[]): Promise<number> {
|
|
12
17
|
const ctx = await loadAuthContext();
|
|
13
18
|
if (!ctx) return 1;
|
|
14
19
|
const client = ctx.client;
|
|
15
20
|
|
|
16
|
-
const
|
|
21
|
+
const wantConfigs = args.includes('--configs');
|
|
17
22
|
const includeDeleted = args.includes('--all') || args.includes('--include-deleted');
|
|
23
|
+
const cleaned = args.filter((a) => !LS_FLAGS.includes(a));
|
|
24
|
+
const { project, config } = resolveProject(cleaned, { requirePositional: true });
|
|
18
25
|
|
|
19
26
|
// ls — project 목록
|
|
20
|
-
if (
|
|
27
|
+
if (!project) {
|
|
21
28
|
const result = await client.listProjectsExtended({ includeDeleted });
|
|
22
29
|
if (result.count === 0) {
|
|
23
30
|
console.log('(no projects yet — run `athsra set <project> KEY=value`)');
|
|
@@ -37,30 +44,45 @@ export async function lsCmd(args: string[]): Promise<number> {
|
|
|
37
44
|
return 0;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
// ls <project> —
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
// ls <project> --configs — 환경(config) 목록 + 각 active/deleted 상태 (default 는 * 표시)
|
|
48
|
+
if (wantConfigs) {
|
|
49
|
+
const result = await client.listConfigs(project);
|
|
50
|
+
if (result.count === 0) {
|
|
51
|
+
console.log(`(no environments for ${project})`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
const width = Math.max(...result.configs.map((cfg) => cfg.config.length));
|
|
55
|
+
for (const cfg of result.configs) {
|
|
56
|
+
const status = cfg.deleted ? '(deleted)' : cfg.active ? 'active' : '(empty)';
|
|
57
|
+
const star = cfg.config === 'default' ? '*' : ' ';
|
|
58
|
+
console.log(`${star} ${cfg.config.padEnd(width)} ${status}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(`(${result.count} environment${result.count > 1 ? 's' : ''})`);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
43
63
|
|
|
44
|
-
|
|
64
|
+
// ls <project>[:<env>] — key 목록 + 값 상태 ((empty) 또는 길이, decrypt 필요)
|
|
65
|
+
const plain = await readPlain(ctx, project, config);
|
|
45
66
|
if (!plain) {
|
|
46
|
-
console.error(`project not found: ${project}`);
|
|
67
|
+
console.error(`project not found: ${project}${configTag(config)}`);
|
|
47
68
|
return 1;
|
|
48
69
|
}
|
|
49
70
|
const keys = Object.keys(plain).sort();
|
|
50
71
|
if (keys.length === 0) {
|
|
51
|
-
console.log(
|
|
72
|
+
console.log(`(no keys${configTag(config)})`);
|
|
52
73
|
return 0;
|
|
53
74
|
}
|
|
54
75
|
const { filled, emptyKeys } = partitionEnv(plain);
|
|
55
76
|
const emptySet = new Set(emptyKeys);
|
|
56
77
|
const width = Math.max(...keys.map((k) => k.length));
|
|
78
|
+
if (config !== 'default') console.log(`# config: ${config}`);
|
|
57
79
|
for (const k of keys) {
|
|
58
80
|
const status = emptySet.has(k) ? '(empty)' : `${(filled[k] ?? '').length} chars`;
|
|
59
81
|
console.log(`${k.padEnd(width)} ${status}`);
|
|
60
82
|
}
|
|
61
83
|
if (emptyKeys.length > 0) {
|
|
62
84
|
console.error(
|
|
63
|
-
`\n${emptyKeys.length} of ${keys.length} key${keys.length > 1 ? 's' : ''} empty — \`athsra run\` skips empty keys (parent env used). Set values: \`athsra set ${project} KEY=value\``,
|
|
85
|
+
`\n${emptyKeys.length} of ${keys.length} key${keys.length > 1 ? 's' : ''} empty — \`athsra run\` skips empty keys (parent env used). Set values: \`athsra set ${projectRef(project, config)} KEY=value\``,
|
|
64
86
|
);
|
|
65
87
|
}
|
|
66
88
|
return 0;
|
package/src/commands/manifest.ts
CHANGED
|
@@ -195,7 +195,7 @@ async function cmdInit(flags: ParsedFlags): Promise<number> {
|
|
|
195
195
|
return 2;
|
|
196
196
|
}
|
|
197
197
|
const ctx = await loadAuthContext();
|
|
198
|
-
if (
|
|
198
|
+
if (ctx?.kind !== 'user') {
|
|
199
199
|
console.error('✗ user token (master pw) 필요. service token 거부.');
|
|
200
200
|
return 1;
|
|
201
201
|
}
|
|
@@ -299,7 +299,7 @@ async function cmdValidate(flags: ParsedFlags): Promise<number> {
|
|
|
299
299
|
return 0;
|
|
300
300
|
}
|
|
301
301
|
const ctx = await loadAuthContext();
|
|
302
|
-
if (
|
|
302
|
+
if (ctx?.kind !== 'user') {
|
|
303
303
|
console.error('✗ envelope diff 위해 user token (master pw) 필요');
|
|
304
304
|
return 1;
|
|
305
305
|
}
|
package/src/commands/mcp.ts
CHANGED
|
@@ -35,15 +35,47 @@
|
|
|
35
35
|
* 모든 로그/에러는 console.error (stderr) 로.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
39
|
+
import { dirname, resolve } from 'node:path';
|
|
38
40
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
39
41
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
40
42
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
41
|
-
import { READ_TOOLS, type ToolDef, WRITE_TOOLS } from '../lib/mcp-tools/defs.ts';
|
|
42
43
|
import {
|
|
44
|
+
buildEntry,
|
|
45
|
+
freshFileContent,
|
|
46
|
+
type McpClient,
|
|
47
|
+
type McpScope,
|
|
48
|
+
resolveTarget,
|
|
49
|
+
upsertServerEntry,
|
|
50
|
+
} from '../lib/mcp-register.ts';
|
|
51
|
+
import {
|
|
52
|
+
handleOrgInvite,
|
|
53
|
+
handleOrgRemoveMember,
|
|
54
|
+
handleProjectShare,
|
|
55
|
+
handleProjectUnshare,
|
|
56
|
+
handlePurge,
|
|
57
|
+
handleServiceTokenCreate,
|
|
58
|
+
handleServiceTokenRevoke,
|
|
59
|
+
} from '../lib/mcp-tools/admin.ts';
|
|
60
|
+
import {
|
|
61
|
+
ADMIN_TOOLS,
|
|
62
|
+
READ_TOOLS,
|
|
63
|
+
type ToolDef,
|
|
64
|
+
VALUE_TOOLS,
|
|
65
|
+
WRITE_TOOLS,
|
|
66
|
+
} from '../lib/mcp-tools/defs.ts';
|
|
67
|
+
import { handleLoginStart, handleLoginStatus } from '../lib/mcp-tools/login.ts';
|
|
68
|
+
import {
|
|
69
|
+
handleAudit,
|
|
70
|
+
handleDoctor,
|
|
43
71
|
handleGetProjectKeys,
|
|
72
|
+
handleGetSecretValue,
|
|
73
|
+
handleListOrgs,
|
|
44
74
|
handleListProjects,
|
|
75
|
+
handleOrgInfo,
|
|
45
76
|
handleShowManifest,
|
|
46
77
|
handleValidateManifest,
|
|
78
|
+
handleVersions,
|
|
47
79
|
handleWhoami,
|
|
48
80
|
} from '../lib/mcp-tools/read.ts';
|
|
49
81
|
import {
|
|
@@ -52,18 +84,26 @@ import {
|
|
|
52
84
|
notLoggedIn,
|
|
53
85
|
type ToolTextResult,
|
|
54
86
|
} from '../lib/mcp-tools/result.ts';
|
|
87
|
+
import { handleRun } from '../lib/mcp-tools/run.ts';
|
|
55
88
|
import {
|
|
89
|
+
handleBulkSet,
|
|
90
|
+
handleDeleteProject,
|
|
56
91
|
handleManifestInit,
|
|
57
92
|
handleManifestModify,
|
|
93
|
+
handleRestoreProject,
|
|
94
|
+
handleRollback,
|
|
58
95
|
handleSetSecret,
|
|
59
96
|
handleUnsetSecret,
|
|
60
97
|
} from '../lib/mcp-tools/write.ts';
|
|
61
98
|
|
|
62
99
|
const USAGE = [
|
|
63
100
|
'usage: athsra mcp [--help]',
|
|
101
|
+
' or: athsra mcp install [--target <dir>] [--client claude|cursor|vscode]',
|
|
102
|
+
' [--scope project|user] [--write] [--admin] [--read-values] [--apply]',
|
|
64
103
|
'',
|
|
65
104
|
'Model Context Protocol stdio server — Claude Code / MCP client 가 envelope',
|
|
66
105
|
'을 도구로 사용. stdio JSON-RPC. 다른 출력 X (logs → stderr).',
|
|
106
|
+
'install: MCP client 설정에 athsra 등록 (dry-run 기본 — `athsra mcp install --help`).',
|
|
67
107
|
'',
|
|
68
108
|
'Claude Code 등록 예시 (~/.claude/settings.json or .mcp.json):',
|
|
69
109
|
' "mcpServers": {',
|
|
@@ -71,34 +111,67 @@ const USAGE = [
|
|
|
71
111
|
' }',
|
|
72
112
|
'',
|
|
73
113
|
'Tools (read-only, 항상 노출):',
|
|
74
|
-
' athsra_whoami — 인증 상태 +
|
|
114
|
+
' athsra_whoami — 인증 상태 + 신원. 온보딩 시 먼저 호출',
|
|
115
|
+
' athsra_login_start/status — in-chat 브라우저 로그인 (터미널 불필요, device_code 무노출)',
|
|
75
116
|
' athsra_list_projects — envelope 이름 목록',
|
|
76
117
|
' athsra_get_project_keys — envelope 키 목록 (값 X — 값은 `athsra run` 으로 주입)',
|
|
77
118
|
' athsra_show_manifest — sibling worker manifest 조회',
|
|
78
119
|
' athsra_validate_manifest — envelope vs manifest diff',
|
|
79
120
|
'Tools (write, ATHSRA_MCP_WRITE=1 opt-in):',
|
|
80
121
|
' athsra_set_secret / athsra_unset_secret / athsra_manifest_init / athsra_manifest_modify',
|
|
122
|
+
' athsra_rollback / athsra_delete_project / athsra_restore_project / athsra_bulk_set',
|
|
123
|
+
'Tools (value, ATHSRA_MCP_READ_VALUES=1 opt-in — 평문이 응답 경계를 넘음):',
|
|
124
|
+
' athsra_get_secret_value (기본 마스킹; full=true+confirm 시 평문) / athsra_run (값 미노출 주입 실행)',
|
|
125
|
+
'Tools (admin, ATHSRA_MCP_ADMIN=1 opt-in — destructive 는 confirm 정확일치):',
|
|
126
|
+
' athsra_org_invite / athsra_org_remove_member / athsra_project_share / athsra_project_unshare',
|
|
127
|
+
' athsra_service_token_create / athsra_service_token_revoke / athsra_purge',
|
|
81
128
|
].join('\n');
|
|
82
129
|
|
|
83
|
-
const VERSION = '1.
|
|
130
|
+
const VERSION = '1.1.0';
|
|
84
131
|
|
|
85
|
-
/** Write tools opt-in via env var. 기본 false — secret
|
|
132
|
+
/** Write/admin tools opt-in via env var. 기본 false — secret 변경·고위험은 명시적 ack 필요. */
|
|
86
133
|
const WRITE_ENABLED = process.env.ATHSRA_MCP_WRITE === '1';
|
|
134
|
+
const ADMIN_ENABLED = process.env.ATHSRA_MCP_ADMIN === '1';
|
|
135
|
+
/** value tier — secret 평문 노출(get_secret_value full)·주입 실행(run). write(변경)와 직교. */
|
|
136
|
+
const VALUE_ENABLED = process.env.ATHSRA_MCP_READ_VALUES === '1';
|
|
87
137
|
|
|
88
|
-
const TOOLS: ToolDef[] =
|
|
138
|
+
const TOOLS: ToolDef[] = [
|
|
139
|
+
...READ_TOOLS,
|
|
140
|
+
...(WRITE_ENABLED ? WRITE_TOOLS : []),
|
|
141
|
+
...(ADMIN_ENABLED ? ADMIN_TOOLS : []),
|
|
142
|
+
...(VALUE_ENABLED ? VALUE_TOOLS : []),
|
|
143
|
+
];
|
|
89
144
|
|
|
90
145
|
const WRITE_TOOL_NAMES = new Set(WRITE_TOOLS.map((t) => t.name));
|
|
146
|
+
const ADMIN_TOOL_NAMES = new Set(ADMIN_TOOLS.map((t) => t.name));
|
|
147
|
+
const VALUE_TOOL_NAMES = new Set(VALUE_TOOLS.map((t) => t.name));
|
|
91
148
|
|
|
92
149
|
async function dispatch(
|
|
93
150
|
name: string,
|
|
94
151
|
args: Record<string, unknown> | undefined,
|
|
95
|
-
|
|
152
|
+
gatesIn: Partial<{ write: boolean; admin: boolean; value: boolean }> = {},
|
|
96
153
|
): Promise<ToolTextResult> {
|
|
97
|
-
|
|
154
|
+
const gates = {
|
|
155
|
+
write: gatesIn.write ?? WRITE_ENABLED,
|
|
156
|
+
admin: gatesIn.admin ?? ADMIN_ENABLED,
|
|
157
|
+
value: gatesIn.value ?? VALUE_ENABLED,
|
|
158
|
+
};
|
|
159
|
+
if (WRITE_TOOL_NAMES.has(name) && !gates.write) {
|
|
98
160
|
return jsonError(
|
|
99
161
|
`write tool '${name}' disabled. Set ATHSRA_MCP_WRITE=1 in the MCP server env to enable.`,
|
|
100
162
|
);
|
|
101
163
|
}
|
|
164
|
+
if (ADMIN_TOOL_NAMES.has(name) && !gates.admin) {
|
|
165
|
+
return jsonError(
|
|
166
|
+
`admin tool '${name}' disabled. Set ATHSRA_MCP_ADMIN=1 in the MCP server env to enable.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (VALUE_TOOL_NAMES.has(name) && !gates.value) {
|
|
170
|
+
return jsonError(
|
|
171
|
+
`value tool '${name}' disabled. Set ATHSRA_MCP_READ_VALUES=1 in the MCP server env to ` +
|
|
172
|
+
'enable secret value reveal / run injection (plaintext crosses the response boundary — opt-in only).',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
102
175
|
try {
|
|
103
176
|
switch (name) {
|
|
104
177
|
case 'athsra_whoami':
|
|
@@ -107,10 +180,28 @@ async function dispatch(
|
|
|
107
180
|
return await handleListProjects(args);
|
|
108
181
|
case 'athsra_get_project_keys':
|
|
109
182
|
return await handleGetProjectKeys(args);
|
|
183
|
+
case 'athsra_get_secret_value':
|
|
184
|
+
return await handleGetSecretValue(args);
|
|
185
|
+
case 'athsra_run':
|
|
186
|
+
return await handleRun(args);
|
|
110
187
|
case 'athsra_show_manifest':
|
|
111
188
|
return handleShowManifest(args);
|
|
112
189
|
case 'athsra_validate_manifest':
|
|
113
190
|
return await handleValidateManifest(args);
|
|
191
|
+
case 'athsra_versions':
|
|
192
|
+
return await handleVersions(args);
|
|
193
|
+
case 'athsra_audit':
|
|
194
|
+
return await handleAudit(args);
|
|
195
|
+
case 'athsra_list_orgs':
|
|
196
|
+
return await handleListOrgs();
|
|
197
|
+
case 'athsra_org_info':
|
|
198
|
+
return await handleOrgInfo();
|
|
199
|
+
case 'athsra_doctor':
|
|
200
|
+
return await handleDoctor();
|
|
201
|
+
case 'athsra_login_start':
|
|
202
|
+
return await handleLoginStart();
|
|
203
|
+
case 'athsra_login_status':
|
|
204
|
+
return await handleLoginStatus();
|
|
114
205
|
case 'athsra_set_secret':
|
|
115
206
|
return await handleSetSecret(args);
|
|
116
207
|
case 'athsra_unset_secret':
|
|
@@ -119,6 +210,28 @@ async function dispatch(
|
|
|
119
210
|
return handleManifestInit(args);
|
|
120
211
|
case 'athsra_manifest_modify':
|
|
121
212
|
return handleManifestModify(args);
|
|
213
|
+
case 'athsra_rollback':
|
|
214
|
+
return await handleRollback(args);
|
|
215
|
+
case 'athsra_delete_project':
|
|
216
|
+
return await handleDeleteProject(args);
|
|
217
|
+
case 'athsra_restore_project':
|
|
218
|
+
return await handleRestoreProject(args);
|
|
219
|
+
case 'athsra_bulk_set':
|
|
220
|
+
return await handleBulkSet(args);
|
|
221
|
+
case 'athsra_org_invite':
|
|
222
|
+
return await handleOrgInvite(args);
|
|
223
|
+
case 'athsra_org_remove_member':
|
|
224
|
+
return await handleOrgRemoveMember(args);
|
|
225
|
+
case 'athsra_project_share':
|
|
226
|
+
return await handleProjectShare(args);
|
|
227
|
+
case 'athsra_project_unshare':
|
|
228
|
+
return await handleProjectUnshare(args);
|
|
229
|
+
case 'athsra_service_token_create':
|
|
230
|
+
return await handleServiceTokenCreate(args);
|
|
231
|
+
case 'athsra_service_token_revoke':
|
|
232
|
+
return await handleServiceTokenRevoke(args);
|
|
233
|
+
case 'athsra_purge':
|
|
234
|
+
return await handlePurge(args);
|
|
122
235
|
default:
|
|
123
236
|
return jsonError(`unknown tool: ${name}`);
|
|
124
237
|
}
|
|
@@ -145,7 +258,142 @@ function buildServer(): Server {
|
|
|
145
258
|
return server;
|
|
146
259
|
}
|
|
147
260
|
|
|
261
|
+
const INSTALL_USAGE = [
|
|
262
|
+
'usage: athsra mcp install [--target <dir>] [--client claude|cursor|vscode]',
|
|
263
|
+
' [--scope project|user] [--write] [--admin] [--read-values] [--apply]',
|
|
264
|
+
'',
|
|
265
|
+
'MCP client 설정 파일에 athsra server 를 등록/갱신 (upsert). dry-run 기본 — 변경은 --apply.',
|
|
266
|
+
'',
|
|
267
|
+
' --target <dir> 대상 repo (기본: cwd)',
|
|
268
|
+
' --client claude(기본) | cursor | vscode',
|
|
269
|
+
' --scope project(기본) | user',
|
|
270
|
+
' claude/project=.mcp.json(없으면 생성) · claude/user=~/.claude.json',
|
|
271
|
+
' cursor=.cursor/mcp.json · vscode=.vscode/mcp.json(servers+type:stdio)',
|
|
272
|
+
' --write/--admin ATHSRA_MCP_WRITE/ADMIN=1 env 동봉 (write/admin tier opt-in)',
|
|
273
|
+
' --read-values ATHSRA_MCP_READ_VALUES=1 env 동봉 (value tier — get_secret_value/run)',
|
|
274
|
+
' --apply 실제 파일 변경 (기본은 계획만 출력)',
|
|
275
|
+
].join('\n');
|
|
276
|
+
|
|
277
|
+
const isClient = (v: string): v is McpClient => v === 'claude' || v === 'cursor' || v === 'vscode';
|
|
278
|
+
const isScope = (v: string): v is McpScope => v === 'project' || v === 'user';
|
|
279
|
+
|
|
280
|
+
/** `athsra mcp install` — Phase 5 B6. MCP client 설정 upsert (lib/mcp-register.ts 코어). */
|
|
281
|
+
export async function installCmd(args: string[]): Promise<number> {
|
|
282
|
+
let targetDir = process.cwd();
|
|
283
|
+
let client: McpClient = 'claude';
|
|
284
|
+
let scope: McpScope = 'project';
|
|
285
|
+
let write = false;
|
|
286
|
+
let admin = false;
|
|
287
|
+
let readValues = false;
|
|
288
|
+
let apply = false;
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < args.length; i++) {
|
|
291
|
+
const a = args[i] ?? '';
|
|
292
|
+
const value = (flag: string): string | null => {
|
|
293
|
+
if (a.startsWith(`${flag}=`)) return a.slice(flag.length + 1);
|
|
294
|
+
if (a === flag) {
|
|
295
|
+
const v = args[i + 1];
|
|
296
|
+
if (v !== undefined) {
|
|
297
|
+
i++;
|
|
298
|
+
return v;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
};
|
|
303
|
+
if (a === '--help' || a === '-h') {
|
|
304
|
+
console.log(INSTALL_USAGE);
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
if (a === '--write') {
|
|
308
|
+
write = true;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (a === '--admin') {
|
|
312
|
+
admin = true;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (a === '--read-values') {
|
|
316
|
+
readValues = true;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (a === '--apply') {
|
|
320
|
+
apply = true;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const target = value('--target') ?? value('-t');
|
|
324
|
+
if (target !== null) {
|
|
325
|
+
targetDir = resolve(target);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const c = value('--client');
|
|
329
|
+
if (c !== null) {
|
|
330
|
+
if (!isClient(c)) {
|
|
331
|
+
console.error(`✗ --client 는 claude|cursor|vscode 중 하나: ${c}`);
|
|
332
|
+
return 2;
|
|
333
|
+
}
|
|
334
|
+
client = c;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
const s = value('--scope');
|
|
338
|
+
if (s !== null) {
|
|
339
|
+
if (!isScope(s)) {
|
|
340
|
+
console.error(`✗ --scope 는 project|user 중 하나: ${s}`);
|
|
341
|
+
return 2;
|
|
342
|
+
}
|
|
343
|
+
scope = s;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
console.error(`✗ unknown flag: ${a}`);
|
|
347
|
+
console.error(INSTALL_USAGE);
|
|
348
|
+
return 2;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const resolved = resolveTarget(client, scope, targetDir);
|
|
352
|
+
if ('error' in resolved) {
|
|
353
|
+
console.error(`✗ ${resolved.error}`);
|
|
354
|
+
return 2;
|
|
355
|
+
}
|
|
356
|
+
const entry = buildEntry(client, { write, admin, readValues });
|
|
357
|
+
const { path, rootKey } = resolved;
|
|
358
|
+
|
|
359
|
+
if (!existsSync(path)) {
|
|
360
|
+
console.log(`${apply ? '✏' : '○'} ${path} (신규 생성 — athsra MCP entry)`);
|
|
361
|
+
if (apply) {
|
|
362
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
363
|
+
writeFileSync(path, freshFileContent(rootKey, entry));
|
|
364
|
+
console.log(' ✓ 적용 완료 — MCP client 재시작 후 athsra_whoami 호출');
|
|
365
|
+
} else {
|
|
366
|
+
console.log(' 적용: --apply');
|
|
367
|
+
}
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const raw = readFileSync(path, 'utf-8');
|
|
372
|
+
let res: ReturnType<typeof upsertServerEntry>;
|
|
373
|
+
try {
|
|
374
|
+
res = upsertServerEntry(raw, rootKey, entry);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
console.error(`✗ ${path} parse 실패: ${(err as Error).message}`);
|
|
377
|
+
return 1;
|
|
378
|
+
}
|
|
379
|
+
if (!res.result.changed) {
|
|
380
|
+
console.log(`= ${path} (${res.result.reason})`);
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
console.log(`${apply ? '✏' : '○'} ${path} (athsra MCP ${res.result.reason})`);
|
|
384
|
+
if (apply && res.output) {
|
|
385
|
+
writeFileSync(path, res.output);
|
|
386
|
+
console.log(' ✓ 적용 완료 — MCP client 재시작 후 athsra_whoami 호출');
|
|
387
|
+
} else {
|
|
388
|
+
console.log(' 적용: --apply');
|
|
389
|
+
}
|
|
390
|
+
return 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
148
393
|
export async function mcpCmd(args: string[]): Promise<number> {
|
|
394
|
+
if (args[0] === 'install') {
|
|
395
|
+
return installCmd(args.slice(1));
|
|
396
|
+
}
|
|
149
397
|
if (args.includes('--help') || args.includes('-h')) {
|
|
150
398
|
console.error(USAGE);
|
|
151
399
|
return 0;
|
|
@@ -175,5 +423,9 @@ export const __test = {
|
|
|
175
423
|
TOOLS,
|
|
176
424
|
READ_TOOLS,
|
|
177
425
|
WRITE_TOOLS,
|
|
426
|
+
ADMIN_TOOLS,
|
|
427
|
+
VALUE_TOOLS,
|
|
178
428
|
WRITE_ENABLED,
|
|
429
|
+
ADMIN_ENABLED,
|
|
430
|
+
VALUE_ENABLED,
|
|
179
431
|
};
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { migrateV1ToV2 } from '@athsra/crypto';
|
|
2
2
|
import { loadAuthContext } from '../lib/auth-context.ts';
|
|
3
|
+
import type { AthsraClient } from '../lib/client.ts';
|
|
4
|
+
import { grantOrgAccess } from '../lib/org-rewrap.ts';
|
|
3
5
|
|
|
4
6
|
const USAGE = [
|
|
5
|
-
'usage: athsra migrate-envelopes [--apply] [--include=p1,p2,...]',
|
|
7
|
+
'usage: athsra migrate-envelopes [--self] [--apply] [--include=p1,p2,...]',
|
|
6
8
|
'',
|
|
7
9
|
'모든 v1 envelope 를 v2 (DEK + master recipient) 로 일괄 마이그레이션.',
|
|
8
10
|
' - default dry-run — read-only survey, 어떤 project 가 마이그레이션될지만 출력.',
|
|
9
11
|
' - --apply — 실제 PUT. 각 project 는 새 version 으로 기록 (versions 보존).',
|
|
10
12
|
' - --include=... — 특정 project 만 (쉼표 구분, 점진 적용용).',
|
|
13
|
+
' - --self — v1→v2 대신, 내 모든 v2 envelope 에 member:self recipient 추가.',
|
|
14
|
+
' identity 로그인 머신(master pw 없음)에서 멤버 경로로 복호 가능해진다.',
|
|
11
15
|
'',
|
|
12
16
|
'service token 추가 전 사전 작업으로 권장 (자동 migrate 도 동작하지만 일괄 처리가 운영',
|
|
13
|
-
'가시성 ↑). 이미 v2 인 project 는 자동 skip — idempotent.',
|
|
17
|
+
'가시성 ↑). 이미 v2/member:self 인 project 는 자동 skip — idempotent.',
|
|
14
18
|
].join('\n');
|
|
15
19
|
|
|
16
20
|
export async function migrateEnvelopesCmd(args: string[]): Promise<number> {
|
|
@@ -20,6 +24,7 @@ export async function migrateEnvelopesCmd(args: string[]): Promise<number> {
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
const apply = args.includes('--apply');
|
|
27
|
+
const self = args.includes('--self');
|
|
23
28
|
let include: Set<string> | null = null;
|
|
24
29
|
for (const a of args) {
|
|
25
30
|
if (a.startsWith('--include=')) {
|
|
@@ -29,7 +34,7 @@ export async function migrateEnvelopesCmd(args: string[]): Promise<number> {
|
|
|
29
34
|
.map((s) => s.trim())
|
|
30
35
|
.filter(Boolean);
|
|
31
36
|
include = new Set(list);
|
|
32
|
-
} else if (a.startsWith('-') && a !== '--apply') {
|
|
37
|
+
} else if (a.startsWith('-') && a !== '--apply' && a !== '--self') {
|
|
33
38
|
console.error(`Unknown flag: ${a}`);
|
|
34
39
|
console.error(USAGE);
|
|
35
40
|
return 2;
|
|
@@ -44,6 +49,10 @@ export async function migrateEnvelopesCmd(args: string[]): Promise<number> {
|
|
|
44
49
|
}
|
|
45
50
|
const { client, masterPw } = ctx;
|
|
46
51
|
|
|
52
|
+
if (self) {
|
|
53
|
+
return migrateSelf(client, masterPw, apply);
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
const projects = (await client.listProjects()).sort();
|
|
48
57
|
const targets = include ? projects.filter((p) => include.has(p)) : projects;
|
|
49
58
|
if (include) {
|
|
@@ -111,3 +120,46 @@ export async function migrateEnvelopesCmd(args: string[]): Promise<number> {
|
|
|
111
120
|
}
|
|
112
121
|
return 0;
|
|
113
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* --self — 내 모든 (v2) 공유 시크릿에 member:self recipient 를 추가 (grantOrgAccess 재사용).
|
|
126
|
+
* identity 로그인 머신(master pw 없음)이 멤버 경로로 복호 가능하게 하는 일괄 전환. 멱등 —
|
|
127
|
+
* 이미 member:self 면 skip. v1 은 먼저 v2 마이그레이션 필요. dry-run 기본(연산·PUT 없이 survey).
|
|
128
|
+
*/
|
|
129
|
+
async function migrateSelf(
|
|
130
|
+
client: AthsraClient,
|
|
131
|
+
masterPw: string,
|
|
132
|
+
apply: boolean,
|
|
133
|
+
): Promise<number> {
|
|
134
|
+
const me = await client.whoami();
|
|
135
|
+
if (me.userId === undefined) {
|
|
136
|
+
console.error('whoami 에 user_id 없음 — 재로그인 필요 (`athsra login`).');
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
console.log(
|
|
140
|
+
`${apply ? 'APPLY' : 'DRY-RUN'} — migrate-envelopes --self (member:${me.userId} recipient)\n`,
|
|
141
|
+
);
|
|
142
|
+
const result = await grantOrgAccess(client, masterPw, me.userId, { dryRun: !apply });
|
|
143
|
+
for (const p of result.granted) {
|
|
144
|
+
console.log(` ${apply ? '✏' : '○'} ${p}: member:self ${apply ? '추가됨' : '추가 예정'}`);
|
|
145
|
+
}
|
|
146
|
+
for (const p of result.skipped) {
|
|
147
|
+
console.log(` = ${p}: 이미 member:self (또는 envelope 없음) — skip`);
|
|
148
|
+
}
|
|
149
|
+
for (const p of result.legacyV1) {
|
|
150
|
+
console.log(` ! ${p}: v1 — 먼저 \`athsra migrate-envelopes --apply\` 필요`);
|
|
151
|
+
}
|
|
152
|
+
for (const f of result.failed) {
|
|
153
|
+
console.log(` ✗ ${f.project}: ${f.error}`);
|
|
154
|
+
}
|
|
155
|
+
console.log(
|
|
156
|
+
`\n요약: granted=${result.granted.length} skipped=${result.skipped.length} ` +
|
|
157
|
+
`legacyV1=${result.legacyV1.length} failed=${result.failed.length}` +
|
|
158
|
+
(apply ? '' : ' (dry-run — 변경 없음)'),
|
|
159
|
+
);
|
|
160
|
+
if (result.failed.length > 0) return 1;
|
|
161
|
+
if (!apply && result.granted.length > 0) {
|
|
162
|
+
console.log('\n적용: athsra migrate-envelopes --self --apply');
|
|
163
|
+
}
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
package/src/commands/purge.ts
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import { loadAuthContext } from '../lib/auth-context.ts';
|
|
2
|
+
import { CONFIG_USAGE_HINT, configTag, resolveProject } from '../lib/auto-project.ts';
|
|
2
3
|
import { promptConfirm } from '../lib/prompt.ts';
|
|
3
4
|
|
|
4
5
|
const USAGE = [
|
|
5
|
-
'usage: athsra purge <project> # hard-delete (모든 versions 영구 제거)',
|
|
6
|
-
' or: athsra purge <project> --yes # confirm 우회 (CI 용)',
|
|
6
|
+
'usage: athsra purge <project>[:<env>] # hard-delete (모든 versions 영구 제거)',
|
|
7
|
+
' or: athsra purge <project>[:<env>] --yes # confirm 우회 (CI 용)',
|
|
7
8
|
'',
|
|
8
9
|
'note: athsra delete <project> --hard 와 동일. 명시적 별칭.',
|
|
10
|
+
CONFIG_USAGE_HINT,
|
|
9
11
|
].join('\n');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
* athsra purge <project>
|
|
13
|
-
* - hard-delete alias. 모든 versions + tombstone 영구
|
|
14
|
+
* athsra purge <project>[:<env>]
|
|
15
|
+
* - hard-delete alias. 모든 versions + tombstone 영구 제거 (해당 환경(config) 한정). 복원 불가능.
|
|
14
16
|
* - delete --hard 와 동일 동작이지만 명시적 명령 — 위험성을 더 분명히 드러냄.
|
|
15
17
|
* - 무조건 double confirm (--yes 또는 ATHSRA_PURGE_CONFIRMED=1 로 우회)
|
|
16
18
|
*/
|
|
17
19
|
export async function purgeCmd(args: string[]): Promise<number> {
|
|
18
|
-
const project = args
|
|
20
|
+
const { project, config, rest } = resolveProject(args, { requirePositional: true });
|
|
19
21
|
if (!project) {
|
|
20
22
|
console.error(USAGE);
|
|
21
23
|
return 2;
|
|
22
24
|
}
|
|
23
|
-
const yes =
|
|
25
|
+
const yes = rest.includes('--yes') || rest.includes('-y');
|
|
26
|
+
const tag = configTag(config);
|
|
24
27
|
|
|
25
28
|
const ctx = await loadAuthContext();
|
|
26
29
|
if (!ctx) return 1;
|
|
@@ -28,17 +31,17 @@ export async function purgeCmd(args: string[]): Promise<number> {
|
|
|
28
31
|
|
|
29
32
|
if (!yes && process.env.ATHSRA_PURGE_CONFIRMED !== '1') {
|
|
30
33
|
const ok = await promptConfirm(
|
|
31
|
-
`PURGE ${project}? All version history permanently removed. NOT RECOVERABLE.`,
|
|
34
|
+
`PURGE ${project}${tag}? All version history permanently removed. NOT RECOVERABLE.`,
|
|
32
35
|
false,
|
|
33
36
|
);
|
|
34
37
|
if (!ok) return 0;
|
|
35
|
-
const ok2 = await promptConfirm(`Type confirmation: really purge ${project}?`, false);
|
|
38
|
+
const ok2 = await promptConfirm(`Type confirmation: really purge ${project}${tag}?`, false);
|
|
36
39
|
if (!ok2) return 0;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
const result = await client.deleteProject(project, { hard: true });
|
|
42
|
+
const result = await client.deleteProject(project, { hard: true, config });
|
|
40
43
|
console.log(
|
|
41
|
-
`✓ ${project}: purged (${result.removed_versions ?? 0} version${(result.removed_versions ?? 0) === 1 ? '' : 's'} permanently removed)`,
|
|
44
|
+
`✓ ${project}${tag}: purged (${result.removed_versions ?? 0} version${(result.removed_versions ?? 0) === 1 ? '' : 's'} permanently removed)`,
|
|
42
45
|
);
|
|
43
46
|
return 0;
|
|
44
47
|
}
|
package/src/commands/restore.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { loadAuthContext } from '../lib/auth-context.ts';
|
|
2
|
+
import { CONFIG_USAGE_HINT, configTag, resolveProject } from '../lib/auto-project.ts';
|
|
2
3
|
|
|
3
|
-
const USAGE =
|
|
4
|
+
const USAGE = [
|
|
5
|
+
'usage: athsra restore <project>[:<env>] # tombstone 제거 + 최신 version 활성화',
|
|
6
|
+
CONFIG_USAGE_HINT,
|
|
7
|
+
].join('\n');
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
* athsra restore <project>
|
|
7
|
-
* - soft-deleted project 복원 (tombstone 제거 + 가장 최신 version 으로 current
|
|
10
|
+
* athsra restore <project>[:<env>]
|
|
11
|
+
* - soft-deleted project 복원 (tombstone 제거 + 가장 최신 version 으로 current 복원, 해당 환경(config) 한정)
|
|
8
12
|
* - tombstone 없으면 400 — 이미 활성
|
|
9
13
|
* - hard-delete 후엔 versions 가 없어 복원 불가
|
|
10
14
|
*/
|
|
11
15
|
export async function restoreCmd(args: string[]): Promise<number> {
|
|
12
|
-
const project = args
|
|
16
|
+
const { project, config } = resolveProject(args, { requirePositional: true });
|
|
13
17
|
if (!project) {
|
|
14
18
|
console.error(USAGE);
|
|
15
19
|
return 2;
|
|
@@ -19,9 +23,9 @@ export async function restoreCmd(args: string[]): Promise<number> {
|
|
|
19
23
|
if (!ctx) return 1;
|
|
20
24
|
const { client } = ctx;
|
|
21
25
|
|
|
22
|
-
const result = await client.restoreProject(project);
|
|
26
|
+
const result = await client.restoreProject(project, config);
|
|
23
27
|
console.log(
|
|
24
|
-
`✓ ${project}: restored to ${result.restored_version} (was deleted ${result.deleted_at} by ${result.deleted_by})`,
|
|
28
|
+
`✓ ${project}${configTag(config)}: restored to ${result.restored_version} (was deleted ${result.deleted_at} by ${result.deleted_by})`,
|
|
25
29
|
);
|
|
26
30
|
return 0;
|
|
27
31
|
}
|