ccprofiles-core 0.2.0 → 0.4.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.js CHANGED
@@ -33,6 +33,7 @@ export function buildManifest(live, platform) {
33
33
  links,
34
34
  mcp: Object.keys(lp.mcpServers).sort(),
35
35
  settingsEnv: lp.settingsEnv,
36
+ skipPermissions: false,
36
37
  };
37
38
  });
38
39
  return { version: 1, hub, profiles, mcpServers };
package/dist/devices.js CHANGED
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { atomicWrite } from './fsutil.js';
5
- const IGNORE = ['secrets.enc', 'secret-names.json', 'devices.json', 'backups/', '*.ccp-tmp'];
5
+ const IGNORE = ['secrets.enc', 'secrets.dpapi.json', 'secret-names.json', 'devices.json', 'backups/', '*.ccp-tmp'];
6
6
  export async function loadDevices(root) {
7
7
  const f = join(root, 'devices.json');
8
8
  if (!existsSync(f))
@@ -1,20 +1,32 @@
1
1
  import { z } from 'zod';
2
2
  export declare class ManifestError extends Error {
3
3
  }
4
- declare const McpServerSchema: z.ZodObject<{
5
- command: z.ZodString;
4
+ declare const McpServerSchema: z.ZodEffects<z.ZodObject<{
5
+ command: z.ZodOptional<z.ZodString>;
6
6
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
7
7
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
8
8
  type: z.ZodOptional<z.ZodString>;
9
9
  url: z.ZodOptional<z.ZodString>;
10
10
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
11
- command: z.ZodString;
11
+ command: z.ZodOptional<z.ZodString>;
12
12
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
13
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
14
14
  type: z.ZodOptional<z.ZodString>;
15
15
  url: z.ZodOptional<z.ZodString>;
16
16
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
17
- command: z.ZodString;
17
+ command: z.ZodOptional<z.ZodString>;
18
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
19
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
20
+ type: z.ZodOptional<z.ZodString>;
21
+ url: z.ZodOptional<z.ZodString>;
22
+ }, z.ZodTypeAny, "passthrough">>, z.objectOutputType<{
23
+ command: z.ZodOptional<z.ZodString>;
24
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
25
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
26
+ type: z.ZodOptional<z.ZodString>;
27
+ url: z.ZodOptional<z.ZodString>;
28
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
29
+ command: z.ZodOptional<z.ZodString>;
18
30
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
19
31
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
20
32
  type: z.ZodOptional<z.ZodString>;
@@ -29,6 +41,7 @@ declare const ProfileSchema: z.ZodObject<{
29
41
  links: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
30
42
  mcp: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
31
43
  settingsEnv: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
44
+ skipPermissions: z.ZodDefault<z.ZodBoolean>;
32
45
  }, "strip", z.ZodTypeAny, {
33
46
  env: Record<string, string>;
34
47
  name: string;
@@ -38,6 +51,7 @@ declare const ProfileSchema: z.ZodObject<{
38
51
  links: Record<string, string>;
39
52
  mcp: string[];
40
53
  settingsEnv: Record<string, string>;
54
+ skipPermissions: boolean;
41
55
  }, {
42
56
  name: string;
43
57
  dir: string;
@@ -47,6 +61,7 @@ declare const ProfileSchema: z.ZodObject<{
47
61
  links?: Record<string, string> | undefined;
48
62
  mcp?: string[] | undefined;
49
63
  settingsEnv?: Record<string, string> | undefined;
64
+ skipPermissions?: boolean | undefined;
50
65
  }>;
51
66
  declare const ManifestSchema: z.ZodObject<{
52
67
  version: z.ZodLiteral<1>;
@@ -60,6 +75,7 @@ declare const ManifestSchema: z.ZodObject<{
60
75
  links: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
61
76
  mcp: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
62
77
  settingsEnv: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
78
+ skipPermissions: z.ZodDefault<z.ZodBoolean>;
63
79
  }, "strip", z.ZodTypeAny, {
64
80
  env: Record<string, string>;
65
81
  name: string;
@@ -69,6 +85,7 @@ declare const ManifestSchema: z.ZodObject<{
69
85
  links: Record<string, string>;
70
86
  mcp: string[];
71
87
  settingsEnv: Record<string, string>;
88
+ skipPermissions: boolean;
72
89
  }, {
73
90
  name: string;
74
91
  dir: string;
@@ -78,21 +95,34 @@ declare const ManifestSchema: z.ZodObject<{
78
95
  links?: Record<string, string> | undefined;
79
96
  mcp?: string[] | undefined;
80
97
  settingsEnv?: Record<string, string> | undefined;
98
+ skipPermissions?: boolean | undefined;
81
99
  }>, "many">;
82
- mcpServers: z.ZodRecord<z.ZodString, z.ZodObject<{
83
- command: z.ZodString;
100
+ mcpServers: z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
101
+ command: z.ZodOptional<z.ZodString>;
84
102
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
85
103
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
86
104
  type: z.ZodOptional<z.ZodString>;
87
105
  url: z.ZodOptional<z.ZodString>;
88
106
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
89
- command: z.ZodString;
107
+ command: z.ZodOptional<z.ZodString>;
108
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
109
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
110
+ type: z.ZodOptional<z.ZodString>;
111
+ url: z.ZodOptional<z.ZodString>;
112
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
113
+ command: z.ZodOptional<z.ZodString>;
114
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
115
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
116
+ type: z.ZodOptional<z.ZodString>;
117
+ url: z.ZodOptional<z.ZodString>;
118
+ }, z.ZodTypeAny, "passthrough">>, z.objectOutputType<{
119
+ command: z.ZodOptional<z.ZodString>;
90
120
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
91
121
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
92
122
  type: z.ZodOptional<z.ZodString>;
93
123
  url: z.ZodOptional<z.ZodString>;
94
124
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
95
- command: z.ZodString;
125
+ command: z.ZodOptional<z.ZodString>;
96
126
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
97
127
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
98
128
  type: z.ZodOptional<z.ZodString>;
@@ -110,9 +140,10 @@ declare const ManifestSchema: z.ZodObject<{
110
140
  links: Record<string, string>;
111
141
  mcp: string[];
112
142
  settingsEnv: Record<string, string>;
143
+ skipPermissions: boolean;
113
144
  }[];
114
145
  mcpServers: Record<string, z.objectOutputType<{
115
- command: z.ZodString;
146
+ command: z.ZodOptional<z.ZodString>;
116
147
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
117
148
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
118
149
  type: z.ZodOptional<z.ZodString>;
@@ -130,9 +161,10 @@ declare const ManifestSchema: z.ZodObject<{
130
161
  links?: Record<string, string> | undefined;
131
162
  mcp?: string[] | undefined;
132
163
  settingsEnv?: Record<string, string> | undefined;
164
+ skipPermissions?: boolean | undefined;
133
165
  }[];
134
166
  mcpServers: Record<string, z.objectInputType<{
135
- command: z.ZodString;
167
+ command: z.ZodOptional<z.ZodString>;
136
168
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
137
169
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
138
170
  type: z.ZodOptional<z.ZodString>;
package/dist/manifest.js CHANGED
@@ -8,12 +8,12 @@ const exec = promisify(execFile);
8
8
  export class ManifestError extends Error {
9
9
  }
10
10
  const McpServerSchema = z.object({
11
- command: z.string(),
11
+ command: z.string().optional(),
12
12
  args: z.array(z.string()).optional(),
13
13
  env: z.record(z.string()).optional(),
14
14
  type: z.string().optional(),
15
15
  url: z.string().optional(),
16
- }).passthrough();
16
+ }).passthrough().refine(s => s.command !== undefined || s.url !== undefined, { message: 'mcp server must have either "command" (local) or "url" (remote)' });
17
17
  const ProfileSchema = z.object({
18
18
  name: z.string().min(1),
19
19
  dir: z.string().min(1),
@@ -23,6 +23,7 @@ const ProfileSchema = z.object({
23
23
  links: z.record(z.string()).default({}),
24
24
  mcp: z.array(z.string()).default([]),
25
25
  settingsEnv: z.record(z.string()).default({}),
26
+ skipPermissions: z.boolean().default(false),
26
27
  });
27
28
  const ManifestSchema = z.object({
28
29
  version: z.literal(1),
@@ -94,8 +95,10 @@ export async function loadManifest(root) {
94
95
  return parseManifest(await readFile(join(root, 'manifest.yaml'), 'utf8'));
95
96
  }
96
97
  export async function saveManifest(root, m) {
98
+ const yaml = serializeManifest(m);
99
+ parseManifest(yaml); // guard: never write a manifest that cannot be reloaded
97
100
  await mkdir(root, { recursive: true });
98
- await writeFile(join(root, 'manifest.yaml'), serializeManifest(m), 'utf8');
101
+ await writeFile(join(root, 'manifest.yaml'), yaml, 'utf8');
99
102
  try {
100
103
  await exec('git', ['-C', root, 'rev-parse', '--git-dir']);
101
104
  await exec('git', ['-C', root, 'add', '-A']);
package/dist/rcblock.js CHANGED
@@ -23,7 +23,8 @@ function renderPosix(pr, p) {
23
23
  ? ` export ${k}="$(ccprofiles secrets get ${v.slice(SECRET_PREFIX.length)})"`
24
24
  : ` export ${k}="${escapePosix(v)}"`);
25
25
  }
26
- lines.push(` CLAUDE_CONFIG_DIR="${profileDirExpr(pr, p)}" claude "$@"`, '}');
26
+ const flag = pr.skipPermissions ? ' --dangerously-skip-permissions' : '';
27
+ lines.push(` CLAUDE_CONFIG_DIR="${profileDirExpr(pr, p)}" claude${flag} "$@"`, '}');
27
28
  return lines.join('\n');
28
29
  }
29
30
  function renderPwsh(pr, p) {
@@ -33,7 +34,8 @@ function renderPwsh(pr, p) {
33
34
  ? ` $env:${k} = (ccprofiles secrets get ${v.slice(SECRET_PREFIX.length)})`
34
35
  : ` $env:${k} = "${escapePwsh(v)}"`);
35
36
  }
36
- lines.push(` $env:CLAUDE_CONFIG_DIR = "${profileDirExpr(pr, p)}"`, ' claude @args', '}');
37
+ const flag = pr.skipPermissions ? ' --dangerously-skip-permissions' : '';
38
+ lines.push(` $env:CLAUDE_CONFIG_DIR = "${profileDirExpr(pr, p)}"`, ` claude${flag} @args`, '}');
37
39
  return lines.join('\n');
38
40
  }
39
41
  export function renderRcBlock(m, p) {
package/dist/secrets.d.ts CHANGED
@@ -35,6 +35,21 @@ export declare class SecretToolBackend implements SecretsBackend {
35
35
  get(key: string): Promise<string | null>;
36
36
  delete(key: string): Promise<void>;
37
37
  }
38
+ export type DpapiCrypt = {
39
+ protect(plain: string): Promise<string>;
40
+ unprotect(b64: string): Promise<string>;
41
+ };
42
+ export declare const powershellDpapi: DpapiCrypt;
43
+ export declare class DpapiBackend implements SecretsBackend {
44
+ private filePath;
45
+ private crypt;
46
+ readonly name = "windows-dpapi";
47
+ constructor(filePath: string, crypt?: DpapiCrypt);
48
+ private load;
49
+ get(key: string): Promise<string | null>;
50
+ set(key: string, value: string): Promise<void>;
51
+ delete(key: string): Promise<void>;
52
+ }
38
53
  export declare class SecretsStore {
39
54
  private backend;
40
55
  private indexPath;
package/dist/secrets.js CHANGED
@@ -100,6 +100,64 @@ export class SecretToolBackend {
100
100
  catch { /* absent */ }
101
101
  }
102
102
  }
103
+ // DPAPI via built-in PowerShell. Secret bytes travel through a spawn env var, never argv.
104
+ export const powershellDpapi = {
105
+ protect: (plain) => runDpapi(
106
+ // [string] cast coerces a $null CCP_IN (empty env var) to '' before GetBytes, which
107
+ // otherwise throws on $null. Only needed here — unprotect's CCP_IN is base64 ciphertext
108
+ // and is never empty for a stored value.
109
+ "$b=[Text.Encoding]::UTF8.GetBytes([string]$env:CCP_IN);" +
110
+ "[Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect($b,$null,'CurrentUser'))", plain),
111
+ unprotect: async (b64) => {
112
+ // PS emits base64 of the *decrypted bytes* (not the decoded string) so runDpapi's
113
+ // trim is always ASCII-safe and we never depend on the PowerShell console's
114
+ // stdout encoding to round-trip arbitrary plaintext (e.g. non-ASCII, trailing whitespace).
115
+ const out = await runDpapi("$b=[Convert]::FromBase64String($env:CCP_IN);" +
116
+ "[Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser'))", b64);
117
+ return Buffer.from(out, 'base64').toString('utf8');
118
+ },
119
+ };
120
+ async function runDpapi(script, input) {
121
+ const child = await import('node:child_process');
122
+ return new Promise((resolve, reject) => {
123
+ const proc = child.spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', 'Add-Type -AssemblyName System.Security;' + script], { env: { ...process.env, CCP_IN: input }, stdio: ['ignore', 'pipe', 'pipe'] });
124
+ let out = '';
125
+ let err = '';
126
+ proc.stdout.on('data', d => { out += d; });
127
+ proc.stderr.on('data', d => { err += d; });
128
+ proc.on('error', reject);
129
+ proc.on('close', code => (code === 0 ? resolve(out.trim()) : reject(new Error(`powershell dpapi exited ${code}: ${err.trim()}`))));
130
+ });
131
+ }
132
+ export class DpapiBackend {
133
+ filePath;
134
+ crypt;
135
+ name = 'windows-dpapi';
136
+ constructor(filePath, crypt = powershellDpapi) {
137
+ this.filePath = filePath;
138
+ this.crypt = crypt;
139
+ }
140
+ async load() {
141
+ if (!existsSync(this.filePath))
142
+ return { entries: {} };
143
+ return JSON.parse(await readFile(this.filePath, 'utf8'));
144
+ }
145
+ async get(key) {
146
+ const f = await this.load();
147
+ const ct = f.entries[key];
148
+ return ct === undefined ? null : this.crypt.unprotect(ct);
149
+ }
150
+ async set(key, value) {
151
+ const f = await this.load();
152
+ f.entries[key] = await this.crypt.protect(value);
153
+ await atomicWrite(this.filePath, JSON.stringify(f));
154
+ }
155
+ async delete(key) {
156
+ const f = await this.load();
157
+ delete f.entries[key];
158
+ await atomicWrite(this.filePath, JSON.stringify(f));
159
+ }
160
+ }
103
161
  export class SecretsStore {
104
162
  backend;
105
163
  indexPath;
@@ -137,6 +195,8 @@ export async function defaultBackend(p, opts) {
137
195
  }
138
196
  catch { /* fall through */ }
139
197
  }
198
+ if (p.os === 'win32')
199
+ return new DpapiBackend(opts.filePath.replace(/\.enc$/, '.dpapi.json'));
140
200
  const pw = opts.passphrase ? await opts.passphrase() : '';
141
201
  if (!pw)
142
202
  throw new Error('encrypted-file backend requires a passphrase');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccprofiles-core",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",