@athsra/cli 0.1.0 → 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/package.json +9 -4
- package/src/commands/admin.ts +308 -0
- package/src/commands/adopt.ts +490 -0
- package/src/commands/audit.ts +156 -0
- package/src/commands/completion.ts +101 -0
- package/src/commands/delete.ts +1 -1
- package/src/commands/doctor.ts +86 -2
- package/src/commands/get.ts +10 -14
- package/src/commands/handoff.ts +20 -16
- package/src/commands/login.ts +306 -1
- package/src/commands/logout.ts +50 -0
- package/src/commands/ls.ts +24 -14
- package/src/commands/manifest.ts +354 -0
- package/src/commands/mcp.ts +595 -0
- package/src/commands/migrate-envelopes.ts +113 -0
- package/src/commands/purge.ts +1 -1
- package/src/commands/restore.ts +1 -1
- package/src/commands/revoke.ts +10 -7
- package/src/commands/rollback.ts +1 -1
- package/src/commands/rotate-master.ts +42 -48
- package/src/commands/run.ts +49 -16
- package/src/commands/service-token.ts +200 -0
- package/src/commands/set.ts +33 -47
- package/src/commands/unset.ts +9 -38
- package/src/commands/versions.ts +10 -4
- package/src/index.ts +79 -4
- package/src/lib/adopt-context.ts +183 -0
- package/src/lib/auth-context.ts +77 -4
- package/src/lib/auto-project.ts +131 -0
- package/src/lib/client.ts +359 -4
- package/src/lib/env-format.ts +25 -0
- package/src/lib/envelope.ts +76 -0
- package/src/lib/secrets-manifest.ts +309 -0
- package/src/lib/workers-builds.ts +274 -0
- package/src/lib/wrangler-sync.ts +202 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { loadConfig } from '../lib/config.ts';
|
|
5
|
+
import { clearMasterPw, clearToken } from '../lib/keyring.ts';
|
|
6
|
+
import { promptConfirm } from '../lib/prompt.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* athsra logout keyring 의 master pw + token clear (worker-side token 유지)
|
|
10
|
+
* athsra logout --full config (~/.athsra/config.json) 도 삭제
|
|
11
|
+
*
|
|
12
|
+
* worker-side 토큰을 invalidate 하지 않습니다. 토큰 유출 우려 시 `athsra revoke` 사용.
|
|
13
|
+
* 동일 머신에서 `athsra login` 다시 실행하면 즉시 재 사용 가능 (PROOF 동일하면 token 재 발급).
|
|
14
|
+
*
|
|
15
|
+
* Doppler-level service UX — keyring clear 와 token revoke 분리. revoke 와 logout 의 차이:
|
|
16
|
+
* - logout = local 정리 (keyring + 선택적 config). worker-side token 활성 유지.
|
|
17
|
+
* - revoke = worker-side token invalidate (irreversible). keyring 도 함께 clear.
|
|
18
|
+
*/
|
|
19
|
+
export async function logoutCmd(args: string[]): Promise<number> {
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
if (!config) {
|
|
22
|
+
console.log('Already logged out (no ~/.athsra/config.json found).');
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
const full = args.includes('--full');
|
|
26
|
+
|
|
27
|
+
if (process.env.ATHSRA_LOGOUT_CONFIRMED !== '1') {
|
|
28
|
+
const ok = await promptConfirm(
|
|
29
|
+
`Logout machine ${config.machineId}? (keyring clear, worker token stays active — use \`athsra revoke\` to fully invalidate)`,
|
|
30
|
+
);
|
|
31
|
+
if (!ok) return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
clearMasterPw(config.machineId);
|
|
35
|
+
clearToken(config.machineId);
|
|
36
|
+
console.log(`✓ keyring cleared for machine: ${config.machineId}`);
|
|
37
|
+
console.log(' • master-pw + Bearer token removed from OS keyring');
|
|
38
|
+
console.log(' • worker-side token still active — `athsra revoke` to invalidate fully');
|
|
39
|
+
|
|
40
|
+
if (full) {
|
|
41
|
+
const configPath = join(homedir(), '.athsra', 'config.json');
|
|
42
|
+
if (existsSync(configPath)) {
|
|
43
|
+
unlinkSync(configPath);
|
|
44
|
+
console.log(` • ${configPath} removed (--full)`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('\nNext: athsra login');
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
package/src/commands/ls.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { decrypt, deriveKey, fromBase64 } from '@athsra/crypto';
|
|
2
1
|
import { loadAuthContext } from '../lib/auth-context.ts';
|
|
3
|
-
import {
|
|
2
|
+
import { partitionEnv } from '../lib/env-format.ts';
|
|
3
|
+
import { readPlain } from '../lib/envelope.ts';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* athsra ls — active projects only
|
|
@@ -9,9 +9,9 @@ import { parseEnv } from '../lib/env-format.ts';
|
|
|
9
9
|
* athsra ls <project> — keys of project (decrypt 필요)
|
|
10
10
|
*/
|
|
11
11
|
export async function lsCmd(args: string[]): Promise<number> {
|
|
12
|
-
const ctx = loadAuthContext();
|
|
12
|
+
const ctx = await loadAuthContext();
|
|
13
13
|
if (!ctx) return 1;
|
|
14
|
-
const
|
|
14
|
+
const client = ctx.client;
|
|
15
15
|
|
|
16
16
|
const positional = args.filter((a) => !a.startsWith('-'));
|
|
17
17
|
const includeDeleted = args.includes('--all') || args.includes('--include-deleted');
|
|
@@ -37,21 +37,31 @@ export async function lsCmd(args: string[]): Promise<number> {
|
|
|
37
37
|
return 0;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// ls <project> — key 목록
|
|
40
|
+
// ls <project> — key 목록 + 값 상태 ((empty) 또는 길이, decrypt 필요)
|
|
41
41
|
const project = positional[0];
|
|
42
42
|
if (!project) return 0;
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
44
|
+
const plain = await readPlain(ctx, project);
|
|
45
|
+
if (!plain) {
|
|
46
46
|
console.error(`project not found: ${project}`);
|
|
47
47
|
return 1;
|
|
48
48
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
const
|
|
55
|
-
|
|
49
|
+
const keys = Object.keys(plain).sort();
|
|
50
|
+
if (keys.length === 0) {
|
|
51
|
+
console.log('(no keys)');
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
const { filled, emptyKeys } = partitionEnv(plain);
|
|
55
|
+
const emptySet = new Set(emptyKeys);
|
|
56
|
+
const width = Math.max(...keys.map((k) => k.length));
|
|
57
|
+
for (const k of keys) {
|
|
58
|
+
const status = emptySet.has(k) ? '(empty)' : `${(filled[k] ?? '').length} chars`;
|
|
59
|
+
console.log(`${k.padEnd(width)} ${status}`);
|
|
60
|
+
}
|
|
61
|
+
if (emptyKeys.length > 0) {
|
|
62
|
+
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\``,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
56
66
|
return 0;
|
|
57
67
|
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* manifest.ts — `athsra manifest {init,show,validate,add,remove}` 명령.
|
|
3
|
+
*
|
|
4
|
+
* Phase 2.6 (2026-05-26) Option γ — sibling worker 의 secrets opt-in manifest 관리.
|
|
5
|
+
*
|
|
6
|
+
* 사용 흐름 (sibling repo 안에서):
|
|
7
|
+
* athsra manifest init --keys=DATABASE_URL,SESSION_SECRET
|
|
8
|
+
* athsra manifest validate
|
|
9
|
+
* athsra manifest add NEW_KEY
|
|
10
|
+
* athsra manifest show
|
|
11
|
+
*
|
|
12
|
+
* manifest 가 worker 디렉토리 안에 commit 됨 (사업장 키 이름만, 값 X — safe to commit).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { inferAdoptContext, type WrangerWorker } from '../lib/adopt-context.ts';
|
|
16
|
+
import { loadAuthContext } from '../lib/auth-context.ts';
|
|
17
|
+
import { resolveProject } from '../lib/auto-project.ts';
|
|
18
|
+
import { readPlain } from '../lib/envelope.ts';
|
|
19
|
+
import {
|
|
20
|
+
applyManifest,
|
|
21
|
+
createManifest,
|
|
22
|
+
loadManifest,
|
|
23
|
+
type SecretsManifest,
|
|
24
|
+
manifestPath,
|
|
25
|
+
saveManifest,
|
|
26
|
+
} from '../lib/secrets-manifest.ts';
|
|
27
|
+
|
|
28
|
+
const USAGE = [
|
|
29
|
+
'usage: athsra manifest <subcommand> [options]',
|
|
30
|
+
'',
|
|
31
|
+
'Phase 2.6 Option γ — sibling worker 의 secrets opt-in manifest 관리.',
|
|
32
|
+
'',
|
|
33
|
+
'서브명령:',
|
|
34
|
+
' init [--worker=<name>] [--all] [--keys=K1,K2,...] [--keys-from=<file>]',
|
|
35
|
+
' manifest 신규 작성. --all 은 envelope 전체 키. --keys 명시 (콤마 구분).',
|
|
36
|
+
'',
|
|
37
|
+
' show [--worker=<name>]',
|
|
38
|
+
' manifest 내용 출력.',
|
|
39
|
+
'',
|
|
40
|
+
' validate [<project>] [--worker=<name>]',
|
|
41
|
+
' manifest vs envelope diff. project 명시 시 envelope 부족분 / 초과분 표시.',
|
|
42
|
+
'',
|
|
43
|
+
' add <KEY> [<KEY2>...] [--worker=<name>]',
|
|
44
|
+
' manifest 에 키 추가 (이미 있으면 idempotent).',
|
|
45
|
+
'',
|
|
46
|
+
' remove <KEY> [<KEY2>...] [--worker=<name>]',
|
|
47
|
+
' manifest 에서 키 제거.',
|
|
48
|
+
'',
|
|
49
|
+
'공통 옵션:',
|
|
50
|
+
' --worker=<name> 다수 wrangler.jsonc 있는 monorepo 에서 명시. 단일이면 생략.',
|
|
51
|
+
'',
|
|
52
|
+
'manifest 위치: <worker.cwd>/.athsra/secrets.json',
|
|
53
|
+
].join('\n');
|
|
54
|
+
|
|
55
|
+
interface ParsedFlags {
|
|
56
|
+
worker?: string;
|
|
57
|
+
keys?: string[];
|
|
58
|
+
keysFrom?: string;
|
|
59
|
+
all: boolean;
|
|
60
|
+
positional: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseFlags(args: string[]): ParsedFlags {
|
|
64
|
+
const positional: string[] = [];
|
|
65
|
+
const flags: Record<string, string | boolean> = {};
|
|
66
|
+
for (const a of args) {
|
|
67
|
+
if (a.startsWith('--')) {
|
|
68
|
+
const eq = a.indexOf('=');
|
|
69
|
+
if (eq > 0) flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
70
|
+
else flags[a.slice(2)] = true;
|
|
71
|
+
} else {
|
|
72
|
+
positional.push(a);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const keysRaw = typeof flags.keys === 'string' ? flags.keys : undefined;
|
|
76
|
+
return {
|
|
77
|
+
worker: typeof flags.worker === 'string' ? flags.worker : undefined,
|
|
78
|
+
keys: keysRaw
|
|
79
|
+
? keysRaw
|
|
80
|
+
.split(',')
|
|
81
|
+
.map((s) => s.trim())
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
: undefined,
|
|
84
|
+
keysFrom: typeof flags['keys-from'] === 'string' ? flags['keys-from'] : undefined,
|
|
85
|
+
all: flags.all === true,
|
|
86
|
+
positional,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function pickWorker(workers: WrangerWorker[], explicit?: string): WrangerWorker | string {
|
|
91
|
+
if (explicit) {
|
|
92
|
+
const found = workers.find((w) => w.name === explicit);
|
|
93
|
+
if (!found) {
|
|
94
|
+
const names = workers.map((w) => w.name).join(', ') || '(none)';
|
|
95
|
+
return `worker '${explicit}' not found in cwd. available: ${names}`;
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
if (workers.length === 0) {
|
|
100
|
+
return 'no wrangler.jsonc found in cwd. cd into sibling repo (or its subdirectory).';
|
|
101
|
+
}
|
|
102
|
+
if (workers.length === 1) {
|
|
103
|
+
const [only] = workers;
|
|
104
|
+
if (!only) return 'no wrangler.jsonc found';
|
|
105
|
+
return only;
|
|
106
|
+
}
|
|
107
|
+
const list = workers.map((w) => ` - ${w.name} (root=${w.rootDirectory})`).join('\n');
|
|
108
|
+
return `multiple workers found. --worker=<name>:\n${list}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readKeysFromFile(path: string): Promise<string[]> {
|
|
112
|
+
const file = Bun.file(path);
|
|
113
|
+
if (!(await file.exists())) {
|
|
114
|
+
throw new Error(`--keys-from file not found: ${path}`);
|
|
115
|
+
}
|
|
116
|
+
const text = await file.text();
|
|
117
|
+
return text
|
|
118
|
+
.split(/\r?\n/)
|
|
119
|
+
.map((s) => s.trim())
|
|
120
|
+
.filter((s) => s.length > 0 && !s.startsWith('#'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function cmdInit(flags: ParsedFlags): Promise<number> {
|
|
124
|
+
const inferred = inferAdoptContext(process.cwd());
|
|
125
|
+
const worker = pickWorker(inferred.workers, flags.worker);
|
|
126
|
+
if (typeof worker === 'string') {
|
|
127
|
+
console.error(`✗ ${worker}`);
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const existing = loadManifest({ workerCwd: worker.cwd });
|
|
132
|
+
if (existing.manifest) {
|
|
133
|
+
console.error(`✗ manifest already exists: ${existing.path}`);
|
|
134
|
+
console.error(' use `athsra manifest add/remove` to modify, or delete file to recreate');
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let secrets: string[];
|
|
139
|
+
if (flags.keys && flags.keys.length > 0) {
|
|
140
|
+
secrets = flags.keys;
|
|
141
|
+
} else if (flags.keysFrom) {
|
|
142
|
+
secrets = await readKeysFromFile(flags.keysFrom);
|
|
143
|
+
} else if (flags.all) {
|
|
144
|
+
// envelope 전체 키 — project 추론 필요
|
|
145
|
+
const { project } = resolveProject([]);
|
|
146
|
+
if (!project) {
|
|
147
|
+
console.error('✗ --all 사용 시 project 추론 실패. .athsra 또는 --project=<x> 명시 필요');
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
|
150
|
+
const ctx = await loadAuthContext();
|
|
151
|
+
if (!ctx || ctx.kind !== 'user') {
|
|
152
|
+
console.error('✗ user token (master pw) 필요. service token 거부.');
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
const envelope = await readPlain(ctx, project);
|
|
156
|
+
if (!envelope) {
|
|
157
|
+
console.error(`✗ envelope '${project}' not found`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
secrets = Object.keys(envelope).filter(
|
|
161
|
+
(k) => typeof envelope[k] === 'string' && envelope[k].length > 0,
|
|
162
|
+
);
|
|
163
|
+
console.log(` envelope '${project}' 의 ${secrets.length} 키를 manifest 로 캡처`);
|
|
164
|
+
} else {
|
|
165
|
+
console.error('✗ secrets 출처 명시 필요: --keys=K1,K2 / --keys-from=<file> / --all');
|
|
166
|
+
return 2;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (secrets.length === 0) {
|
|
170
|
+
console.error('✗ no secrets to capture');
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let manifest: SecretsManifest;
|
|
175
|
+
try {
|
|
176
|
+
manifest = createManifest(secrets);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(`✗ ${(err as Error).message}`);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const saved = saveManifest(worker.cwd, manifest);
|
|
183
|
+
console.log(`✓ manifest written: ${saved}`);
|
|
184
|
+
console.log(` worker: ${worker.name}`);
|
|
185
|
+
console.log(` secrets: ${manifest.secrets.length} (alphabetical)`);
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log('다음 단계:');
|
|
188
|
+
console.log(` git add ${saved}`);
|
|
189
|
+
console.log(' git commit -m "feat(athsra): add secrets manifest"');
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cmdShow(flags: ParsedFlags): number {
|
|
194
|
+
const inferred = inferAdoptContext(process.cwd());
|
|
195
|
+
const worker = pickWorker(inferred.workers, flags.worker);
|
|
196
|
+
if (typeof worker === 'string') {
|
|
197
|
+
console.error(`✗ ${worker}`);
|
|
198
|
+
return 2;
|
|
199
|
+
}
|
|
200
|
+
const loaded = loadManifest({ workerCwd: worker.cwd });
|
|
201
|
+
if (loaded.error) {
|
|
202
|
+
console.error(`✗ manifest invalid (${loaded.path}): ${loaded.error}`);
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
if (!loaded.manifest) {
|
|
206
|
+
console.error(`✗ no manifest at ${loaded.path}`);
|
|
207
|
+
console.error(' create with: athsra manifest init --keys=K1,K2,...');
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
console.log(`# ${loaded.path}`);
|
|
211
|
+
console.log(`# worker: ${worker.name}`);
|
|
212
|
+
console.log(`# secrets: ${loaded.manifest.secrets.length}`);
|
|
213
|
+
console.log('');
|
|
214
|
+
for (const k of loaded.manifest.secrets) {
|
|
215
|
+
console.log(k);
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function cmdValidate(flags: ParsedFlags): Promise<number> {
|
|
221
|
+
const inferred = inferAdoptContext(process.cwd());
|
|
222
|
+
const worker = pickWorker(inferred.workers, flags.worker);
|
|
223
|
+
if (typeof worker === 'string') {
|
|
224
|
+
console.error(`✗ ${worker}`);
|
|
225
|
+
return 2;
|
|
226
|
+
}
|
|
227
|
+
const loaded = loadManifest({ workerCwd: worker.cwd });
|
|
228
|
+
if (loaded.error) {
|
|
229
|
+
console.error(`✗ manifest invalid (${loaded.path}): ${loaded.error}`);
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
if (!loaded.manifest) {
|
|
233
|
+
console.error(`✗ no manifest at ${loaded.path}`);
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
console.log(`✓ manifest schema valid (${loaded.manifest.secrets.length} secrets)`);
|
|
237
|
+
|
|
238
|
+
const projectArg = flags.positional[0];
|
|
239
|
+
const { project } = resolveProject(projectArg ? [projectArg] : []);
|
|
240
|
+
if (!project) {
|
|
241
|
+
console.log(' (project 미지정 — envelope diff 생략. project 명시 시 추가 검증)');
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
const ctx = await loadAuthContext();
|
|
245
|
+
if (!ctx || ctx.kind !== 'user') {
|
|
246
|
+
console.error('✗ envelope diff 위해 user token (master pw) 필요');
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
const envelope = await readPlain(ctx, project);
|
|
250
|
+
if (!envelope) {
|
|
251
|
+
console.error(`✗ envelope '${project}' not found`);
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
const envelopeKeys = Object.keys(envelope).filter(
|
|
255
|
+
(k) => typeof envelope[k] === 'string' && envelope[k].length > 0,
|
|
256
|
+
);
|
|
257
|
+
const applied = applyManifest(loaded.manifest, envelopeKeys);
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(`envelope '${project}' diff:`);
|
|
260
|
+
console.log(` ✓ allowed (will sync): ${applied.allowed.length}`);
|
|
261
|
+
if (applied.missing.length > 0) {
|
|
262
|
+
console.log(` ⚠ in manifest but missing from envelope: ${applied.missing.length}`);
|
|
263
|
+
for (const k of applied.missing) console.log(` - ${k}`);
|
|
264
|
+
}
|
|
265
|
+
if (applied.excluded.length > 0) {
|
|
266
|
+
console.log(` · in envelope but not in manifest (excluded): ${applied.excluded.length}`);
|
|
267
|
+
if (applied.excluded.length <= 10) {
|
|
268
|
+
for (const k of applied.excluded) console.log(` - ${k}`);
|
|
269
|
+
} else {
|
|
270
|
+
for (const k of applied.excluded.slice(0, 5)) console.log(` - ${k}`);
|
|
271
|
+
console.log(` ... + ${applied.excluded.length - 5} more`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return applied.missing.length > 0 ? 1 : 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function modifyManifest(
|
|
278
|
+
flags: ParsedFlags,
|
|
279
|
+
op: 'add' | 'remove',
|
|
280
|
+
): number {
|
|
281
|
+
const keys = flags.positional;
|
|
282
|
+
if (keys.length === 0) {
|
|
283
|
+
console.error(`✗ usage: athsra manifest ${op} <KEY> [<KEY2>...] [--worker=<name>]`);
|
|
284
|
+
return 2;
|
|
285
|
+
}
|
|
286
|
+
const inferred = inferAdoptContext(process.cwd());
|
|
287
|
+
const worker = pickWorker(inferred.workers, flags.worker);
|
|
288
|
+
if (typeof worker === 'string') {
|
|
289
|
+
console.error(`✗ ${worker}`);
|
|
290
|
+
return 2;
|
|
291
|
+
}
|
|
292
|
+
const loaded = loadManifest({ workerCwd: worker.cwd });
|
|
293
|
+
if (loaded.error) {
|
|
294
|
+
console.error(`✗ manifest invalid (${loaded.path}): ${loaded.error}`);
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
if (!loaded.manifest) {
|
|
298
|
+
console.error(`✗ no manifest at ${loaded.path}`);
|
|
299
|
+
console.error(' create with: athsra manifest init --keys=K1,K2,...');
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
const current = new Set(loaded.manifest.secrets);
|
|
303
|
+
const before = current.size;
|
|
304
|
+
if (op === 'add') {
|
|
305
|
+
for (const k of keys) current.add(k);
|
|
306
|
+
} else {
|
|
307
|
+
for (const k of keys) current.delete(k);
|
|
308
|
+
}
|
|
309
|
+
const after = current.size;
|
|
310
|
+
if (after === before) {
|
|
311
|
+
console.log(`(no change — ${op} ${keys.join(',')})`);
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
let updated: SecretsManifest;
|
|
315
|
+
try {
|
|
316
|
+
updated = createManifest(Array.from(current));
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.error(`✗ ${(err as Error).message}`);
|
|
319
|
+
return 1;
|
|
320
|
+
}
|
|
321
|
+
const saved = saveManifest(worker.cwd, updated);
|
|
322
|
+
console.log(`✓ manifest ${op}: ${keys.join(', ')} (now ${after} secrets)`);
|
|
323
|
+
console.log(` ${saved}`);
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function manifestCmd(args: string[]): Promise<number> {
|
|
328
|
+
const [sub, ...rest] = args;
|
|
329
|
+
if (!sub || sub === '--help' || sub === '-h' || sub === 'help') {
|
|
330
|
+
console.log(USAGE);
|
|
331
|
+
return sub ? 0 : 2;
|
|
332
|
+
}
|
|
333
|
+
const flags = parseFlags(rest);
|
|
334
|
+
switch (sub) {
|
|
335
|
+
case 'init':
|
|
336
|
+
return cmdInit(flags);
|
|
337
|
+
case 'show':
|
|
338
|
+
return cmdShow(flags);
|
|
339
|
+
case 'validate':
|
|
340
|
+
return cmdValidate(flags);
|
|
341
|
+
case 'add':
|
|
342
|
+
return modifyManifest(flags, 'add');
|
|
343
|
+
case 'remove':
|
|
344
|
+
case 'rm':
|
|
345
|
+
return modifyManifest(flags, 'remove');
|
|
346
|
+
default:
|
|
347
|
+
console.error(`✗ unknown subcommand: ${sub}`);
|
|
348
|
+
console.error(USAGE);
|
|
349
|
+
return 2;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Path helper for callers (e.g., adopt.ts error formatting). */
|
|
354
|
+
export { manifestPath };
|