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 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
- export declare function planApply(m: Manifest, live: LiveProfile[], p: Platform): ApplyAction[];
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
- export function planApply(m, live, p) {
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] : a.kind === 'rc-block' ? [a.rcFile] : []);
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: ccp export)');
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: ccp export)');
14
+ throw new Error('not a ccprofiles bundle (expected a file created by: ccprofiles export)');
15
15
  return parsed;
16
16
  }
@@ -6,5 +6,6 @@ export interface LiveProfile {
6
6
  account: string | null;
7
7
  mcpServers: Record<string, McpServerDef>;
8
8
  links: Record<string, string>;
9
+ settingsEnv: Record<string, string>;
9
10
  }
10
11
  export declare function discoverProfiles(home: string): Promise<LiveProfile[]>;
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));
@@ -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}="$(ccp secrets get ${v.slice(SECRET_PREFIX.length)})"`
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} = (ccp secrets get ${v.slice(SECRET_PREFIX.length)})`
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', '}');
@@ -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 \`ccp serve\` running on the other device?`);
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)
@@ -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 ccp serve' });
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 ccp serve' });
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.1.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": ["dist"],
8
+ "files": [
9
+ "dist"
10
+ ],
9
11
  "license": "MIT",
10
- "repository": { "type": "git", "url": "git+https://github.com/tmfadhlul/claude-code-profile.git", "directory": "packages/core" },
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": ["claude-code", "claude", "profiles", "mcp", "config-management"],
14
- "engines": { "node": ">=20" },
15
- "dependencies": { "yaml": "^2.5.0", "zod": "^3.23.0" }
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
  }