ccprofiles-core 0.1.1 → 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/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/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/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/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
|
}
|