@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.
Files changed (45) hide show
  1. package/README.md +34 -10
  2. package/package.json +3 -3
  3. package/src/commands/delete.ts +16 -13
  4. package/src/commands/get.ts +8 -5
  5. package/src/commands/handoff.ts +13 -3
  6. package/src/commands/login.ts +208 -61
  7. package/src/commands/logout.ts +3 -2
  8. package/src/commands/ls.ts +32 -10
  9. package/src/commands/manifest.ts +2 -2
  10. package/src/commands/mcp.ts +259 -7
  11. package/src/commands/migrate-envelopes.ts +55 -3
  12. package/src/commands/purge.ts +13 -10
  13. package/src/commands/restore.ts +10 -6
  14. package/src/commands/rollback.ts +12 -9
  15. package/src/commands/rotate-master.ts +13 -13
  16. package/src/commands/run.ts +6 -24
  17. package/src/commands/service-token.ts +15 -31
  18. package/src/commands/set.ts +7 -6
  19. package/src/commands/unset.ts +11 -8
  20. package/src/commands/versions.ts +7 -5
  21. package/src/index.ts +12 -8
  22. package/src/lib/auth-context.ts +77 -13
  23. package/src/lib/auth-proof.ts +26 -0
  24. package/src/lib/auto-project.ts +58 -14
  25. package/src/lib/client.ts +112 -19
  26. package/src/lib/config.ts +2 -0
  27. package/src/lib/device-login.ts +157 -0
  28. package/src/lib/env-format.ts +1 -1
  29. package/src/lib/envelope.ts +105 -15
  30. package/src/lib/identity-key.ts +21 -0
  31. package/src/lib/keyring.ts +25 -0
  32. package/src/lib/mcp-register.ts +223 -0
  33. package/src/lib/mcp-tools/admin.ts +267 -0
  34. package/src/lib/mcp-tools/args.ts +26 -0
  35. package/src/lib/mcp-tools/confirm.ts +21 -0
  36. package/src/lib/mcp-tools/defs.ts +388 -3
  37. package/src/lib/mcp-tools/login.ts +156 -0
  38. package/src/lib/mcp-tools/mask.ts +41 -0
  39. package/src/lib/mcp-tools/read.ts +115 -1
  40. package/src/lib/mcp-tools/result.ts +5 -5
  41. package/src/lib/mcp-tools/run.ts +101 -0
  42. package/src/lib/mcp-tools/write.ts +84 -5
  43. package/src/lib/oidc-flow.ts +43 -1
  44. package/src/lib/org-rewrap.ts +9 -3
  45. package/src/lib/service-tokens.ts +62 -0
@@ -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> — keys of project (decrypt 필요)
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 positional = args.filter((a) => !a.startsWith('-'));
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 (positional.length === 0) {
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> — key 목록 + 상태 ((empty) 또는 길이, decrypt 필요)
41
- const project = positional[0];
42
- if (!project) return 0;
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
- const plain = await readPlain(ctx, project);
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('(no keys)');
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;
@@ -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 (!ctx || ctx.kind !== 'user') {
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 (!ctx || ctx.kind !== 'user') {
302
+ if (ctx?.kind !== 'user') {
303
303
  console.error('✗ envelope diff 위해 user token (master pw) 필요');
304
304
  return 1;
305
305
  }
@@ -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 — 인증 상태 + 신원 (미인증 시 `athsra login --sso` 안내). 온보딩 시 먼저 호출',
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.0.0';
130
+ const VERSION = '1.1.0';
84
131
 
85
- /** Write tools opt-in via env var. 기본 false — secret 변경은 명시적 ack 필요. */
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[] = WRITE_ENABLED ? [...READ_TOOLS, ...WRITE_TOOLS] : READ_TOOLS;
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
- writeEnabled: boolean = WRITE_ENABLED,
152
+ gatesIn: Partial<{ write: boolean; admin: boolean; value: boolean }> = {},
96
153
  ): Promise<ToolTextResult> {
97
- if (WRITE_TOOL_NAMES.has(name) && !writeEnabled) {
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
+ }
@@ -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[0];
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 = args.includes('--yes') || args.includes('-y');
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
  }
@@ -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 = 'usage: athsra restore <project> # tombstone 제거 + 최신 version 활성화';
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[0];
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
  }