ccprofiles-core 0.1.0 → 0.2.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/dist/adopt.d.ts +8 -0
- package/dist/adopt.js +22 -0
- package/dist/apply.d.ts +7 -1
- package/dist/apply.js +50 -2
- package/dist/bundle.js +2 -2
- package/dist/discovery.d.ts +1 -0
- package/dist/discovery.js +10 -0
- package/dist/manifest.d.ts +8 -0
- package/dist/manifest.js +10 -0
- package/dist/rcblock.js +2 -2
- package/dist/syncclient.js +1 -1
- package/dist/syncserver.js +2 -2
- package/package.json +23 -6
package/dist/adopt.d.ts
CHANGED
|
@@ -2,3 +2,11 @@ import type { LiveProfile } from './discovery.js';
|
|
|
2
2
|
import type { Manifest } from './manifest.js';
|
|
3
3
|
import { type Platform } from './platform.js';
|
|
4
4
|
export declare function buildManifest(live: LiveProfile[], platform: Platform): Manifest;
|
|
5
|
+
/**
|
|
6
|
+
* Re-migrate secret refs after a snapshot/adopt rebuilds the manifest from live state.
|
|
7
|
+
* Live settings.json holds resolved plaintext values, so a freshly built manifest would
|
|
8
|
+
* re-leak a previously migrated token as plaintext. For each profile that existed before,
|
|
9
|
+
* if the old manifest had a secret:// ref whose resolved value matches the newly-discovered
|
|
10
|
+
* plaintext value, restore the ref instead of the plaintext.
|
|
11
|
+
*/
|
|
12
|
+
export declare function preserveSecretRefs(newM: Manifest, oldM: Manifest, getSecret: (name: string) => Promise<string | null>): Promise<void>;
|
package/dist/adopt.js
CHANGED
|
@@ -32,7 +32,29 @@ export function buildManifest(live, platform) {
|
|
|
32
32
|
env: {},
|
|
33
33
|
links,
|
|
34
34
|
mcp: Object.keys(lp.mcpServers).sort(),
|
|
35
|
+
settingsEnv: lp.settingsEnv,
|
|
35
36
|
};
|
|
36
37
|
});
|
|
37
38
|
return { version: 1, hub, profiles, mcpServers };
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Re-migrate secret refs after a snapshot/adopt rebuilds the manifest from live state.
|
|
42
|
+
* Live settings.json holds resolved plaintext values, so a freshly built manifest would
|
|
43
|
+
* re-leak a previously migrated token as plaintext. For each profile that existed before,
|
|
44
|
+
* if the old manifest had a secret:// ref whose resolved value matches the newly-discovered
|
|
45
|
+
* plaintext value, restore the ref instead of the plaintext.
|
|
46
|
+
*/
|
|
47
|
+
export async function preserveSecretRefs(newM, oldM, getSecret) {
|
|
48
|
+
for (const pr of newM.profiles) {
|
|
49
|
+
const old = oldM.profiles.find(p => p.name === pr.name);
|
|
50
|
+
if (!old)
|
|
51
|
+
continue;
|
|
52
|
+
for (const [k, v] of Object.entries(old.settingsEnv ?? {})) {
|
|
53
|
+
if (!v.startsWith('secret://'))
|
|
54
|
+
continue;
|
|
55
|
+
const resolved = await getSecret(v.slice('secret://'.length));
|
|
56
|
+
if (resolved !== null && pr.settingsEnv[k] === resolved)
|
|
57
|
+
pr.settingsEnv[k] = v;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/apply.d.ts
CHANGED
|
@@ -16,8 +16,14 @@ export type ApplyAction = {
|
|
|
16
16
|
kind: 'rc-block';
|
|
17
17
|
rcFile: string;
|
|
18
18
|
block: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'set-settings-env';
|
|
21
|
+
settingsPath: string;
|
|
22
|
+
env: Record<string, string>;
|
|
19
23
|
};
|
|
20
|
-
|
|
24
|
+
/** Resolve secret:// refs in every profile's settingsEnv. Throws on a missing secret. */
|
|
25
|
+
export declare function resolveSettingsEnv(m: Manifest, getSecret: (name: string) => Promise<string | null>): Promise<Record<string, Record<string, string>>>;
|
|
26
|
+
export declare function planApply(m: Manifest, live: LiveProfile[], p: Platform, resolvedSettingsEnv?: Record<string, Record<string, string>>): ApplyAction[];
|
|
21
27
|
export declare function executeApply(actions: ApplyAction[], opts: {
|
|
22
28
|
backupRoot: string;
|
|
23
29
|
stamp: string;
|
package/dist/apply.js
CHANGED
|
@@ -10,7 +10,28 @@ function configPathFor(dir, home) {
|
|
|
10
10
|
function sortKeys(o) {
|
|
11
11
|
return Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)));
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
const SECRET_PREFIX = 'secret://';
|
|
14
|
+
/** Resolve secret:// refs in every profile's settingsEnv. Throws on a missing secret. */
|
|
15
|
+
export async function resolveSettingsEnv(m, getSecret) {
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const pr of m.profiles) {
|
|
18
|
+
const env = {};
|
|
19
|
+
for (const [k, v] of Object.entries(pr.settingsEnv ?? {})) {
|
|
20
|
+
if (v.startsWith(SECRET_PREFIX)) {
|
|
21
|
+
const ref = v.slice(SECRET_PREFIX.length);
|
|
22
|
+
const val = await getSecret(ref);
|
|
23
|
+
if (val === null)
|
|
24
|
+
throw new Error(`profile "${pr.name}": secret not found: ${ref} (for ${k})`);
|
|
25
|
+
env[k] = val;
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
env[k] = v;
|
|
29
|
+
}
|
|
30
|
+
out[pr.name] = env;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
export function planApply(m, live, p, resolvedSettingsEnv) {
|
|
14
35
|
const actions = [];
|
|
15
36
|
const hubProfile = m.profiles.find(x => x.name === m.hub) ?? null;
|
|
16
37
|
for (const pr of m.profiles) {
|
|
@@ -34,6 +55,19 @@ export function planApply(m, live, p) {
|
|
|
34
55
|
continue;
|
|
35
56
|
actions.push({ kind: 'link', from, to });
|
|
36
57
|
}
|
|
58
|
+
const senv = pr.settingsEnv ?? {}; // literals in older tests may omit the field
|
|
59
|
+
if (Object.keys(senv).length > 0) {
|
|
60
|
+
let desired = resolvedSettingsEnv?.[pr.name];
|
|
61
|
+
if (!desired) {
|
|
62
|
+
if (Object.values(senv).some(v => v.startsWith(SECRET_PREFIX)))
|
|
63
|
+
throw new Error(`profile "${pr.name}": settingsEnv has secret refs — pass resolved settings env to planApply`);
|
|
64
|
+
desired = senv;
|
|
65
|
+
}
|
|
66
|
+
const currentEnv = lp?.settingsEnv ?? null;
|
|
67
|
+
if (!currentEnv || JSON.stringify(sortKeys(currentEnv)) !== JSON.stringify(sortKeys(desired))) {
|
|
68
|
+
actions.push({ kind: 'set-settings-env', settingsPath: join(dir, 'settings.json'), env: desired });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
37
71
|
}
|
|
38
72
|
const block = renderRcBlock(m, p);
|
|
39
73
|
let rcCurrent = '';
|
|
@@ -49,7 +83,10 @@ export async function executeApply(actions, opts) {
|
|
|
49
83
|
const performed = actions.map(describe);
|
|
50
84
|
if (opts.dryRun)
|
|
51
85
|
return { backupDir: null, performed };
|
|
52
|
-
const touched = actions.flatMap(a => a.kind === 'set-mcp-servers' ? [a.configPath]
|
|
86
|
+
const touched = actions.flatMap(a => a.kind === 'set-mcp-servers' ? [a.configPath]
|
|
87
|
+
: a.kind === 'rc-block' ? [a.rcFile]
|
|
88
|
+
: a.kind === 'set-settings-env' ? [a.settingsPath]
|
|
89
|
+
: []);
|
|
53
90
|
const backupDir = touched.length ? await backupFiles(touched, opts.backupRoot, opts.stamp) : null;
|
|
54
91
|
for (const a of actions) {
|
|
55
92
|
if (a.kind === 'create-profile-dir') {
|
|
@@ -89,6 +126,16 @@ export async function executeApply(actions, opts) {
|
|
|
89
126
|
await mkdir(dirname(a.rcFile), { recursive: true });
|
|
90
127
|
await atomicWrite(a.rcFile, upsertManagedBlock(rc, a.block));
|
|
91
128
|
}
|
|
129
|
+
else if (a.kind === 'set-settings-env') {
|
|
130
|
+
let cfg = {};
|
|
131
|
+
try {
|
|
132
|
+
cfg = JSON.parse(await readFile(a.settingsPath, 'utf8'));
|
|
133
|
+
}
|
|
134
|
+
catch { /* new file */ }
|
|
135
|
+
cfg.env = a.env;
|
|
136
|
+
await mkdir(dirname(a.settingsPath), { recursive: true });
|
|
137
|
+
await atomicWrite(a.settingsPath, JSON.stringify(cfg, null, 2));
|
|
138
|
+
}
|
|
92
139
|
}
|
|
93
140
|
return { backupDir, performed };
|
|
94
141
|
}
|
|
@@ -98,5 +145,6 @@ function describe(a) {
|
|
|
98
145
|
case 'create-profile-dir': return `create ${a.dir}`;
|
|
99
146
|
case 'link': return `link ${a.from} -> ${a.to}`;
|
|
100
147
|
case 'rc-block': return `update managed block in ${a.rcFile}`;
|
|
148
|
+
case 'set-settings-env': return `set settings env (${Object.keys(a.env).length}) in ${a.settingsPath}`;
|
|
101
149
|
}
|
|
102
150
|
}
|
package/dist/bundle.js
CHANGED
|
@@ -8,9 +8,9 @@ export function importBundle(buf) {
|
|
|
8
8
|
parsed = JSON.parse(gunzipSync(buf).toString('utf8'));
|
|
9
9
|
}
|
|
10
10
|
catch {
|
|
11
|
-
throw new Error('not a ccprofiles bundle (expected a file created by:
|
|
11
|
+
throw new Error('not a ccprofiles bundle (expected a file created by: ccprofiles export)');
|
|
12
12
|
}
|
|
13
13
|
if (parsed?.v !== 1 || typeof parsed.manifestYaml !== 'string')
|
|
14
|
-
throw new Error('not a ccprofiles bundle (expected a file created by:
|
|
14
|
+
throw new Error('not a ccprofiles bundle (expected a file created by: ccprofiles export)');
|
|
15
15
|
return parsed;
|
|
16
16
|
}
|
package/dist/discovery.d.ts
CHANGED
package/dist/discovery.js
CHANGED
|
@@ -26,6 +26,15 @@ export async function discoverProfiles(home) {
|
|
|
26
26
|
catch { /* skip */ }
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
const settingsEnv = {};
|
|
30
|
+
try {
|
|
31
|
+
const s = JSON.parse(await readFile(join(dir, 'settings.json'), 'utf8'));
|
|
32
|
+
if (s && typeof s.env === 'object' && s.env !== null)
|
|
33
|
+
for (const [k, v] of Object.entries(s.env))
|
|
34
|
+
if (typeof v === 'string')
|
|
35
|
+
settingsEnv[k] = v;
|
|
36
|
+
}
|
|
37
|
+
catch { /* no settings.json */ }
|
|
29
38
|
out.push({
|
|
30
39
|
dirName: e.name,
|
|
31
40
|
dir,
|
|
@@ -33,6 +42,7 @@ export async function discoverProfiles(home) {
|
|
|
33
42
|
account: cfg?.oauthAccount?.emailAddress ?? null,
|
|
34
43
|
mcpServers: cfg?.mcpServers ?? {},
|
|
35
44
|
links,
|
|
45
|
+
settingsEnv,
|
|
36
46
|
});
|
|
37
47
|
}
|
|
38
48
|
return out.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
package/dist/manifest.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ declare const ProfileSchema: z.ZodObject<{
|
|
|
28
28
|
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
29
29
|
links: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
30
30
|
mcp: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
31
|
+
settingsEnv: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
31
32
|
}, "strip", z.ZodTypeAny, {
|
|
32
33
|
env: Record<string, string>;
|
|
33
34
|
name: string;
|
|
@@ -36,6 +37,7 @@ declare const ProfileSchema: z.ZodObject<{
|
|
|
36
37
|
auth: "env" | "oauth" | "api-key";
|
|
37
38
|
links: Record<string, string>;
|
|
38
39
|
mcp: string[];
|
|
40
|
+
settingsEnv: Record<string, string>;
|
|
39
41
|
}, {
|
|
40
42
|
name: string;
|
|
41
43
|
dir: string;
|
|
@@ -44,6 +46,7 @@ declare const ProfileSchema: z.ZodObject<{
|
|
|
44
46
|
env?: Record<string, string> | undefined;
|
|
45
47
|
links?: Record<string, string> | undefined;
|
|
46
48
|
mcp?: string[] | undefined;
|
|
49
|
+
settingsEnv?: Record<string, string> | undefined;
|
|
47
50
|
}>;
|
|
48
51
|
declare const ManifestSchema: z.ZodObject<{
|
|
49
52
|
version: z.ZodLiteral<1>;
|
|
@@ -56,6 +59,7 @@ declare const ManifestSchema: z.ZodObject<{
|
|
|
56
59
|
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
57
60
|
links: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
58
61
|
mcp: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
62
|
+
settingsEnv: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
59
63
|
}, "strip", z.ZodTypeAny, {
|
|
60
64
|
env: Record<string, string>;
|
|
61
65
|
name: string;
|
|
@@ -64,6 +68,7 @@ declare const ManifestSchema: z.ZodObject<{
|
|
|
64
68
|
auth: "env" | "oauth" | "api-key";
|
|
65
69
|
links: Record<string, string>;
|
|
66
70
|
mcp: string[];
|
|
71
|
+
settingsEnv: Record<string, string>;
|
|
67
72
|
}, {
|
|
68
73
|
name: string;
|
|
69
74
|
dir: string;
|
|
@@ -72,6 +77,7 @@ declare const ManifestSchema: z.ZodObject<{
|
|
|
72
77
|
env?: Record<string, string> | undefined;
|
|
73
78
|
links?: Record<string, string> | undefined;
|
|
74
79
|
mcp?: string[] | undefined;
|
|
80
|
+
settingsEnv?: Record<string, string> | undefined;
|
|
75
81
|
}>, "many">;
|
|
76
82
|
mcpServers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
77
83
|
command: z.ZodString;
|
|
@@ -103,6 +109,7 @@ declare const ManifestSchema: z.ZodObject<{
|
|
|
103
109
|
auth: "env" | "oauth" | "api-key";
|
|
104
110
|
links: Record<string, string>;
|
|
105
111
|
mcp: string[];
|
|
112
|
+
settingsEnv: Record<string, string>;
|
|
106
113
|
}[];
|
|
107
114
|
mcpServers: Record<string, z.objectOutputType<{
|
|
108
115
|
command: z.ZodString;
|
|
@@ -122,6 +129,7 @@ declare const ManifestSchema: z.ZodObject<{
|
|
|
122
129
|
env?: Record<string, string> | undefined;
|
|
123
130
|
links?: Record<string, string> | undefined;
|
|
124
131
|
mcp?: string[] | undefined;
|
|
132
|
+
settingsEnv?: Record<string, string> | undefined;
|
|
125
133
|
}[];
|
|
126
134
|
mcpServers: Record<string, z.objectInputType<{
|
|
127
135
|
command: z.ZodString;
|
package/dist/manifest.js
CHANGED
|
@@ -22,6 +22,7 @@ const ProfileSchema = z.object({
|
|
|
22
22
|
env: z.record(z.string()).default({}),
|
|
23
23
|
links: z.record(z.string()).default({}),
|
|
24
24
|
mcp: z.array(z.string()).default([]),
|
|
25
|
+
settingsEnv: z.record(z.string()).default({}),
|
|
25
26
|
});
|
|
26
27
|
const ManifestSchema = z.object({
|
|
27
28
|
version: z.literal(1),
|
|
@@ -56,6 +57,15 @@ export function assertSafeManifest(m) {
|
|
|
56
57
|
throw new ManifestError(`unsafe secret reference in profile "${p.name}": ${JSON.stringify(v)}`);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
for (const [k, v] of Object.entries(p.settingsEnv)) {
|
|
61
|
+
if (!SAFE_ENV_KEY.test(k))
|
|
62
|
+
throw new ManifestError(`unsafe settings env var name in profile "${p.name}": ${JSON.stringify(k)}`);
|
|
63
|
+
if (v.startsWith(SECRET_PREFIX)) {
|
|
64
|
+
const ref = v.slice(SECRET_PREFIX.length);
|
|
65
|
+
if (!SAFE_NAME.test(ref))
|
|
66
|
+
throw new ManifestError(`unsafe secret reference in profile "${p.name}": ${JSON.stringify(v)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
71
|
export function parseManifest(text) {
|
package/dist/rcblock.js
CHANGED
|
@@ -20,7 +20,7 @@ function renderPosix(pr, p) {
|
|
|
20
20
|
const lines = [`${pr.launcher}() {`];
|
|
21
21
|
for (const [k, v] of Object.entries(pr.env)) {
|
|
22
22
|
lines.push(v.startsWith(SECRET_PREFIX)
|
|
23
|
-
? ` export ${k}="$(
|
|
23
|
+
? ` export ${k}="$(ccprofiles secrets get ${v.slice(SECRET_PREFIX.length)})"`
|
|
24
24
|
: ` export ${k}="${escapePosix(v)}"`);
|
|
25
25
|
}
|
|
26
26
|
lines.push(` CLAUDE_CONFIG_DIR="${profileDirExpr(pr, p)}" claude "$@"`, '}');
|
|
@@ -30,7 +30,7 @@ function renderPwsh(pr, p) {
|
|
|
30
30
|
const lines = [`function ${pr.launcher} {`];
|
|
31
31
|
for (const [k, v] of Object.entries(pr.env)) {
|
|
32
32
|
lines.push(v.startsWith(SECRET_PREFIX)
|
|
33
|
-
? ` $env:${k} = (
|
|
33
|
+
? ` $env:${k} = (ccprofiles secrets get ${v.slice(SECRET_PREFIX.length)})`
|
|
34
34
|
: ` $env:${k} = "${escapePwsh(v)}"`);
|
|
35
35
|
}
|
|
36
36
|
lines.push(` $env:CLAUDE_CONFIG_DIR = "${profileDirExpr(pr, p)}"`, ' claude @args', '}');
|
package/dist/syncclient.js
CHANGED
|
@@ -9,7 +9,7 @@ async function post(host, port, path, payload) {
|
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
catch {
|
|
12
|
-
throw new Error(`cannot reach ${host}:${port} — is \`
|
|
12
|
+
throw new Error(`cannot reach ${host}:${port} — is \`ccprofiles serve\` running on the other device?`);
|
|
13
13
|
}
|
|
14
14
|
const data = await res.json();
|
|
15
15
|
if (!res.ok)
|
package/dist/syncserver.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function startSyncServer(deps) {
|
|
|
46
46
|
return send(res, 400, { error: 'invalid request body' });
|
|
47
47
|
if (req.url === '/pair') {
|
|
48
48
|
if (locked)
|
|
49
|
-
return send(res, 429, { error: 'pairing locked after too many failed attempts — restart
|
|
49
|
+
return send(res, 429, { error: 'pairing locked after too many failed attempts — restart ccprofiles serve' });
|
|
50
50
|
const { privateKey, publicRaw } = handshakeKeys();
|
|
51
51
|
const salt = newSalt();
|
|
52
52
|
const key = deriveSharedKey(privateKey, payload.clientPub, salt);
|
|
@@ -56,7 +56,7 @@ export async function startSyncServer(deps) {
|
|
|
56
56
|
}
|
|
57
57
|
if (req.url === '/pair/confirm') {
|
|
58
58
|
if (locked)
|
|
59
|
-
return send(res, 429, { error: 'pairing locked after too many failed attempts — restart
|
|
59
|
+
return send(res, 429, { error: 'pairing locked after too many failed attempts — restart ccprofiles serve' });
|
|
60
60
|
const pending = typeof payload.handshakeId === 'string' ? handshakes.get(payload.handshakeId) : undefined;
|
|
61
61
|
if (!pending)
|
|
62
62
|
return send(res, 400, { error: 'no pending handshake (call /pair first)' });
|
package/package.json
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccprofiles-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Core library for ccprofiles — discovery, manifest, secrets, apply, and LAN sync for Claude Code multi-account setups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
-
"files": [
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
9
11
|
"license": "MIT",
|
|
10
|
-
"repository": {
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/tmfadhlul/claude-code-profile.git",
|
|
15
|
+
"directory": "packages/core"
|
|
16
|
+
},
|
|
11
17
|
"homepage": "https://github.com/tmfadhlul/claude-code-profile#readme",
|
|
12
18
|
"bugs": "https://github.com/tmfadhlul/claude-code-profile/issues",
|
|
13
|
-
"keywords": [
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude-code",
|
|
21
|
+
"claude",
|
|
22
|
+
"profiles",
|
|
23
|
+
"mcp",
|
|
24
|
+
"config-management"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"yaml": "^2.5.0",
|
|
31
|
+
"zod": "^3.23.0"
|
|
32
|
+
}
|
|
16
33
|
}
|