@cprof/cli 0.0.1-alpha.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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/command-utils.d.ts +12 -0
- package/dist/command-utils.d.ts.map +1 -0
- package/dist/command-utils.js +38 -0
- package/dist/commands/diff.d.ts +7 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +21 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +66 -0
- package/dist/commands/install.d.ts +9 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +73 -0
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.d.ts.map +1 -0
- package/dist/commands/profiles.js +47 -0
- package/dist/commands/refresh.d.ts +9 -0
- package/dist/commands/refresh.d.ts.map +1 -0
- package/dist/commands/refresh.js +48 -0
- package/dist/commands/validate.d.ts +7 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +22 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +110 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vedant Nandoskar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# cprof
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@cprof/cli)
|
|
4
|
+
[](https://github.com/Vedant1202/claude-prof/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Snapshot, scrub, and migrate your Claude Code setup as a redacted, portable profile.
|
|
7
|
+
|
|
8
|
+
> **Alpha.** cprof is early and the profile format may still change. Redaction is
|
|
9
|
+
> best-effort — always review a generated profile before sharing it.
|
|
10
|
+
|
|
11
|
+
`cprof` captures your project or global Claude Code configuration — settings, MCP
|
|
12
|
+
servers, `CLAUDE.md` memory, rules, skills, commands, agents, and plugin
|
|
13
|
+
inventory — into a deterministic, schema-valid, **secret-redacted**
|
|
14
|
+
`claude-profile.json`. You can then `diff`, `validate`, and `install` a trusted
|
|
15
|
+
profile onto another machine with a non-destructive deep merge. It runs fully
|
|
16
|
+
offline and never executes hook or plugin code.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @cprof/cli@alpha
|
|
22
|
+
# or run it without installing:
|
|
23
|
+
npx @cprof/cli@alpha --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The npm package is **`@cprof/cli`**; the installed command is **`cprof`**.
|
|
27
|
+
Requires Node.js >= 22.
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 1. Snapshot the current project into claude-profile.json
|
|
33
|
+
cprof init
|
|
34
|
+
|
|
35
|
+
# 2. Review what was captured — and confirm secrets are redacted
|
|
36
|
+
cat claude-profile.json
|
|
37
|
+
|
|
38
|
+
# 3. On another machine, apply it with a non-destructive deep merge
|
|
39
|
+
cprof install claude-profile.json --dry-run # preview the write plan
|
|
40
|
+
cprof install claude-profile.json # apply it
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
| Command | What it does |
|
|
46
|
+
| -------------------------------------------------- | --------------------------------------------------------------- |
|
|
47
|
+
| `cprof init [--global \| --include-global]` | Snapshot the current setup into `claude-profile.json` |
|
|
48
|
+
| `cprof refresh` | Rebuild the profile from its recorded source scope |
|
|
49
|
+
| `cprof install <file> [--dry-run] [--force] [...]` | Apply a trusted profile (deep merge; backs up before overwrite) |
|
|
50
|
+
| `cprof validate <file>` | Validate a profile against the schema |
|
|
51
|
+
| `cprof diff <a.json> <b.json>` | Compare two profiles semantically |
|
|
52
|
+
| `cprof profiles list` | List profiles recorded by local installs |
|
|
53
|
+
|
|
54
|
+
Run `cprof --help` for the full usage.
|
|
55
|
+
|
|
56
|
+
## Redaction & limits
|
|
57
|
+
|
|
58
|
+
Secrets are redacted on capture: provider keys (via
|
|
59
|
+
[secretlint](https://github.com/secretlint/secretlint)), secret-like key names,
|
|
60
|
+
JWTs, and high-entropy values become `${env:NAME}` placeholders, and the
|
|
61
|
+
generated manifest is re-scanned before it is written. It will **not** catch
|
|
62
|
+
low-entropy secrets stored under non-sensitive keys. Review every profile before
|
|
63
|
+
sharing it.
|
|
64
|
+
|
|
65
|
+
## Documentation
|
|
66
|
+
|
|
67
|
+
Full docs: **https://vedant1202.github.io/claude-prof/** · Source & issues:
|
|
68
|
+
[github.com/Vedant1202/claude-prof](https://github.com/Vedant1202/claude-prof)
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CprofProfile } from "@cprof/schema";
|
|
2
|
+
export type CommandWriter = Pick<NodeJS.WriteStream, "write">;
|
|
3
|
+
export type ReadProfileFileResult = {
|
|
4
|
+
readonly ok: true;
|
|
5
|
+
readonly profile: CprofProfile;
|
|
6
|
+
} | {
|
|
7
|
+
readonly ok: false;
|
|
8
|
+
readonly exitCode: 1 | 2;
|
|
9
|
+
readonly errors: readonly string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function readProfileFile(filePath: string): Promise<ReadProfileFileResult>;
|
|
12
|
+
//# sourceMappingURL=command-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-utils.d.ts","sourceRoot":"","sources":["../src/command-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AAE9D,MAAM,MAAM,qBAAqB,GAC7B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GACrD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC,CAAC;AAEN,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,qBAAqB,CAAC,CAmChC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { validateProfile } from "@cprof/core";
|
|
3
|
+
export async function readProfileFile(filePath) {
|
|
4
|
+
let contents;
|
|
5
|
+
try {
|
|
6
|
+
contents = await readFile(filePath, "utf8");
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
exitCode: 2,
|
|
13
|
+
errors: [`file not found: ${filePath}`],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const value = JSON.parse(contents);
|
|
20
|
+
const validation = validateProfile(value);
|
|
21
|
+
if (!validation.valid) {
|
|
22
|
+
return { ok: false, exitCode: 1, errors: validation.errors };
|
|
23
|
+
}
|
|
24
|
+
return { ok: true, profile: value };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
errors: [
|
|
31
|
+
error instanceof Error ? error.message : "profile JSON is invalid",
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function isNodeError(error) {
|
|
37
|
+
return error instanceof Error && "code" in error;
|
|
38
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface DiffCommandOptions {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly stdout: Pick<NodeJS.WriteStream, "write">;
|
|
4
|
+
readonly stderr: Pick<NodeJS.WriteStream, "write">;
|
|
5
|
+
}
|
|
6
|
+
export declare function runDiff(flags: readonly string[], options: DiffCommandOptions): Promise<number>;
|
|
7
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/commands/diff.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CACpD;AAED,wBAAsB,OAAO,CAC3B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAoBjB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { diffProfiles, formatProfileDiff } from "@cprof/core";
|
|
4
|
+
export async function runDiff(flags, options) {
|
|
5
|
+
const json = flags.includes("--json");
|
|
6
|
+
const paths = flags.filter((flag) => flag !== "--json");
|
|
7
|
+
if (paths.length !== 2) {
|
|
8
|
+
options.stderr.write("usage: cprof diff [--json] <a.json> <b.json>\n");
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
const leftPath = paths[0];
|
|
12
|
+
const rightPath = paths[1];
|
|
13
|
+
const left = await readJson(resolve(options.cwd, leftPath));
|
|
14
|
+
const right = await readJson(resolve(options.cwd, rightPath));
|
|
15
|
+
const diff = diffProfiles(left, right);
|
|
16
|
+
options.stdout.write(json ? `${JSON.stringify(diff, null, 2)}\n` : formatProfileDiff(diff));
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
async function readJson(filePath) {
|
|
20
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface InitCommandOptions {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly homeDir?: string;
|
|
4
|
+
readonly stdout: Pick<NodeJS.WriteStream, "write">;
|
|
5
|
+
readonly stderr: Pick<NodeJS.WriteStream, "write">;
|
|
6
|
+
}
|
|
7
|
+
export declare function runInit(flags: readonly string[], options: InitCommandOptions): Promise<number>;
|
|
8
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CACpD;AAED,wBAAsB,OAAO,CAC3B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CA0DjB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { createProfileGitignore, createScanReport, scanClaudeProfile, validateProfile, } from "@cprof/core";
|
|
5
|
+
export async function runInit(flags, options) {
|
|
6
|
+
const parsed = parseInitFlags(flags);
|
|
7
|
+
if (parsed.valid === false) {
|
|
8
|
+
options.stderr.write(`${parsed.error}\n`);
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
const scan = await scanClaudeProfile({
|
|
12
|
+
name: createProfileName(options.cwd, parsed.mode, parsed.includeGlobal),
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
cwd: options.cwd,
|
|
15
|
+
homeDir: options.homeDir ?? homedir(),
|
|
16
|
+
outputRoot: options.cwd,
|
|
17
|
+
mode: parsed.mode,
|
|
18
|
+
includeGlobal: parsed.mode === "project" ? parsed.includeGlobal : false,
|
|
19
|
+
});
|
|
20
|
+
const manifest = scan.manifest;
|
|
21
|
+
const validation = validateProfile(manifest);
|
|
22
|
+
if (!validation.valid) {
|
|
23
|
+
options.stderr.write(`${validation.errors.join("\n")}\n`);
|
|
24
|
+
return validation.exitCode;
|
|
25
|
+
}
|
|
26
|
+
if (!scan.leakCheck.ok) {
|
|
27
|
+
const leakedPaths = [
|
|
28
|
+
...new Set(scan.leakCheck.leaks.map((leak) => leak.path)),
|
|
29
|
+
];
|
|
30
|
+
options.stderr.write(`refusing to write: redaction left a secret in ${leakedPaths.join(", ")}\n`);
|
|
31
|
+
return 3;
|
|
32
|
+
}
|
|
33
|
+
await writeFile(join(options.cwd, "claude-profile.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
34
|
+
await writeFile(join(options.cwd, ".gitignore"), createProfileGitignore(), "utf8");
|
|
35
|
+
await writeFile(join(options.cwd, "cprof-scan-report.txt"), createScanReport(scan.report), "utf8");
|
|
36
|
+
options.stdout.write(`Wrote claude-profile.json (${manifest.profileScope}${manifest.includesGlobal ? " + global" : ""})\n`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
function parseInitFlags(flags) {
|
|
40
|
+
const supportedFlags = new Set(["--global", "--include-global"]);
|
|
41
|
+
const unknownFlag = flags.find((flag) => !supportedFlags.has(flag));
|
|
42
|
+
if (unknownFlag !== undefined) {
|
|
43
|
+
return { valid: false, error: `unknown init flag: ${unknownFlag}` };
|
|
44
|
+
}
|
|
45
|
+
if (flags.includes("--global") && flags.includes("--include-global")) {
|
|
46
|
+
return {
|
|
47
|
+
valid: false,
|
|
48
|
+
error: "init cannot combine --global and --include-global",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (flags.includes("--global")) {
|
|
52
|
+
return { valid: true, mode: "global", includeGlobal: false };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
valid: true,
|
|
56
|
+
mode: "project",
|
|
57
|
+
includeGlobal: flags.includes("--include-global"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function createProfileName(cwd, mode, includeGlobal) {
|
|
61
|
+
if (mode === "global") {
|
|
62
|
+
return "global-profile";
|
|
63
|
+
}
|
|
64
|
+
const projectName = basename(resolve(cwd)) || "project";
|
|
65
|
+
return includeGlobal ? `${projectName}-with-global` : projectName;
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface InstallCommandOptions {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly homeDir?: string;
|
|
4
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
5
|
+
readonly stdout: Pick<NodeJS.WriteStream, "write">;
|
|
6
|
+
readonly stderr: Pick<NodeJS.WriteStream, "write">;
|
|
7
|
+
}
|
|
8
|
+
export declare function runInstall(flags: readonly string[], options: InstallCommandOptions): Promise<number>;
|
|
9
|
+
//# sourceMappingURL=install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CACpD;AAUD,wBAAsB,UAAU,CAC9B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,CA6BjB"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { installProfile } from "@cprof/core";
|
|
4
|
+
export async function runInstall(flags, options) {
|
|
5
|
+
const parsed = parseInstallFlags(flags);
|
|
6
|
+
if (parsed.valid === false) {
|
|
7
|
+
options.stderr.write(`${parsed.error}\n`);
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
const result = await installProfile({
|
|
11
|
+
profilePath: resolve(options.cwd, parsed.profilePath),
|
|
12
|
+
cwd: options.cwd,
|
|
13
|
+
homeDir: options.homeDir ?? homedir(),
|
|
14
|
+
env: options.env,
|
|
15
|
+
dryRun: parsed.dryRun,
|
|
16
|
+
force: parsed.force,
|
|
17
|
+
scope: parsed.scope,
|
|
18
|
+
installSource: parsed.profilePath,
|
|
19
|
+
});
|
|
20
|
+
const output = result.ok ? options.stdout : options.stderr;
|
|
21
|
+
output.write(result.report);
|
|
22
|
+
if (result.ok) {
|
|
23
|
+
options.stdout.write(`${parsed.dryRun ? "Planned" : "Installed"} ${result.writes.length} writes\n`);
|
|
24
|
+
}
|
|
25
|
+
return result.exitCode;
|
|
26
|
+
}
|
|
27
|
+
function parseInstallFlags(flags) {
|
|
28
|
+
let profilePath;
|
|
29
|
+
let dryRun = false;
|
|
30
|
+
let force = false;
|
|
31
|
+
let scope;
|
|
32
|
+
for (const flag of flags) {
|
|
33
|
+
if (flag === "--dry-run") {
|
|
34
|
+
dryRun = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (flag === "--force") {
|
|
38
|
+
force = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (flag === "--global") {
|
|
42
|
+
scope = "global";
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (flag === "--include-global") {
|
|
46
|
+
scope = "include-global";
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (flag.startsWith("--")) {
|
|
50
|
+
return { valid: false, error: `unknown install flag: ${flag}` };
|
|
51
|
+
}
|
|
52
|
+
if (profilePath !== undefined) {
|
|
53
|
+
return { valid: false, error: `unexpected install argument: ${flag}` };
|
|
54
|
+
}
|
|
55
|
+
profilePath = flag;
|
|
56
|
+
}
|
|
57
|
+
if (profilePath === undefined) {
|
|
58
|
+
return { valid: false, error: "install requires a profile path" };
|
|
59
|
+
}
|
|
60
|
+
if (flags.includes("--global") && flags.includes("--include-global")) {
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
error: "install cannot combine --global and --include-global",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
valid: true,
|
|
68
|
+
profilePath,
|
|
69
|
+
dryRun,
|
|
70
|
+
force,
|
|
71
|
+
scope,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ProfilesCommandOptions {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly homeDir?: string;
|
|
4
|
+
readonly stdout: Pick<NodeJS.WriteStream, "write">;
|
|
5
|
+
readonly stderr: Pick<NodeJS.WriteStream, "write">;
|
|
6
|
+
}
|
|
7
|
+
export declare function runProfiles(flags: readonly string[], options: ProfilesCommandOptions): Promise<number>;
|
|
8
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../src/commands/profiles.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CACpD;AAQD,wBAAsB,WAAW,CAC/B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAcjB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { loadInstalledProfileState, } from "@cprof/core";
|
|
4
|
+
export async function runProfiles(flags, options) {
|
|
5
|
+
const parsed = parseProfilesFlags(flags);
|
|
6
|
+
if (!parsed.valid) {
|
|
7
|
+
options.stderr.write(`${parsed.error}\n`);
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
const state = await loadInstalledProfileState(statePath(options.cwd, options.homeDir ?? homedir(), parsed.global));
|
|
11
|
+
options.stdout.write(formatInstalls(state.installs, parsed.json));
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
function parseProfilesFlags(flags) {
|
|
15
|
+
const global = flags.includes("--global");
|
|
16
|
+
const json = flags.includes("--json");
|
|
17
|
+
const positional = flags.filter((flag) => flag !== "--global" && flag !== "--json");
|
|
18
|
+
const unknownFlag = positional.find((flag) => flag.startsWith("--"));
|
|
19
|
+
if (unknownFlag !== undefined) {
|
|
20
|
+
return { valid: false, error: `unknown profiles flag: ${unknownFlag}` };
|
|
21
|
+
}
|
|
22
|
+
const [action, extra] = positional;
|
|
23
|
+
if (action !== "list") {
|
|
24
|
+
return { valid: false, error: "profiles requires action: list" };
|
|
25
|
+
}
|
|
26
|
+
if (extra !== undefined) {
|
|
27
|
+
return { valid: false, error: `unexpected profiles argument: ${extra}` };
|
|
28
|
+
}
|
|
29
|
+
return { valid: true, global, json };
|
|
30
|
+
}
|
|
31
|
+
function statePath(cwd, homeDir, global) {
|
|
32
|
+
return global
|
|
33
|
+
? join(resolve(homeDir), ".claude", ".cprof-state.json")
|
|
34
|
+
: join(resolve(cwd), ".cprof-state.json");
|
|
35
|
+
}
|
|
36
|
+
function formatInstalls(installs, json) {
|
|
37
|
+
if (json) {
|
|
38
|
+
return `${JSON.stringify({ installs }, null, 2)}\n`;
|
|
39
|
+
}
|
|
40
|
+
if (installs.length === 0) {
|
|
41
|
+
return "No installed profiles recorded.\n";
|
|
42
|
+
}
|
|
43
|
+
return `${installs.map(formatInstallLine).join("\n")}\n`;
|
|
44
|
+
}
|
|
45
|
+
function formatInstallLine(install) {
|
|
46
|
+
return `${install.name} ${install.version} (${install.target}) - ${install.source}`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CommandWriter } from "../command-utils.js";
|
|
2
|
+
export interface RefreshCommandOptions {
|
|
3
|
+
readonly cwd: string;
|
|
4
|
+
readonly homeDir?: string;
|
|
5
|
+
readonly stdout: CommandWriter;
|
|
6
|
+
readonly stderr: CommandWriter;
|
|
7
|
+
}
|
|
8
|
+
export declare function runRefresh(flags: readonly string[], options: RefreshCommandOptions): Promise<number>;
|
|
9
|
+
//# sourceMappingURL=refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../src/commands/refresh.ts"],"names":[],"mappings":"AAWA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE1E,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC;AAED,wBAAsB,UAAU,CAC9B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,CAgEjB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createProfileGitignore, createScanReport, scanClaudeProfile, validateProfile, } from "@cprof/core";
|
|
5
|
+
import { readProfileFile } from "../command-utils.js";
|
|
6
|
+
export async function runRefresh(flags, options) {
|
|
7
|
+
if (flags.length > 0) {
|
|
8
|
+
options.stderr.write(`unknown refresh flag: ${flags[0]}\n`);
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
const profilePath = join(options.cwd, "claude-profile.json");
|
|
12
|
+
const existing = await readProfileFile(profilePath);
|
|
13
|
+
if (!existing.ok) {
|
|
14
|
+
options.stderr.write(`${existing.errors.join("\n")}\n`);
|
|
15
|
+
return existing.exitCode;
|
|
16
|
+
}
|
|
17
|
+
const scan = await scanClaudeProfile({
|
|
18
|
+
name: existing.profile.name,
|
|
19
|
+
version: existing.profile.version,
|
|
20
|
+
description: existing.profile.description,
|
|
21
|
+
claudeCode: existing.profile.claudeCode,
|
|
22
|
+
cwd: options.cwd,
|
|
23
|
+
homeDir: options.homeDir ?? homedir(),
|
|
24
|
+
outputRoot: options.cwd,
|
|
25
|
+
mode: existing.profile.profileScope,
|
|
26
|
+
includeGlobal: existing.profile.profileScope === "project"
|
|
27
|
+
? existing.profile.includesGlobal
|
|
28
|
+
: false,
|
|
29
|
+
});
|
|
30
|
+
const refreshed = scan.manifest;
|
|
31
|
+
const validation = validateProfile(refreshed);
|
|
32
|
+
if (!validation.valid) {
|
|
33
|
+
options.stderr.write(`${validation.errors.join("\n")}\n`);
|
|
34
|
+
return validation.exitCode;
|
|
35
|
+
}
|
|
36
|
+
if (!scan.leakCheck.ok) {
|
|
37
|
+
const leakedPaths = [
|
|
38
|
+
...new Set(scan.leakCheck.leaks.map((leak) => leak.path)),
|
|
39
|
+
];
|
|
40
|
+
options.stderr.write(`refusing to write: redaction left a secret in ${leakedPaths.join(", ")}\n`);
|
|
41
|
+
return 3;
|
|
42
|
+
}
|
|
43
|
+
await writeFile(profilePath, `${JSON.stringify(refreshed, null, 2)}\n`, "utf8");
|
|
44
|
+
await writeFile(join(options.cwd, ".gitignore"), createProfileGitignore(), "utf8");
|
|
45
|
+
await writeFile(join(options.cwd, "cprof-scan-report.txt"), createScanReport(scan.report), "utf8");
|
|
46
|
+
options.stdout.write("Refreshed claude-profile.json\n");
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ValidateCommandOptions {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
readonly stdout: Pick<NodeJS.WriteStream, "write">;
|
|
4
|
+
readonly stderr: Pick<NodeJS.WriteStream, "write">;
|
|
5
|
+
}
|
|
6
|
+
export declare function runValidate(flags: readonly string[], options: ValidateCommandOptions): Promise<number>;
|
|
7
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CACpD;AAED,wBAAsB,WAAW,CAC/B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { validateProfileFile } from "@cprof/core";
|
|
3
|
+
export async function runValidate(flags, options) {
|
|
4
|
+
const json = flags.includes("--json");
|
|
5
|
+
const paths = flags.filter((flag) => flag !== "--json");
|
|
6
|
+
const filePath = paths[0];
|
|
7
|
+
if (filePath === undefined || paths.length > 1) {
|
|
8
|
+
options.stderr.write("usage: cprof validate [--json] <file>\n");
|
|
9
|
+
return 1;
|
|
10
|
+
}
|
|
11
|
+
const result = await validateProfileFile(resolve(options.cwd, filePath));
|
|
12
|
+
if (json) {
|
|
13
|
+
options.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
14
|
+
}
|
|
15
|
+
else if (result.valid) {
|
|
16
|
+
options.stdout.write("valid\n");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
options.stderr.write(`${result.errors.join("\n")}\n`);
|
|
20
|
+
}
|
|
21
|
+
return result.exitCode;
|
|
22
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface MainOptions {
|
|
3
|
+
readonly cwd?: string;
|
|
4
|
+
readonly homeDir?: string;
|
|
5
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
6
|
+
readonly stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
7
|
+
readonly stderr?: Pick<NodeJS.WriteStream, "write">;
|
|
8
|
+
}
|
|
9
|
+
export declare function main(argv?: readonly string[], options?: MainOptions): Promise<number>;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAaA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;CACrD;AAyCD,wBAAsB,IAAI,CACxB,IAAI,GAAE,SAAS,MAAM,EAA0B,EAC/C,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,MAAM,CAAC,CA8EjB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runDiff } from "./commands/diff.js";
|
|
3
|
+
import { runInit } from "./commands/init.js";
|
|
4
|
+
import { runInstall } from "./commands/install.js";
|
|
5
|
+
import { runProfiles } from "./commands/profiles.js";
|
|
6
|
+
import { runRefresh } from "./commands/refresh.js";
|
|
7
|
+
import { runValidate } from "./commands/validate.js";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
const USAGE = `cprof — snapshot, scrub, and migrate your Claude Code setup
|
|
12
|
+
|
|
13
|
+
Usage: cprof <command> [options]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
init [--global | --include-global] Snapshot the current setup into claude-profile.json
|
|
17
|
+
refresh Rebuild the profile from its recorded source scope
|
|
18
|
+
install <file> [--dry-run] [--force] [--global | --include-global]
|
|
19
|
+
Apply a trusted profile to this machine (deep merge)
|
|
20
|
+
validate <file> Validate a profile against the schema
|
|
21
|
+
diff <a.json> <b.json> Compare two profiles semantically
|
|
22
|
+
profiles list List profiles recorded by local installs
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
-h, --help Show this help
|
|
26
|
+
-v, --version Show the version
|
|
27
|
+
|
|
28
|
+
Profiles are local-first and secret-redacted on capture, but redaction is
|
|
29
|
+
best-effort — always review a profile before sharing it.
|
|
30
|
+
|
|
31
|
+
Docs: https://vedant1202.github.io/claude-prof/
|
|
32
|
+
`;
|
|
33
|
+
function readVersion() {
|
|
34
|
+
try {
|
|
35
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
36
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
37
|
+
return pkg.version ?? "0.0.0";
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return "0.0.0";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function main(argv = process.argv.slice(2), options = {}) {
|
|
44
|
+
const stdout = options.stdout ?? process.stdout;
|
|
45
|
+
const stderr = options.stderr ?? process.stderr;
|
|
46
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
47
|
+
stdout.write(`${readVersion()}\n`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
if (argv.length === 0 ||
|
|
51
|
+
argv[0] === "help" ||
|
|
52
|
+
argv.includes("--help") ||
|
|
53
|
+
argv.includes("-h")) {
|
|
54
|
+
stdout.write(USAGE);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
const [command, ...flags] = argv;
|
|
58
|
+
if (command === "init") {
|
|
59
|
+
return runInit(flags, {
|
|
60
|
+
cwd: options.cwd ?? process.cwd(),
|
|
61
|
+
homeDir: options.homeDir,
|
|
62
|
+
stdout,
|
|
63
|
+
stderr,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (command === "refresh") {
|
|
67
|
+
return runRefresh(flags, {
|
|
68
|
+
cwd: options.cwd ?? process.cwd(),
|
|
69
|
+
homeDir: options.homeDir,
|
|
70
|
+
stdout,
|
|
71
|
+
stderr,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (command === "install") {
|
|
75
|
+
return runInstall(flags, {
|
|
76
|
+
cwd: options.cwd ?? process.cwd(),
|
|
77
|
+
homeDir: options.homeDir,
|
|
78
|
+
env: options.env,
|
|
79
|
+
stdout,
|
|
80
|
+
stderr,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (command === "validate") {
|
|
84
|
+
return runValidate(flags, {
|
|
85
|
+
cwd: options.cwd ?? process.cwd(),
|
|
86
|
+
stdout,
|
|
87
|
+
stderr,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (command === "profiles") {
|
|
91
|
+
return runProfiles(flags, {
|
|
92
|
+
cwd: options.cwd ?? process.cwd(),
|
|
93
|
+
homeDir: options.homeDir,
|
|
94
|
+
stdout,
|
|
95
|
+
stderr,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (command === "diff") {
|
|
99
|
+
return runDiff(flags, {
|
|
100
|
+
cwd: options.cwd ?? process.cwd(),
|
|
101
|
+
stdout,
|
|
102
|
+
stderr,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
stderr.write(`unknown command: ${command}\nRun \`cprof --help\` to see available commands.\n`);
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
109
|
+
process.exitCode = await main();
|
|
110
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cprof/cli",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"description": "Local-first CLI to snapshot, scrub, and migrate your Claude Code setup as a redacted, portable profile.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"homepage": "https://vedant1202.github.io/claude-prof/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Vedant1202/claude-prof.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/Vedant1202/claude-prof/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude",
|
|
16
|
+
"claude-code",
|
|
17
|
+
"anthropic",
|
|
18
|
+
"mcp",
|
|
19
|
+
"cli",
|
|
20
|
+
"developer-tools",
|
|
21
|
+
"local-first",
|
|
22
|
+
"secret-redaction",
|
|
23
|
+
"dotfiles",
|
|
24
|
+
"configuration",
|
|
25
|
+
"migration",
|
|
26
|
+
"backup"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"cprof": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"default": "./dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@cprof/schema": "0.0.1-alpha.0",
|
|
48
|
+
"@cprof/core": "0.0.1-alpha.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"vitest": "^3.2.4"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -p tsconfig.json",
|
|
55
|
+
"test": "vitest run"
|
|
56
|
+
}
|
|
57
|
+
}
|