claude-code-station 0.2.1
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/CHANGELOG.md +176 -0
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/bin/ccs +376 -0
- package/bin/ccs-config.ts +528 -0
- package/bin/ccs-db.ts +404 -0
- package/bin/ccs-delete-session.ts +48 -0
- package/bin/ccs-delete.sh +100 -0
- package/bin/ccs-init.ts +287 -0
- package/bin/ccs-list.ts +363 -0
- package/bin/ccs-preview-session.ts +147 -0
- package/bin/ccs-preview.ts +368 -0
- package/bin/ccs-sanitize.ts +57 -0
- package/bin/ccs-scan-sessions.ts +402 -0
- package/bin/ccs-scan.ts +734 -0
- package/bin/ccs-secrets.ts +104 -0
- package/bin/ccs-time.ts +27 -0
- package/bin/ccs-utils.ts +161 -0
- package/docs/design/repos-yml-schema.md +217 -0
- package/docs/design/sqlite-schema.md +253 -0
- package/docs/v0.2.0-regression-checklist.md +40 -0
- package/docs/v0.2.0-review-notes.md +151 -0
- package/docs/v0.2.1-backlog.md +225 -0
- package/package.json +44 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ccs-secrets.ts — Unified secret detection for ccs (scan + preview).
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for secret-masking patterns. Both the scan engine
|
|
5
|
+
* (which writes `first_line` columns into state.db) and the fzf preview pane
|
|
6
|
+
* (which renders session text live) import from here so no credential can
|
|
7
|
+
* land in cache or terminal output through pattern drift.
|
|
8
|
+
*
|
|
9
|
+
* All matches are replaced with the sentinel [REDACTED].
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Pattern origins:
|
|
14
|
+
* anthropic = Anthropic API key (sk-ant-...)
|
|
15
|
+
* openai = OpenAI API key (sk-... excluding sk-ant-)
|
|
16
|
+
* github-pat = GitHub Personal Access Token
|
|
17
|
+
* github-oauth = GitHub OAuth access token
|
|
18
|
+
* github-server = GitHub server-to-server token (GitHub Apps)
|
|
19
|
+
* github-user = GitHub user-to-server token
|
|
20
|
+
* github-refresh= GitHub OAuth refresh token
|
|
21
|
+
* github-fine = GitHub fine-grained personal access token (github_pat_)
|
|
22
|
+
* gitlab-pat = GitLab personal access token (glpat-)
|
|
23
|
+
* google-oauth = Google OAuth client secret (GOCSPX-)
|
|
24
|
+
* sendgrid = SendGrid API key (SG.xxx.yyy)
|
|
25
|
+
* npm-token = npm automation/publish token (npm_)
|
|
26
|
+
* slack-webhook = Slack incoming-webhook URL (hooks.slack.com/services/...)
|
|
27
|
+
* aws-access = AWS long-lived access key ID (AKIA prefix)
|
|
28
|
+
* aws-sts = AWS STS temporary session credentials (ASIA prefix)
|
|
29
|
+
* google-api = Google API key (AIza prefix)
|
|
30
|
+
* stripe-live = Stripe live-mode secret/restricted key (sk_live / rk_live)
|
|
31
|
+
* stripe-test = Stripe test-mode secret/restricted key (sk_test / rk_test)
|
|
32
|
+
* twilio-account= Twilio Account SID (AC + 32 hex chars)
|
|
33
|
+
* slack-token = Slack bot/user/app/refresh/OAuth token family (xox[baprs])
|
|
34
|
+
* jwt = JSON Web Token (3 base64url segments separated by dots)
|
|
35
|
+
* bearer = "Bearer " Authorization header token
|
|
36
|
+
* op-ref = 1Password op:// secret reference
|
|
37
|
+
* url-cred = Any URL with embedded user:password credentials
|
|
38
|
+
* (postgres/mysql/mongodb/redis/https/..., incl. +srv)
|
|
39
|
+
* env-assign = Generic KEY=value / KEY: value assignment where the name
|
|
40
|
+
* ends in KEY/TOKEN/SECRET/PASSWORD (catch-all for pasted
|
|
41
|
+
* .env lines and handoff notes)
|
|
42
|
+
* private-key = PEM-encoded private key block
|
|
43
|
+
*/
|
|
44
|
+
export const SECRET_PATTERNS: { name: string; re: RegExp }[] = [
|
|
45
|
+
{ name: "anthropic", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
|
|
46
|
+
{ name: "openai", re: /sk-(?!ant-)[A-Za-z0-9_-]{20,}/g },
|
|
47
|
+
{ name: "github-pat", re: /ghp_[A-Za-z0-9]{36,}/g },
|
|
48
|
+
{ name: "github-oauth", re: /gho_[A-Za-z0-9]{36,}/g },
|
|
49
|
+
{ name: "github-server", re: /ghs_[A-Za-z0-9]{36,}/g },
|
|
50
|
+
{ name: "github-user", re: /ghu_[A-Za-z0-9]{36,}/g },
|
|
51
|
+
{ name: "github-refresh", re: /ghr_[A-Za-z0-9]{36,}/g },
|
|
52
|
+
{ name: "github-fine", re: /github_pat_[A-Za-z0-9_]{22,}/g },
|
|
53
|
+
{ name: "gitlab-pat", re: /glpat-[A-Za-z0-9_-]{20,}/g },
|
|
54
|
+
{ name: "google-oauth", re: /GOCSPX-[A-Za-z0-9_-]{20,}/g },
|
|
55
|
+
{ name: "sendgrid", re: /SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g },
|
|
56
|
+
{ name: "npm-token", re: /npm_[A-Za-z0-9]{36}/g },
|
|
57
|
+
{ name: "slack-webhook", re: /hooks\.slack\.com\/services\/\S+/g },
|
|
58
|
+
{ name: "aws-access", re: /AKIA[A-Z0-9]{16}/g },
|
|
59
|
+
{ name: "aws-sts", re: /ASIA[A-Z0-9]{16}/g },
|
|
60
|
+
{ name: "google-api", re: /AIza[A-Za-z0-9_-]{35}/g },
|
|
61
|
+
{ name: "stripe-live", re: /[sr]k_live_[A-Za-z0-9]{24,}/g },
|
|
62
|
+
{ name: "stripe-test", re: /[sr]k_test_[A-Za-z0-9]{24,}/g },
|
|
63
|
+
// Case-insensitive hex: Twilio renders SIDs lowercase today, but tooling
|
|
64
|
+
// and docs frequently uppercase them — both must mask (review C-3).
|
|
65
|
+
{ name: "twilio-account", re: /AC[a-fA-F0-9]{32}/g },
|
|
66
|
+
{ name: "slack-token", re: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
67
|
+
{
|
|
68
|
+
name: "jwt",
|
|
69
|
+
re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
70
|
+
},
|
|
71
|
+
{ name: "bearer", re: /Bearer\s+[A-Za-z0-9._-]{20,}/g },
|
|
72
|
+
{ name: "op-ref", re: /op:\/\/[A-Za-z0-9._/-]+/g },
|
|
73
|
+
{
|
|
74
|
+
// Any scheme (postgres/mysql/mongodb/redis/https/...) with a user:password
|
|
75
|
+
// userinfo component — broader than the old db-url pattern so credentials
|
|
76
|
+
// in plain https:// URLs are caught too.
|
|
77
|
+
name: "url-cred",
|
|
78
|
+
re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:[^\s:@/]+@[^\s]+/gi,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
// Conservative catch-all: only fires on ALL-CAPS env-style names ending in
|
|
82
|
+
// a credential-ish suffix, so prose like "the key: rotate it" stays intact.
|
|
83
|
+
name: "env-assign",
|
|
84
|
+
re: /\b[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)\s*[=:]\s*\S{8,}/g,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "private-key",
|
|
88
|
+
re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const REPLACEMENT = "[REDACTED]";
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Mask all known secret patterns in the given string.
|
|
96
|
+
* Returns a new string; the original is not mutated.
|
|
97
|
+
*/
|
|
98
|
+
export function maskSecrets(input: string): string {
|
|
99
|
+
let out = input;
|
|
100
|
+
for (const { re } of SECRET_PATTERNS) {
|
|
101
|
+
out = out.replace(re, REPLACEMENT);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
package/bin/ccs-time.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ccs-time.ts — Shared timestamp parsing for ccs list/preview renderers.
|
|
3
|
+
*
|
|
4
|
+
* state.db holds two timestamp shapes (audit logic H-2 / M-1):
|
|
5
|
+
* 1. ISO 8601 with offset — e.g. "2026-06-12T03:04:05.000Z"
|
|
6
|
+
* (written by nowIso() / JSONL timestamps / mtime conversions)
|
|
7
|
+
* 2. SQLite naive datetime — e.g. "2026-06-12 03:04:05"
|
|
8
|
+
* (written by `datetime('now')` DDL defaults; always UTC per SQLite docs)
|
|
9
|
+
*
|
|
10
|
+
* `Date.parse` interprets shape 2 as LOCAL time on V8, which would skew
|
|
11
|
+
* rendered ages by the UTC offset (+9h in JST). Every consumer must therefore
|
|
12
|
+
* parse DB timestamps through this module instead of calling Date.parse
|
|
13
|
+
* directly.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Normalize a DB timestamp to an unambiguous ISO 8601 UTC string. */
|
|
17
|
+
export function normalizeDbTime(iso: string): string {
|
|
18
|
+
return iso.includes("T") ? iso : iso.replace(" ", "T") + "Z";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a DB timestamp into epoch milliseconds.
|
|
23
|
+
* Returns NaN for unparseable input (callers decide the fallback).
|
|
24
|
+
*/
|
|
25
|
+
export function parseDbTime(iso: string): number {
|
|
26
|
+
return Date.parse(normalizeDbTime(iso));
|
|
27
|
+
}
|
package/bin/ccs-utils.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ccs-utils.ts — Shared helpers for ccs scan/list/preview modules.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for display-text truncation, relative-time
|
|
5
|
+
* bucketing, JSONL message-content extraction, and cross-module constants
|
|
6
|
+
* (review A-1/A-2/A-3/K-8). Before this module existed, each renderer kept
|
|
7
|
+
* its own copy of these helpers with "keep in sync" comments — and they
|
|
8
|
+
* drifted. Behavioral differences must now be expressed as explicit options,
|
|
9
|
+
* never as parallel implementations.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { stripControlChars } from "./ccs-sanitize.ts";
|
|
13
|
+
import { parseDbTime } from "./ccs-time.ts";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Cross-module constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/** Session JSONL files above this size are never read into memory. */
|
|
20
|
+
export const MAX_JSONL_SIZE = 50 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
/** Canonical session-UUID gate. Bash copies live in bin/ccs and ccs-delete.sh. */
|
|
23
|
+
export const UUID_RE =
|
|
24
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
25
|
+
|
|
26
|
+
/** Current time as ISO 8601 UTC — the only timestamp format ccs writes. */
|
|
27
|
+
export function nowIso(): string {
|
|
28
|
+
return new Date().toISOString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Display truncation
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Single-line display truncation: strips control chars (incl. ESC, DEL),
|
|
37
|
+
* collapses whitespace runs, and clips to `max` with an ellipsis.
|
|
38
|
+
*
|
|
39
|
+
* Display-side defense against terminal-escape injection (audit NEW-1):
|
|
40
|
+
* fzf renders the list with --ansi and the preview pane prints raw to the
|
|
41
|
+
* terminal. Intake sanitization in ccs-scan is the first line of defense.
|
|
42
|
+
*/
|
|
43
|
+
export function truncate(s: string, max: number): string {
|
|
44
|
+
if (!s) return "";
|
|
45
|
+
const flat = stripControlChars(s);
|
|
46
|
+
return flat.length <= max ? flat : flat.slice(0, max - 1) + "…";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Time formatting
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export type TimeBucket =
|
|
54
|
+
| { kind: "now" } // < 1 minute (incl. clock skew into the future)
|
|
55
|
+
| { kind: "m" | "h" | "d" | "w"; value: number }
|
|
56
|
+
| { kind: "date"; iso: string } // >= 4 weeks → absolute YYYY-MM-DD
|
|
57
|
+
| { kind: "invalid" };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Bucket a DB timestamp for relative display. The thresholds
|
|
61
|
+
* (<1m / <60m / <24h / <7d / <4w / absolute date) are the single source of
|
|
62
|
+
* truth shared by the list badges and the preview pane, so the same
|
|
63
|
+
* timestamp can never render as "52w" in one place and "2025-06-12" in the
|
|
64
|
+
* other (audit logic L-3).
|
|
65
|
+
*/
|
|
66
|
+
export function timeBucket(
|
|
67
|
+
iso: string | null | undefined,
|
|
68
|
+
nowMs: number = Date.now(),
|
|
69
|
+
): TimeBucket {
|
|
70
|
+
if (!iso) return { kind: "invalid" };
|
|
71
|
+
// parseDbTime, not Date.parse: naive "YYYY-MM-DD HH:MM:SS" values from
|
|
72
|
+
// SQLite are UTC and must not be parsed as local time (audit logic H-2).
|
|
73
|
+
const t = parseDbTime(iso);
|
|
74
|
+
if (Number.isNaN(t)) return { kind: "invalid" };
|
|
75
|
+
const diffMs = nowMs - t;
|
|
76
|
+
if (diffMs < 60_000) return { kind: "now" };
|
|
77
|
+
const m = Math.floor(diffMs / 60_000);
|
|
78
|
+
if (m < 60) return { kind: "m", value: m };
|
|
79
|
+
const h = Math.floor(m / 60);
|
|
80
|
+
if (h < 24) return { kind: "h", value: h };
|
|
81
|
+
const d = Math.floor(h / 24);
|
|
82
|
+
if (d < 7) return { kind: "d", value: d };
|
|
83
|
+
const w = Math.floor(d / 7);
|
|
84
|
+
if (w < 4) return { kind: "w", value: w };
|
|
85
|
+
return { kind: "date", iso: new Date(t).toISOString().slice(0, 10) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Render a DB timestamp as relative age ("5m ago", "3d ago") or an absolute
|
|
90
|
+
* date past 4 weeks. `invalid` is returned for null/unparseable input —
|
|
91
|
+
* list badges pass "" (badge omitted), the preview pane passes "-".
|
|
92
|
+
*/
|
|
93
|
+
export function formatRelativeTime(
|
|
94
|
+
iso: string | null | undefined,
|
|
95
|
+
invalid = "-",
|
|
96
|
+
): string {
|
|
97
|
+
const b = timeBucket(iso);
|
|
98
|
+
switch (b.kind) {
|
|
99
|
+
case "invalid":
|
|
100
|
+
return invalid;
|
|
101
|
+
case "now":
|
|
102
|
+
return "<1m ago";
|
|
103
|
+
case "date":
|
|
104
|
+
return b.iso;
|
|
105
|
+
default:
|
|
106
|
+
return `${b.value}${b.kind} ago`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Render a DB timestamp as local "YYYY-MM-DD HH:MM"; `invalid` on failure. */
|
|
111
|
+
export function formatDateTime(
|
|
112
|
+
iso: string | null | undefined,
|
|
113
|
+
invalid = "-",
|
|
114
|
+
): string {
|
|
115
|
+
if (!iso) return invalid;
|
|
116
|
+
const d = new Date(parseDbTime(iso));
|
|
117
|
+
if (Number.isNaN(d.getTime())) return invalid;
|
|
118
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
119
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// JSONL message-content extraction
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
export interface ExtractTextOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Include "[tool: name]" / "[tool result]" placeholders for non-text
|
|
129
|
+
* blocks. The conversation preview wants them (visual flow); topic
|
|
130
|
+
* extraction in the scanner does not (the topic is the user's words).
|
|
131
|
+
*/
|
|
132
|
+
includeToolBlocks?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Flatten a Claude Code JSONL `message.content` value (string or block
|
|
137
|
+
* array) into one string. Shared by topic extraction (ccs-scan-sessions)
|
|
138
|
+
* and the conversation preview (ccs-preview-session); the historical bug
|
|
139
|
+
* class here was two copies drifting apart behind a "keep in sync" comment
|
|
140
|
+
* (review A-2) — behavioral differences belong in `opts`, not in forks.
|
|
141
|
+
*/
|
|
142
|
+
export function extractText(
|
|
143
|
+
content: unknown,
|
|
144
|
+
opts: ExtractTextOptions = {},
|
|
145
|
+
): string {
|
|
146
|
+
if (typeof content === "string") return content;
|
|
147
|
+
if (!Array.isArray(content)) return "";
|
|
148
|
+
const texts: string[] = [];
|
|
149
|
+
for (const block of content) {
|
|
150
|
+
if (!block || typeof block !== "object") continue;
|
|
151
|
+
const b = block as { type?: unknown; text?: unknown; name?: unknown };
|
|
152
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
153
|
+
texts.push(b.text);
|
|
154
|
+
} else if (opts.includeToolBlocks && b.type === "tool_use") {
|
|
155
|
+
texts.push(`[tool: ${typeof b.name === "string" ? b.name : "?"}]`);
|
|
156
|
+
} else if (opts.includeToolBlocks && b.type === "tool_result") {
|
|
157
|
+
texts.push("[tool result]");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return texts.join(" ");
|
|
161
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# repos.yml スキーマ設計
|
|
2
|
+
|
|
3
|
+
> ccs v0.2.0 — リポジトリ定義ファイルのフォーマット仕様
|
|
4
|
+
|
|
5
|
+
## 配置
|
|
6
|
+
|
|
7
|
+
- パス: `~/.config/ccs/repos.yml`
|
|
8
|
+
- 初回 `ccs` 実行時、存在しなければテンプレートと `README.md` を自動生成
|
|
9
|
+
|
|
10
|
+
## トップレベル構造
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
version: 1 # スキーマバージョン(必須、現在 1)
|
|
14
|
+
defaults: # 全リポジトリのデフォルト値(任意)
|
|
15
|
+
command: "claude"
|
|
16
|
+
repos: # リポジトリ定義配列(必須、1件以上)
|
|
17
|
+
- name: ClaudeCode
|
|
18
|
+
path: ~/.claude
|
|
19
|
+
description: Claude Code設定
|
|
20
|
+
...
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## `repos[]` エントリのフィールド
|
|
24
|
+
|
|
25
|
+
| フィールド | 型 | 必須 | デフォルト | 説明 |
|
|
26
|
+
|---|---|:---:|---|---|
|
|
27
|
+
| `name` | string | ✅ | — | 表示名(fzf行に出る、日本語可) |
|
|
28
|
+
| `path` | string | ✅* | — | リポジトリのフルパス。`~` 展開あり |
|
|
29
|
+
| `description` | string | — | `""` | プレビュー補助情報 |
|
|
30
|
+
| `command` | string | — | `defaults.command` or `claude` | 起動コマンド。特殊な場合は丸ごと上書き |
|
|
31
|
+
| `cwd` | string | — | `path` | 起動時の作業ディレクトリ(通常は `path` と同じ) |
|
|
32
|
+
| `tags` | string[] | — | `[]` | 絞り込み用タグ(例: `["work", "backend"]`) |
|
|
33
|
+
| `disabled` | bool | — | `false` | true ならリストから除外 |
|
|
34
|
+
| `scan` | bool | — | `true` | false なら状態走査をスキップ(非git含む) |
|
|
35
|
+
| `icon` | string | — | `📁` | 表示アイコン(絵文字1文字推奨) |
|
|
36
|
+
| `custom` | object | — | `{}` | 外部システム連携情報。任意のキー/値ペア(後述) |
|
|
37
|
+
|
|
38
|
+
\* `path` は `command` に `cwd:` が含まれる特殊ケースを除き必須。
|
|
39
|
+
|
|
40
|
+
### `custom` フィールド(外部システム連携)
|
|
41
|
+
|
|
42
|
+
ユーザー固有の連携情報を自由に保持できる。プレビューペインで「連携バッジ」として表示され、未入力なら表示しない。
|
|
43
|
+
|
|
44
|
+
**よくある例:**
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
repos:
|
|
48
|
+
- name: My Project
|
|
49
|
+
path: ~/projects/my-project
|
|
50
|
+
custom:
|
|
51
|
+
plane_project_id: "abc12345-xxxx"
|
|
52
|
+
plane_url: "https://plane.example.com/my-project"
|
|
53
|
+
attio_workspace: "acme-corp"
|
|
54
|
+
notion_db: "abc123"
|
|
55
|
+
linear_team: "PROJ"
|
|
56
|
+
slack_channel: "#dev-general"
|
|
57
|
+
figma_file: "https://www.figma.com/file/example123"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**プレビュー表示例:**
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
🔗 Integrations
|
|
64
|
+
Plane: ✅ abc12345-xxxx
|
|
65
|
+
Attio: ✅ acme-corp
|
|
66
|
+
Notion: ✅ abc123
|
|
67
|
+
Slack: ✅ #dev-general
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**仕様:**
|
|
71
|
+
- `custom` の値は文字列・数値・bool・配列・ネストobject 何でも可(JSON保存)
|
|
72
|
+
- 既知キー(後述の組み込み連携)は専用の表示ロジックでバッジ化
|
|
73
|
+
- 未知キーは `key: value` の形でそのまま表示
|
|
74
|
+
|
|
75
|
+
**組み込み既知キー(v0.2.0):**
|
|
76
|
+
|
|
77
|
+
| キー | 表示 | 備考 |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `plane_project_id` | Plane バッジ | `plane_url` あればURL併記 |
|
|
80
|
+
| `plane_url` | — | バッジリンク用 |
|
|
81
|
+
| `attio_workspace` | Attio バッジ | |
|
|
82
|
+
| `notion_db` | Notion バッジ | |
|
|
83
|
+
| `linear_team` | Linear バッジ | |
|
|
84
|
+
| `slack_channel` | Slack バッジ | `#` 自動付与 |
|
|
85
|
+
| `github_repo` | GitHub バッジ | `owner/repo` 形式 |
|
|
86
|
+
| `figma_file` | Figma バッジ | URL でも ID でも可 |
|
|
87
|
+
|
|
88
|
+
**未知キーの扱い:**
|
|
89
|
+
- そのまま `key: value` 形式で「Other Integrations」セクションに表示
|
|
90
|
+
- 将来需要が高まったキーは組み込み既知キーに昇格
|
|
91
|
+
|
|
92
|
+
**バリデーション:**
|
|
93
|
+
- `custom` がobjectでない → エラー
|
|
94
|
+
- 中身の型は問わない(ユーザー責任)
|
|
95
|
+
- シークレット混入注意(APIキー等は書かない、URL/IDのみ推奨)
|
|
96
|
+
|
|
97
|
+
## バリデーションルール
|
|
98
|
+
|
|
99
|
+
| ルール | エラー例 |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `version` は整数、現在サポートは `1` のみ | `unsupported version: 2` |
|
|
102
|
+
| `name` はユニーク | `duplicate name: "ClaudeCode"` |
|
|
103
|
+
| `path` 存在チェック | `path not found: ~/projects/foo`(警告扱い、disabled 推奨) |
|
|
104
|
+
| `path` は `$HOME` 配下必須 | `path outside $HOME: /etc/foo`(セキュリティ) |
|
|
105
|
+
| `name` に shell metachar 不可 (`;&\|<>$\`"'\\` + 全制御文字 = `SHELL_METACHARS`、audit NEW-3。`path`/`cwd`/`command` と同一ポリシー) | `~/.config/ccs/repos.yml: repos[0].name contains shell metacharacter(s) or control char(s): "..."` |
|
|
106
|
+
| `command` に shell metachar 不可 (`;&\|<>$\`"'\\` + 制御文字) | `~/.config/ccs/repos.yml: repos[0].command contains shell metacharacter(s): "claude;..."` |
|
|
107
|
+
| `defaults.command`(CCS_CMD/CCR_CMD 由来含む)もロード時に同検証(レビューA-6) | `~/.config/ccs/repos.yml: defaults.command (resolved from defaults.command / CCS_CMD / CCR_CMD) contains shell metacharacter(s): "..."` |
|
|
108
|
+
| `custom` のJSONサイズ 64KB 上限 | `~/.config/ccs/repos.yml: repos[0].custom exceeds 64KB JSON size limit` |
|
|
109
|
+
| `disabled: true` の場合 scan/command 無視 | — |
|
|
110
|
+
|
|
111
|
+
## 例1: シンプル
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
version: 1
|
|
115
|
+
repos:
|
|
116
|
+
- name: ClaudeCode
|
|
117
|
+
path: ~/.claude
|
|
118
|
+
description: Claude Code設定
|
|
119
|
+
- name: My Project
|
|
120
|
+
path: ~/projects/my-project
|
|
121
|
+
description: Main development repository
|
|
122
|
+
tags: [work]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 例2: 特殊コマンド対応(CMS dev server起動など)
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
version: 1
|
|
129
|
+
defaults:
|
|
130
|
+
command: "claude"
|
|
131
|
+
repos:
|
|
132
|
+
- name: ClaudeCode
|
|
133
|
+
path: ~/.claude
|
|
134
|
+
|
|
135
|
+
- name: CMS (dev server)
|
|
136
|
+
path: ~/sites/blog-cms
|
|
137
|
+
command: "npm run develop"
|
|
138
|
+
icon: "🚀"
|
|
139
|
+
scan: false # Claude起動じゃないので状態走査不要
|
|
140
|
+
tags: [dev-server]
|
|
141
|
+
|
|
142
|
+
- name: personal-notes
|
|
143
|
+
path: ~/projects/personal-notes
|
|
144
|
+
tags: [personal]
|
|
145
|
+
disabled: true # 今は使わない
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## 例3: 1Passwordラッパー付き(CCR_CMD互換)
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
version: 1
|
|
152
|
+
defaults:
|
|
153
|
+
command: "opr claude" # 全リポジトリで opr ラッパー経由
|
|
154
|
+
repos:
|
|
155
|
+
- name: Startup MVP
|
|
156
|
+
path: ~/work/startup-mvp
|
|
157
|
+
- name: My Project
|
|
158
|
+
path: ~/projects/my-project
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## 環境変数との優先順位
|
|
162
|
+
|
|
163
|
+
`repos[].command` > `defaults.command` > `CCS_CMD` 環境変数 > `"claude"`
|
|
164
|
+
|
|
165
|
+
**設計意図**: per-repo の明示的指定(`command:` フィールド)が常に最優先。`CCS_CMD` env は「全リポジトリ共通のラッパー(例: `opr claude`)を `defaults.command` 未指定時に注入したい」用途のフォールバックとして機能する。CMS 起動のような特殊コマンドが env で踏み潰されるのを防ぐため、env を最弱に置く。
|
|
166
|
+
|
|
167
|
+
ccr v0.1.3 の `CCR_CMD` は `CCS_CMD` にリネーム(移行ガイドで明記、CCR_CMD指定時は警告ログ + 暫定honor)。
|
|
168
|
+
|
|
169
|
+
**Security note**: `CCS_CMD` / `CCR_CMD` env values are subject to the same shell metacharacter rejection as `command:` in YAML. If env-sourced value contains any forbidden character, `loadConfig()` throws `ConfigError` before launch.
|
|
170
|
+
|
|
171
|
+
**未マッピングセッションへの適用(レビューA-8)**: repos.yml に登録されていない cwd のセッション(fzf 上で `❓` 表示)も、この優先順位の `defaults.command` 以降のチェーンに従う。scan 時に解決済みフォールバックが state.db の `meta.defaults_command` に保存され、ccs-list が RESUME 行の起動コマンドとして使用する(旧実装はハードコードの `"claude"` 固定だった)。
|
|
172
|
+
|
|
173
|
+
## JSON Schema(抜粋)
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
178
|
+
"type": "object",
|
|
179
|
+
"required": ["version", "repos"],
|
|
180
|
+
"properties": {
|
|
181
|
+
"version": { "const": 1 },
|
|
182
|
+
"defaults": {
|
|
183
|
+
"type": "object",
|
|
184
|
+
"properties": {
|
|
185
|
+
"command": { "type": "string" }
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"repos": {
|
|
189
|
+
"type": "array",
|
|
190
|
+
"minItems": 1,
|
|
191
|
+
"items": {
|
|
192
|
+
"type": "object",
|
|
193
|
+
"required": ["name"],
|
|
194
|
+
"properties": {
|
|
195
|
+
"name": { "type": "string", "pattern": "^[^\\t\\n\\\\]+$" },
|
|
196
|
+
"path": { "type": "string" },
|
|
197
|
+
"description": { "type": "string" },
|
|
198
|
+
"command": { "type": "string" },
|
|
199
|
+
"cwd": { "type": "string" },
|
|
200
|
+
"tags": { "type": "array", "items": { "type": "string" } },
|
|
201
|
+
"disabled": { "type": "boolean" },
|
|
202
|
+
"scan": { "type": "boolean" },
|
|
203
|
+
"icon": { "type": "string", "maxLength": 4 },
|
|
204
|
+
"custom": { "type": "object", "additionalProperties": true }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## 将来拡張(v0.3.0以降・未実装)
|
|
213
|
+
|
|
214
|
+
- `group`: リポジトリのグループ化(fzfヘッダーでグループ切替)
|
|
215
|
+
- `env`: リポジトリ毎の環境変数注入
|
|
216
|
+
- `post_launch`: 起動後に実行するフック
|
|
217
|
+
- `priority`: ソート重み(上に出したいリポジトリ)
|