@sechroom/cli 2026.6.29 → 2026.6.30
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/index.js +363 -216
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1780,7 +1780,7 @@ import { delimiter, dirname as dirname4, join as join4 } from "path";
|
|
|
1780
1780
|
|
|
1781
1781
|
// src/sem.ts
|
|
1782
1782
|
import { basename as basename2, dirname as dirname2, join as join2 } from "path";
|
|
1783
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1783
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1784
1784
|
var SEM_FILE = join2(".sechroom", "lane.json");
|
|
1785
1785
|
var LEGACY_SEM_FILE = ".sem";
|
|
1786
1786
|
var STATE_DIR_NAME2 = ".sechroom";
|
|
@@ -1799,6 +1799,43 @@ function resolveSemPathForRead(start = process.cwd()) {
|
|
|
1799
1799
|
dir = parent;
|
|
1800
1800
|
}
|
|
1801
1801
|
}
|
|
1802
|
+
function applyWorktreeLaneSuffix(lane, start = process.cwd()) {
|
|
1803
|
+
try {
|
|
1804
|
+
let dir = start;
|
|
1805
|
+
let gitPath;
|
|
1806
|
+
for (; ; ) {
|
|
1807
|
+
const candidate = join2(dir, ".git");
|
|
1808
|
+
if (existsSync2(candidate)) {
|
|
1809
|
+
gitPath = candidate;
|
|
1810
|
+
break;
|
|
1811
|
+
}
|
|
1812
|
+
const parent = dirname2(dir);
|
|
1813
|
+
if (parent === dir) break;
|
|
1814
|
+
dir = parent;
|
|
1815
|
+
}
|
|
1816
|
+
if (!gitPath || statSync(gitPath).isDirectory()) return lane;
|
|
1817
|
+
const gitFile = readFileSync2(gitPath, "utf8");
|
|
1818
|
+
const common = gitFile.trim().match(/^gitdir:\s*(.+)\/worktrees\/[^/\s]+\s*$/);
|
|
1819
|
+
if (!common) return lane;
|
|
1820
|
+
const worktreesDir = join2(common[1], "worktrees");
|
|
1821
|
+
const siblings = readdirSync(worktreesDir).filter((n) => {
|
|
1822
|
+
try {
|
|
1823
|
+
return statSync(join2(worktreesDir, n)).isDirectory();
|
|
1824
|
+
} catch {
|
|
1825
|
+
return false;
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
return laneWithWorktreeSuffix(lane, gitFile, siblings);
|
|
1829
|
+
} catch {
|
|
1830
|
+
return lane;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function laneWithWorktreeSuffix(lane, gitFile, siblings) {
|
|
1834
|
+
const m = gitFile.trim().match(/\/worktrees\/([^/\s]+)\s*$/);
|
|
1835
|
+
if (!m) return lane;
|
|
1836
|
+
const idx = [...siblings].sort().indexOf(m[1]);
|
|
1837
|
+
return idx < 0 ? lane : `${lane}-${idx + 2}`;
|
|
1838
|
+
}
|
|
1802
1839
|
function parseSem(text) {
|
|
1803
1840
|
const out = {};
|
|
1804
1841
|
for (const raw of text.split("\n")) {
|
|
@@ -2090,8 +2127,9 @@ function resolveLane(flagLane, cwd) {
|
|
|
2090
2127
|
const env = process.env.SECHROOM_LANE;
|
|
2091
2128
|
if (env) return env;
|
|
2092
2129
|
const start = cwd ?? process.cwd();
|
|
2093
|
-
const
|
|
2094
|
-
return
|
|
2130
|
+
const base = readSem(resolveSemPathForRead(start))?.values["code-lane"];
|
|
2131
|
+
if (!base) return void 0;
|
|
2132
|
+
return applyWorktreeLaneSuffix(base, start);
|
|
2095
2133
|
}
|
|
2096
2134
|
var INTENT_FILE = join4(".sechroom", "continuity.json");
|
|
2097
2135
|
function resolveIntentPath(start) {
|
|
@@ -2859,9 +2897,8 @@ auto-resumes where you left off and checkpoints working state before compacting.
|
|
|
2859
2897
|
}
|
|
2860
2898
|
|
|
2861
2899
|
// src/setup/skills-offer.ts
|
|
2862
|
-
import { mkdirSync as
|
|
2863
|
-
import {
|
|
2864
|
-
import { join as join5 } from "path";
|
|
2900
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
2901
|
+
import { join as join6 } from "path";
|
|
2865
2902
|
|
|
2866
2903
|
// src/setup/lane-pin.ts
|
|
2867
2904
|
var CODE_LANE_PREFIX_BY_CLIENT = {
|
|
@@ -2943,57 +2980,157 @@ I can pin this checkout's lane so operator skills + the continuity hook resolve
|
|
|
2943
2980
|
writePin(code || void 0, design || void 0);
|
|
2944
2981
|
}
|
|
2945
2982
|
|
|
2946
|
-
// src/setup/
|
|
2947
|
-
var
|
|
2983
|
+
// src/setup/skill-resolution.ts
|
|
2984
|
+
var SYSTEM_WORKSPACE_ID = "wsp_system";
|
|
2985
|
+
var SKILL_ROLE_TAG = "sechroom:role:skill-template";
|
|
2986
|
+
var SKILL_NAME_PREFIX = "skill:";
|
|
2987
|
+
var AGENT_ROLE_TAG = "sechroom:role:agent-template";
|
|
2988
|
+
var AGENT_NAME_PREFIX = "agent:";
|
|
2989
|
+
function tagsOf(row) {
|
|
2990
|
+
const m = row?.item ?? row;
|
|
2991
|
+
return m?.tags ?? m?.Tags ?? [];
|
|
2992
|
+
}
|
|
2948
2993
|
function tagValue(tags, prefix) {
|
|
2949
2994
|
return tags.find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
2950
2995
|
}
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2996
|
+
function bodyOf(row) {
|
|
2997
|
+
const m = row?.item ?? row;
|
|
2998
|
+
return m?.text ?? m?.Text ?? "";
|
|
2999
|
+
}
|
|
3000
|
+
function entriesFromRows(rows, surface, source, roleTag, namePrefix) {
|
|
3001
|
+
const out = /* @__PURE__ */ new Map();
|
|
3002
|
+
for (const row of rows ?? []) {
|
|
3003
|
+
const tags = tagsOf(row);
|
|
3004
|
+
if (!tags.includes(roleTag)) continue;
|
|
3005
|
+
if (tagValue(tags, "target:") !== surface) continue;
|
|
3006
|
+
const name = tagValue(tags, namePrefix);
|
|
3007
|
+
if (!name) continue;
|
|
3008
|
+
out.set(name, { name, body: bodyOf(row), source });
|
|
3009
|
+
}
|
|
3010
|
+
return out;
|
|
3011
|
+
}
|
|
3012
|
+
function resolveByRole(systemRows, personalRows, surface, roleTag, namePrefix) {
|
|
3013
|
+
const merged = entriesFromRows(systemRows, surface, "system", roleTag, namePrefix);
|
|
3014
|
+
for (const [name, item] of entriesFromRows(personalRows, surface, "personal", roleTag, namePrefix)) {
|
|
3015
|
+
merged.set(name, item);
|
|
3016
|
+
}
|
|
3017
|
+
return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
3018
|
+
}
|
|
3019
|
+
function resolveSkills(systemRows, personalRows, surface) {
|
|
3020
|
+
return resolveByRole(systemRows, personalRows, surface, SKILL_ROLE_TAG, SKILL_NAME_PREFIX);
|
|
3021
|
+
}
|
|
3022
|
+
function resolveAgents(systemRows, personalRows, surface) {
|
|
3023
|
+
return resolveByRole(systemRows, personalRows, surface, AGENT_ROLE_TAG, AGENT_NAME_PREFIX);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// src/setup/skills-lock.ts
|
|
3027
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3028
|
+
import { homedir as homedir5 } from "os";
|
|
3029
|
+
import { join as join5 } from "path";
|
|
3030
|
+
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
3031
|
+
var DEFAULT_SKILLS_SLUG = "operator-skills";
|
|
3032
|
+
function skillsDir(global) {
|
|
3033
|
+
return global ? join5(homedir5(), ".claude", "skills") : join5(process.cwd(), ".claude", "skills");
|
|
3034
|
+
}
|
|
3035
|
+
function agentsDir(global) {
|
|
3036
|
+
return global ? join5(homedir5(), ".claude", "agents") : join5(process.cwd(), ".claude", "agents");
|
|
3037
|
+
}
|
|
3038
|
+
function readSkillsLock(dir) {
|
|
3039
|
+
const lockPath = join5(dir, SKILLS_LOCK);
|
|
3040
|
+
if (!existsSync6(lockPath)) return {};
|
|
3041
|
+
try {
|
|
3042
|
+
return JSON.parse(readFileSync5(lockPath, "utf8"));
|
|
3043
|
+
} catch {
|
|
3044
|
+
return {};
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
function writeSkillsLock(dir, lock) {
|
|
3048
|
+
mkdirSync5(dir, { recursive: true });
|
|
3049
|
+
writeFileSync5(join5(dir, SKILLS_LOCK), JSON.stringify(lock, null, 2) + "\n");
|
|
3050
|
+
}
|
|
3051
|
+
function recordMaterialisedSkills(dir, slug, skills, meta = {}) {
|
|
3052
|
+
const lock = readSkillsLock(dir);
|
|
3053
|
+
lock[slug] = { surface: meta.surface, skills: [...skills].sort() };
|
|
3054
|
+
writeSkillsLock(dir, lock);
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/setup/skills-offer.ts
|
|
3058
|
+
async function fetchFeedRows(cfg, workspaceId) {
|
|
2955
3059
|
try {
|
|
2956
3060
|
const client = await makeClient(cfg);
|
|
2957
3061
|
const feed = await client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
2958
3062
|
params: {
|
|
2959
|
-
path: { workspaceId
|
|
3063
|
+
path: { workspaceId },
|
|
3064
|
+
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace;
|
|
3065
|
+
// includeText: the feed omits bodies by default, we need them for SKILL.md.
|
|
2960
3066
|
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
2961
3067
|
}
|
|
2962
3068
|
}).then((r) => r.data).catch(() => void 0);
|
|
2963
|
-
|
|
3069
|
+
return feed?.results ?? feed?.Results ?? [];
|
|
2964
3070
|
} catch {
|
|
2965
|
-
return;
|
|
3071
|
+
return [];
|
|
2966
3072
|
}
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
3073
|
+
}
|
|
3074
|
+
async function maybeOfferSkills(cfg, personalWorkspaceId, opts) {
|
|
3075
|
+
const surface = opts.surface ?? "claude-code";
|
|
3076
|
+
const [systemRows, personalRows] = await Promise.all([
|
|
3077
|
+
fetchFeedRows(cfg, SYSTEM_WORKSPACE_ID),
|
|
3078
|
+
personalWorkspaceId ? fetchFeedRows(cfg, personalWorkspaceId) : Promise.resolve([])
|
|
3079
|
+
]);
|
|
3080
|
+
const skills = resolveSkills(systemRows, personalRows, surface);
|
|
3081
|
+
const agents = resolveAgents(systemRows, personalRows, surface);
|
|
3082
|
+
if (skills.length === 0 && agents.length === 0) return;
|
|
3083
|
+
const sDir = skillsDir(true);
|
|
3084
|
+
const aDir = agentsDir(true);
|
|
3085
|
+
if (opts.dryRun) {
|
|
3086
|
+
const lines = (label, items) => items.length === 0 ? "" : `
|
|
3087
|
+
Would materialise ${style.bold(String(items.length))} ${label} for ${surface}:
|
|
3088
|
+
` + items.map((s) => ` ${s.name} ${style.dim(`[${s.source}]`)}`).join("\n") + "\n";
|
|
3089
|
+
process.stderr.write(lines("operator skill(s)", skills) + lines("agent(s)", agents));
|
|
3090
|
+
return;
|
|
2976
3091
|
}
|
|
2977
|
-
const
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
3092
|
+
const summary = [
|
|
3093
|
+
skills.length > 0 ? `${style.bold(String(skills.length))} skill(s)` : "",
|
|
3094
|
+
agents.length > 0 ? `${style.bold(String(agents.length))} agent(s)` : ""
|
|
3095
|
+
].filter(Boolean).join(" + ");
|
|
3096
|
+
process.stderr.write(`
|
|
3097
|
+
Found ${summary} available to you for ${surface}.
|
|
3098
|
+
`);
|
|
3099
|
+
if (skills.length > 0) process.stderr.write(` skills: ${skills.map((s) => s.name).join(", ")}
|
|
3100
|
+
`);
|
|
3101
|
+
if (agents.length > 0) process.stderr.write(` agents: ${agents.map((a) => a.name).join(", ")}
|
|
3102
|
+
`);
|
|
3103
|
+
const dest = [skills.length > 0 ? `${sDir}/` : "", agents.length > 0 ? `${aDir}/` : ""].filter(Boolean).join(" + ");
|
|
3104
|
+
const materialise = opts.yes ? true : canPrompt() ? await promptYesNo(`Write them to ${dest} so ${surface} can use them?`) : false;
|
|
2986
3105
|
if (!materialise) return;
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
3106
|
+
if (skills.length > 0) {
|
|
3107
|
+
const written = [];
|
|
3108
|
+
for (const s of skills) {
|
|
3109
|
+
mkdirSync6(join6(sDir, s.name), { recursive: true });
|
|
3110
|
+
writeFileSync6(join6(sDir, s.name, "SKILL.md"), s.body.endsWith("\n") ? s.body : s.body + "\n");
|
|
3111
|
+
written.push(s.name);
|
|
3112
|
+
}
|
|
3113
|
+
recordMaterialisedSkills(sDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
3114
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} skill(s) to ${sDir}
|
|
3115
|
+
`);
|
|
2993
3116
|
}
|
|
2994
|
-
|
|
3117
|
+
if (agents.length > 0) {
|
|
3118
|
+
mkdirSync6(aDir, { recursive: true });
|
|
3119
|
+
const written = [];
|
|
3120
|
+
for (const a of agents) {
|
|
3121
|
+
const file = `${a.name}.md`;
|
|
3122
|
+
writeFileSync6(join6(aDir, file), a.body.endsWith("\n") ? a.body : a.body + "\n");
|
|
3123
|
+
written.push(file);
|
|
3124
|
+
}
|
|
3125
|
+
recordMaterialisedSkills(aDir, DEFAULT_SKILLS_SLUG, written, { surface });
|
|
3126
|
+
process.stderr.write(`${style.green("\u2713")} wrote ${written.length} agent(s) to ${aDir}
|
|
2995
3127
|
`);
|
|
2996
|
-
|
|
3128
|
+
}
|
|
3129
|
+
await ensureLanePin(cfg, {
|
|
3130
|
+
yes: opts.yes,
|
|
3131
|
+
dryRun: opts.dryRun,
|
|
3132
|
+
clients: [surface]
|
|
3133
|
+
});
|
|
2997
3134
|
}
|
|
2998
3135
|
|
|
2999
3136
|
// src/commands/setup.ts
|
|
@@ -3144,13 +3281,13 @@ async function runClients(clients, cmd, opts) {
|
|
|
3144
3281
|
}
|
|
3145
3282
|
|
|
3146
3283
|
// src/commands/onboard.ts
|
|
3147
|
-
import { existsSync as
|
|
3148
|
-
import { join as
|
|
3284
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3285
|
+
import { basename as basename3, join as join8 } from "path";
|
|
3149
3286
|
|
|
3150
3287
|
// src/commands/fanout.ts
|
|
3151
3288
|
import { spawnSync } from "child_process";
|
|
3152
|
-
import { existsSync as
|
|
3153
|
-
import { isAbsolute, join as
|
|
3289
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3290
|
+
import { isAbsolute, join as join7, resolve } from "path";
|
|
3154
3291
|
var ICON = {
|
|
3155
3292
|
refresh: "\u21BB",
|
|
3156
3293
|
bind: "+",
|
|
@@ -3163,28 +3300,28 @@ function resolveChildDir(path, root) {
|
|
|
3163
3300
|
function discoverChildren(root) {
|
|
3164
3301
|
let names;
|
|
3165
3302
|
try {
|
|
3166
|
-
names =
|
|
3303
|
+
names = readdirSync2(root);
|
|
3167
3304
|
} catch {
|
|
3168
3305
|
return [];
|
|
3169
3306
|
}
|
|
3170
3307
|
const out = [];
|
|
3171
3308
|
for (const name of names.sort()) {
|
|
3172
3309
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
3173
|
-
const dir =
|
|
3310
|
+
const dir = join7(root, name);
|
|
3174
3311
|
try {
|
|
3175
|
-
if (!
|
|
3312
|
+
if (!statSync2(dir).isDirectory()) continue;
|
|
3176
3313
|
} catch {
|
|
3177
3314
|
continue;
|
|
3178
3315
|
}
|
|
3179
|
-
if (
|
|
3316
|
+
if (existsSync7(join7(dir, ".git")) || committedBindingPath(dir)) out.push(name);
|
|
3180
3317
|
}
|
|
3181
3318
|
return out;
|
|
3182
3319
|
}
|
|
3183
3320
|
function readManifest(path) {
|
|
3184
|
-
if (!
|
|
3321
|
+
if (!existsSync7(path)) return null;
|
|
3185
3322
|
let parsed;
|
|
3186
3323
|
try {
|
|
3187
|
-
parsed = JSON.parse(
|
|
3324
|
+
parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
3188
3325
|
} catch (err2) {
|
|
3189
3326
|
throw new Error(`couldn't parse ${path}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3190
3327
|
}
|
|
@@ -3322,7 +3459,39 @@ async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
|
3322
3459
|
);
|
|
3323
3460
|
}
|
|
3324
3461
|
}
|
|
3325
|
-
async function
|
|
3462
|
+
async function fetchPersonalWorkspaceId(client) {
|
|
3463
|
+
try {
|
|
3464
|
+
const { data } = await client.GET("/me/personal-workspace", {});
|
|
3465
|
+
return data?.workspaceId ?? null;
|
|
3466
|
+
} catch {
|
|
3467
|
+
return null;
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
function nameTokens(s) {
|
|
3471
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
|
|
3472
|
+
}
|
|
3473
|
+
function personalSubtreeIds(personalId, all) {
|
|
3474
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
3475
|
+
for (const w of all) {
|
|
3476
|
+
if (!w.parentId) continue;
|
|
3477
|
+
(childrenOf.get(w.parentId) ?? childrenOf.set(w.parentId, []).get(w.parentId)).push(w);
|
|
3478
|
+
}
|
|
3479
|
+
const ids = /* @__PURE__ */ new Set([personalId]);
|
|
3480
|
+
const queue = [personalId];
|
|
3481
|
+
while (queue.length > 0) {
|
|
3482
|
+
const id = queue.shift();
|
|
3483
|
+
for (const child of childrenOf.get(id) ?? []) {
|
|
3484
|
+
if (!ids.has(child.id)) {
|
|
3485
|
+
ids.add(child.id);
|
|
3486
|
+
queue.push(child.id);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
return ids;
|
|
3491
|
+
}
|
|
3492
|
+
async function pickWorkspace(client, opts = {}) {
|
|
3493
|
+
const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
|
|
3494
|
+
const dirName = opts.dirName ?? basename3(process.cwd());
|
|
3326
3495
|
const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
|
|
3327
3496
|
if (all.length === 0) {
|
|
3328
3497
|
process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
@@ -3330,22 +3499,34 @@ async function pickWorkspace(client, promptLabel = "Bind this directory to a wor
|
|
|
3330
3499
|
return void 0;
|
|
3331
3500
|
}
|
|
3332
3501
|
const byId = new Map(all.map((w) => [w.id, w]));
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3502
|
+
const personalId = await fetchPersonalWorkspaceId(client);
|
|
3503
|
+
const excluded = personalId ? personalSubtreeIds(personalId, all) : /* @__PURE__ */ new Set();
|
|
3504
|
+
let candidates = all.filter((w) => !excluded.has(w.id));
|
|
3505
|
+
if (candidates.length === 0) candidates = all;
|
|
3506
|
+
const dirToks = new Set(nameTokens(dirName));
|
|
3507
|
+
const isMatch = (w) => nameTokens(w.name).some((t) => dirToks.has(t));
|
|
3508
|
+
const suggestions = candidates.filter(isMatch);
|
|
3509
|
+
let pool = candidates;
|
|
3510
|
+
if (candidates.length > 12 && suggestions.length === 0) {
|
|
3511
|
+
const q = (await promptText(`Filter ${candidates.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
|
|
3336
3512
|
if (q) {
|
|
3337
|
-
const hits =
|
|
3513
|
+
const hits = candidates.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
|
|
3338
3514
|
if (hits.length > 0) pool = hits;
|
|
3339
3515
|
else process.stderr.write(`no match for "${q}" \u2014 listing all
|
|
3340
3516
|
`);
|
|
3341
3517
|
}
|
|
3342
3518
|
}
|
|
3343
3519
|
const SKIP = "__skip__";
|
|
3520
|
+
const byPath = (a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId));
|
|
3521
|
+
const matched = pool.filter(isMatch).sort(byPath);
|
|
3522
|
+
const rest = pool.filter((w) => !isMatch(w)).sort(byPath);
|
|
3344
3523
|
const choices = [
|
|
3345
|
-
...
|
|
3524
|
+
...matched.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: style.dim(`matches "${dirName}"`) })),
|
|
3525
|
+
...rest.map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
|
|
3346
3526
|
{ label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
|
|
3347
3527
|
];
|
|
3348
|
-
const
|
|
3528
|
+
const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
|
|
3529
|
+
const chosen = await promptSelect(promptLabel, choices, defaultValue);
|
|
3349
3530
|
if (chosen === SKIP) return void 0;
|
|
3350
3531
|
const picked = byId.get(chosen);
|
|
3351
3532
|
const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
|
|
@@ -3372,7 +3553,7 @@ async function resolveWorkspaceBinding(client, existing, opts) {
|
|
|
3372
3553
|
}
|
|
3373
3554
|
if (existing) return existing;
|
|
3374
3555
|
if (!canPrompt() || opts.yes) return void 0;
|
|
3375
|
-
return pickWorkspace(client);
|
|
3556
|
+
return pickWorkspace(client, { dirName: basename3(process.cwd()) });
|
|
3376
3557
|
}
|
|
3377
3558
|
async function ensureTenant(baseUrl, g, opts) {
|
|
3378
3559
|
const persisted = readPersisted();
|
|
@@ -3505,10 +3686,10 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
3505
3686
|
}
|
|
3506
3687
|
async function planRecurseChild(entry, root, client, opts) {
|
|
3507
3688
|
const dir = resolveChildDir(entry.path, root);
|
|
3508
|
-
if (!
|
|
3689
|
+
if (!existsSync8(dir)) {
|
|
3509
3690
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3510
3691
|
}
|
|
3511
|
-
if (
|
|
3692
|
+
if (existsSync8(join8(dir, ".sechroom.json"))) {
|
|
3512
3693
|
return {
|
|
3513
3694
|
label: entry.path,
|
|
3514
3695
|
dir,
|
|
@@ -3535,7 +3716,10 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
3535
3716
|
process.stderr.write(`
|
|
3536
3717
|
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
3537
3718
|
`);
|
|
3538
|
-
const ws = await pickWorkspace(client,
|
|
3719
|
+
const ws = await pickWorkspace(client, {
|
|
3720
|
+
promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
|
|
3721
|
+
dirName: basename3(entry.path)
|
|
3722
|
+
});
|
|
3539
3723
|
if (!ws) {
|
|
3540
3724
|
return { label: entry.path, dir, disposition: "skip-unbound", argv: [], reason: "unbound \u2014 no workspace chosen (skipped)" };
|
|
3541
3725
|
}
|
|
@@ -3578,7 +3762,7 @@ This fan-out will pin the same lane in every repo:
|
|
|
3578
3762
|
async function runRecurse(cfg, g, opts) {
|
|
3579
3763
|
const { yes, dryRun, json } = opts;
|
|
3580
3764
|
const root = process.cwd();
|
|
3581
|
-
const manifestPath =
|
|
3765
|
+
const manifestPath = join8(root, ".sechroom", "repos.json");
|
|
3582
3766
|
const fromManifest = readManifest(manifestPath);
|
|
3583
3767
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
3584
3768
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
@@ -3762,14 +3946,21 @@ async function chooseWire(opts, yes) {
|
|
|
3762
3946
|
return opts.mcp === false ? "agent-only" : "full";
|
|
3763
3947
|
}
|
|
3764
3948
|
var FALLBACK_AGENT_PROMPT = "Resume my sechroom continuity, summarise what I was last working on, then suggest the next step.";
|
|
3949
|
+
function printNextStepBlock(heading, lines) {
|
|
3950
|
+
const rule = style.dim("\u2500".repeat(52));
|
|
3951
|
+
process.stdout.write(
|
|
3952
|
+
`
|
|
3953
|
+
${rule}
|
|
3954
|
+
${style.bold(heading)}
|
|
3955
|
+
|
|
3956
|
+
` + lines.map((l) => ` ${l}`).join("\n") + `
|
|
3957
|
+
${rule}
|
|
3958
|
+
`
|
|
3959
|
+
);
|
|
3960
|
+
}
|
|
3765
3961
|
async function printStarterPrompt(mode, cfg) {
|
|
3766
3962
|
if (mode === "cli") {
|
|
3767
|
-
|
|
3768
|
-
`
|
|
3769
|
-
${style.bold("Next:")} pick up where you left off \u2014
|
|
3770
|
-
${style.cyan("sechroom continuity resume-me")}
|
|
3771
|
-
`
|
|
3772
|
-
);
|
|
3963
|
+
printNextStepBlock("Next \u2014 pick up where you left off:", [style.cyan("sechroom continuity resume-me")]);
|
|
3773
3964
|
return;
|
|
3774
3965
|
}
|
|
3775
3966
|
let primary = FALLBACK_AGENT_PROMPT;
|
|
@@ -3781,21 +3972,16 @@ ${style.bold("Next:")} pick up where you left off \u2014
|
|
|
3781
3972
|
} catch {
|
|
3782
3973
|
}
|
|
3783
3974
|
}
|
|
3784
|
-
|
|
3785
|
-
`
|
|
3786
|
-
${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
3787
|
-
${style.cyan(`"${primary}"`)}
|
|
3788
|
-
`
|
|
3789
|
-
);
|
|
3975
|
+
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [style.cyan(`"${primary}"`)]);
|
|
3790
3976
|
}
|
|
3791
3977
|
|
|
3792
3978
|
// src/commands/sweep.ts
|
|
3793
|
-
import { existsSync as
|
|
3794
|
-
import { dirname as dirname6, join as
|
|
3795
|
-
var DEFAULT_MANIFEST =
|
|
3979
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3980
|
+
import { dirname as dirname6, join as join9, resolve as resolve2 } from "path";
|
|
3981
|
+
var DEFAULT_MANIFEST = join9(".sechroom", "repos.json");
|
|
3796
3982
|
function planEntry(entry, root) {
|
|
3797
3983
|
const dir = resolveChildDir(entry.path, root);
|
|
3798
|
-
if (!
|
|
3984
|
+
if (!existsSync9(dir)) {
|
|
3799
3985
|
return { label: entry.path, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3800
3986
|
}
|
|
3801
3987
|
if (committedBindingPath(dir)) {
|
|
@@ -3889,106 +4075,78 @@ Examples:
|
|
|
3889
4075
|
}
|
|
3890
4076
|
|
|
3891
4077
|
// src/commands/skills.ts
|
|
3892
|
-
import {
|
|
3893
|
-
import {
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
var
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
}
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
4078
|
+
import { join as join10 } from "path";
|
|
4079
|
+
import { existsSync as existsSync10, rmSync as rmSync2 } from "fs";
|
|
4080
|
+
|
|
4081
|
+
// src/commands/lane.ts
|
|
4082
|
+
var LANE_KEYS = ["code-lane", "design-lane"];
|
|
4083
|
+
function showLane(json) {
|
|
4084
|
+
const found = readSem();
|
|
4085
|
+
if (!found) {
|
|
4086
|
+
if (json) return emit({ path: null, values: {} }, true);
|
|
4087
|
+
return console.log(
|
|
4088
|
+
style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom lane set'.`)
|
|
4089
|
+
);
|
|
4090
|
+
}
|
|
4091
|
+
const resolved = { ...found.values };
|
|
4092
|
+
let suffixed = false;
|
|
4093
|
+
for (const k of LANE_KEYS) {
|
|
4094
|
+
const v = found.values[k];
|
|
4095
|
+
if (!v) continue;
|
|
4096
|
+
resolved[k] = applyWorktreeLaneSuffix(v);
|
|
4097
|
+
if (resolved[k] !== v) suffixed = true;
|
|
4098
|
+
}
|
|
4099
|
+
if (json) return emit({ path: found.path, values: resolved, worktreeSuffixApplied: suffixed }, true);
|
|
4100
|
+
console.log(style.dim(`from ${found.path}`));
|
|
4101
|
+
Object.entries(resolved).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
4102
|
+
if (suffixed) console.log(style.dim(" (worktree -N suffix applied \u2014 non-primary git worktree)"));
|
|
4103
|
+
}
|
|
4104
|
+
function setLane(opts) {
|
|
4105
|
+
if (!opts.codeLane && !opts.designLane) fail("Provide --code-lane and/or --design-lane.");
|
|
4106
|
+
const target = localSemPath();
|
|
4107
|
+
const values = readLocalSemValues();
|
|
4108
|
+
if (opts.codeLane) values["code-lane"] = opts.codeLane;
|
|
4109
|
+
if (opts.designLane) values["design-lane"] = opts.designLane;
|
|
4110
|
+
writeSem(values, target);
|
|
4111
|
+
if (opts.json) return emit({ path: target, values }, true);
|
|
4112
|
+
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
4113
|
+
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
4114
|
+
}
|
|
4115
|
+
function registerLane(program2) {
|
|
4116
|
+
const lane = program2.command("lane").description("Show this checkout's continuity lane pin (worktree-aware -N suffix applied)").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
4117
|
+
lane.command("set").description("Write this checkout's lane pin to ./.sechroom/lane.json").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
4118
|
+
(opts, cmd) => setLane({
|
|
4119
|
+
codeLane: opts.codeLane,
|
|
4120
|
+
designLane: opts.designLane,
|
|
4121
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
4122
|
+
})
|
|
4123
|
+
);
|
|
4124
|
+
lane.addHelpText(
|
|
4125
|
+
"after",
|
|
4126
|
+
`
|
|
4127
|
+
Examples:
|
|
4128
|
+
$ sechroom lane show the resolved lane(s)
|
|
4129
|
+
$ sechroom lane set --code-lane claude-code-chris --design-lane claude-design-chris
|
|
4130
|
+
|
|
4131
|
+
In a non-primary git worktree the ratified concurrent-session -N suffix is auto-applied (SBC-1094).
|
|
4132
|
+
(Aliases: 'sechroom skills lane' / 'skills set-lane' \u2014 kept for back-compat.)`
|
|
4133
|
+
);
|
|
3906
4134
|
}
|
|
4135
|
+
|
|
4136
|
+
// src/commands/skills.ts
|
|
3907
4137
|
function registerSkills(program2) {
|
|
3908
|
-
const skills = program2.command("skills").description("
|
|
4138
|
+
const skills = program2.command("skills").description("Manage operator skills (materialised by `onboard`)");
|
|
3909
4139
|
skills.addHelpText(
|
|
3910
4140
|
"after",
|
|
3911
4141
|
`
|
|
3912
4142
|
Examples:
|
|
3913
|
-
$ sechroom skills install --code-lane claude-code-chris --design-lane claude-design-chris
|
|
3914
|
-
$ sechroom skills install operator-skills --surface claude-code --local
|
|
3915
4143
|
$ sechroom skills list
|
|
3916
4144
|
$ sechroom skills set-lane --code-lane claude-code-chris --design-lane claude-design-chris
|
|
3917
4145
|
$ sechroom skills lane
|
|
3918
|
-
$ sechroom skills clean
|
|
4146
|
+
$ sechroom skills clean
|
|
4147
|
+
|
|
4148
|
+
To install/refresh skills, run 'sechroom onboard' (it offers to materialise them).`
|
|
3919
4149
|
);
|
|
3920
|
-
skills.command("install [slug]").description(`Install a skills bundle (default ${DEFAULT_SLUG}) into your personal workspace + write SKILL.md files`).option("--version <v>", "bundle version (default: latest published in the catalogue)").option("--instance <name>", "install as a named, separate instance (install the same bundle more than once)").option("--code-lane <id>", "identity.code-lane binding (e.g. claude-code-chris)").option("--design-lane <id>", "identity.design-lane binding (e.g. claude-design-chris)").option("--surface <s>", "skill target surface to materialise", "claude-code").option("--local", "write to ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts, cmd) => {
|
|
3921
|
-
const slug = slugArg || DEFAULT_SLUG;
|
|
3922
|
-
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3923
|
-
const pw = await runApi("resolving personal workspace", () => client.GET("/me/personal-workspace", {}));
|
|
3924
|
-
const personalWsId = pw?.id || pw?.workspaceId || pw?.personalWorkspaceId || pw?.item?.id;
|
|
3925
|
-
if (!personalWsId) fail("Could not resolve your personal workspace.");
|
|
3926
|
-
let version = opts.version;
|
|
3927
|
-
if (!version) {
|
|
3928
|
-
const cat = await runApi("reading the bundle catalogue", () => client.GET("/me/bundles", {}));
|
|
3929
|
-
const item = (cat?.bundles ?? cat?.Bundles ?? []).find((b) => (b.slug ?? b.Slug) === slug);
|
|
3930
|
-
if (!item) fail(`Bundle '${slug}' is not in your self-serve catalogue (must be UserInstallable + Published).`);
|
|
3931
|
-
version = item.latestVersion ?? item.LatestVersion;
|
|
3932
|
-
if (!version) fail(`Bundle '${slug}' has no installable (Published) version.`);
|
|
3933
|
-
}
|
|
3934
|
-
const installOptions = {};
|
|
3935
|
-
if (opts.codeLane) installOptions["identity.code-lane"] = opts.codeLane;
|
|
3936
|
-
if (opts.designLane) installOptions["identity.design-lane"] = opts.designLane;
|
|
3937
|
-
const res = await runApi(
|
|
3938
|
-
`installing ${slug}@${version}${opts.instance ? ` (${opts.instance})` : ""}`,
|
|
3939
|
-
() => client.POST("/me/bundles/{slug}/versions/{version}/install", {
|
|
3940
|
-
params: { path: { slug, version } },
|
|
3941
|
-
// instance: null/absent = the default instance (reinstall updates in
|
|
3942
|
-
// place); a name installs a separate instance.
|
|
3943
|
-
body: { installOptions, instance: opts.instance ?? null }
|
|
3944
|
-
})
|
|
3945
|
-
);
|
|
3946
|
-
const status = String(res?.status ?? res?.Status ?? "");
|
|
3947
|
-
if (status && status.toLowerCase() !== "completed") {
|
|
3948
|
-
fail(`Install did not complete (status=${status}; ${res?.failureReason ?? res?.FailureReason ?? ""}).`);
|
|
3949
|
-
}
|
|
3950
|
-
const feed = await runApi(
|
|
3951
|
-
"materialising skill files",
|
|
3952
|
-
() => client.GET("/workspaces/{workspaceId}/memories/feed", {
|
|
3953
|
-
// cascadeWorkspaces: skills land in an "Operator Skills" SUB-workspace of
|
|
3954
|
-
// the personal workspace, so we recurse from the personal-ws root.
|
|
3955
|
-
// includeText: the feed omits bodies by default; we need them for SKILL.md.
|
|
3956
|
-
params: {
|
|
3957
|
-
path: { workspaceId: personalWsId },
|
|
3958
|
-
query: { limit: 200, cascadeWorkspaces: true, includeText: true }
|
|
3959
|
-
}
|
|
3960
|
-
})
|
|
3961
|
-
);
|
|
3962
|
-
const rows = feed?.results ?? feed?.Results ?? [];
|
|
3963
|
-
const dir = skillsDir(!opts.local);
|
|
3964
|
-
const wantInstance = opts.instance || "default";
|
|
3965
|
-
const written = [];
|
|
3966
|
-
const bundleTagPrefix = `sechroom:bundle:${slug}@`;
|
|
3967
|
-
for (const r of rows) {
|
|
3968
|
-
const m = r.item ?? r;
|
|
3969
|
-
const tags = m.tags ?? m.Tags ?? [];
|
|
3970
|
-
if (!hasAny(tags, ROLE_TAGS)) continue;
|
|
3971
|
-
if (tagValue2(tags, "target:") !== opts.surface) continue;
|
|
3972
|
-
if (!tags.some((t) => t.startsWith(bundleTagPrefix))) continue;
|
|
3973
|
-
if ((tagValue2(tags, "sechroom:skill-instance:") ?? "default") !== wantInstance) continue;
|
|
3974
|
-
const name = tagValue2(tags, "skill:");
|
|
3975
|
-
if (!name) continue;
|
|
3976
|
-
const body = m.text ?? m.Text ?? "";
|
|
3977
|
-
mkdirSync6(join9(dir, name), { recursive: true });
|
|
3978
|
-
writeFileSync6(join9(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
3979
|
-
written.push(name);
|
|
3980
|
-
}
|
|
3981
|
-
mkdirSync6(dir, { recursive: true });
|
|
3982
|
-
const lockPath = join9(dir, LOCK);
|
|
3983
|
-
const lock = existsSync9(lockPath) ? JSON.parse(readFileSync6(lockPath, "utf8")) : {};
|
|
3984
|
-
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
3985
|
-
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
3986
|
-
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
3987
|
-
const instanceNote = opts.instance ? ` (${opts.instance})` : "";
|
|
3988
|
-
console.log(style.green(`Installed ${slug}@${version}${instanceNote} \u2014 ${written.length} skill(s) \u2192 ${dir}`));
|
|
3989
|
-
written.forEach((n) => console.log(" " + style.dim("\u2022") + " " + n));
|
|
3990
|
-
if (written.length === 0) console.log(style.dim(` (no '${opts.surface}' skill bodies found; check --surface)`));
|
|
3991
|
-
});
|
|
3992
4150
|
skills.command("list").description("List your installed bundles (GET /me/bundle-installs)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
3993
4151
|
const client = await makeClient(resolveConfig(cmd.optsWithGlobals()));
|
|
3994
4152
|
const data = await runApi("reading your installs", () => client.GET("/me/bundle-installs", {}));
|
|
@@ -4001,49 +4159,33 @@ Examples:
|
|
|
4001
4159
|
console.log(` ${i.bundleSlug ?? i.BundleSlug}@${i.bundleVersion ?? i.BundleVersion ?? "?"}${tag}`);
|
|
4002
4160
|
});
|
|
4003
4161
|
});
|
|
4004
|
-
skills.command("clean [slug]").description(`Remove
|
|
4005
|
-
const slug = slugArg ||
|
|
4162
|
+
skills.command("clean [slug]").description(`Remove skill files materialised by onboard (default ${DEFAULT_SKILLS_SLUG})`).option("--local", "clean ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts) => {
|
|
4163
|
+
const slug = slugArg || DEFAULT_SKILLS_SLUG;
|
|
4006
4164
|
const dir = skillsDir(!opts.local);
|
|
4007
|
-
const
|
|
4008
|
-
if (!existsSync9(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
|
|
4009
|
-
const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
|
|
4165
|
+
const lock = readSkillsLock(dir);
|
|
4010
4166
|
const entry = lock[slug];
|
|
4011
|
-
if (!entry) fail(`No
|
|
4167
|
+
if (!entry) fail(`No materialised skills recorded for '${slug}' in ${join10(dir, SKILLS_LOCK)}.`);
|
|
4012
4168
|
const removed = [];
|
|
4013
4169
|
for (const name of entry.skills) {
|
|
4014
|
-
const skillPath =
|
|
4015
|
-
if (
|
|
4170
|
+
const skillPath = join10(dir, name);
|
|
4171
|
+
if (existsSync10(skillPath)) {
|
|
4016
4172
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
4017
4173
|
removed.push(name);
|
|
4018
4174
|
}
|
|
4019
4175
|
}
|
|
4020
4176
|
delete lock[slug];
|
|
4021
|
-
|
|
4177
|
+
writeSkillsLock(dir, lock);
|
|
4022
4178
|
if (opts.json) return emit({ slug, removed, dir }, true);
|
|
4023
4179
|
console.log(style.green(`Removed ${removed.length} skill(s) for ${slug} from ${dir}`));
|
|
4024
4180
|
});
|
|
4025
|
-
skills.command("set-lane").description("
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
console.log(style.green(`Wrote lane pin \u2192 ${target} ${style.dim("(git-ignored)")}`));
|
|
4034
|
-
Object.entries(values).forEach(([k, v]) => console.log(" " + style.dim(k) + " = " + v));
|
|
4035
|
-
});
|
|
4036
|
-
skills.command("lane").description("Show the lane pin resolved from ./.sechroom/lane.json (nearest in this checkout; legacy ./.sem honoured)").option("--json", "machine output").action((opts, cmd) => {
|
|
4037
|
-
const json = cmd.optsWithGlobals().json;
|
|
4038
|
-
const found = readSem();
|
|
4039
|
-
if (!found) {
|
|
4040
|
-
if (json) return emit({ path: null, values: {} }, true);
|
|
4041
|
-
return console.log(style.dim(`No ./.sechroom/lane.json pin in this checkout. Run 'sechroom skills set-lane'.`));
|
|
4042
|
-
}
|
|
4043
|
-
if (json) return emit(found, true);
|
|
4044
|
-
console.log(style.dim(`from ${found.path}`));
|
|
4045
|
-
Object.entries(found.values).forEach(([k, v]) => console.log(" " + style.bold(k) + " = " + v));
|
|
4046
|
-
});
|
|
4181
|
+
skills.command("set-lane").description("Alias of `sechroom lane set` (kept for back-compat) \u2014 write this checkout's lane pin").option("--code-lane <id>", "code-surface lane id (e.g. claude-code-chris)").option("--design-lane <id>", "design / substrate-authoring lane id (e.g. claude-design-chris)").option("--json", "machine output").action(
|
|
4182
|
+
(opts, cmd) => setLane({
|
|
4183
|
+
codeLane: opts.codeLane,
|
|
4184
|
+
designLane: opts.designLane,
|
|
4185
|
+
json: Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)
|
|
4186
|
+
})
|
|
4187
|
+
);
|
|
4188
|
+
skills.command("lane").description("Alias of `sechroom lane` (kept for back-compat) \u2014 show this checkout's lane pin").option("--json", "machine output").action((opts, cmd) => showLane(Boolean(opts.json) || Boolean(cmd.optsWithGlobals().json)));
|
|
4047
4189
|
skills.command("set-workflow").description("Set your per-operator workflow defaults (server-side; follows you across tenants)").option("--default-code-lane <id>", "personal default code lane (e.g. claude-code-chris)").option("--default-design-lane <id>", "personal default design lane (e.g. claude-design-chris)").option("--handover-recipient <id>", "your daily-handover counterparty (e.g. andy)").option("--json", "machine output").action(async (opts, cmd) => {
|
|
4048
4190
|
if (!opts.defaultCodeLane && !opts.defaultDesignLane && !opts.handoverRecipient)
|
|
4049
4191
|
fail("Provide at least one of --default-code-lane / --default-design-lane / --handover-recipient.");
|
|
@@ -4107,22 +4249,24 @@ Examples:
|
|
|
4107
4249
|
}
|
|
4108
4250
|
|
|
4109
4251
|
// src/commands/reset.ts
|
|
4110
|
-
import { homedir as
|
|
4111
|
-
import { join as
|
|
4112
|
-
import { existsSync as
|
|
4113
|
-
var
|
|
4114
|
-
var localSkillsDir = () =>
|
|
4115
|
-
var globalSkillsDir = () =>
|
|
4252
|
+
import { homedir as homedir6 } from "os";
|
|
4253
|
+
import { join as join11 } from "path";
|
|
4254
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
4255
|
+
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
4256
|
+
var localSkillsDir = () => join11(process.cwd(), ".claude", "skills");
|
|
4257
|
+
var globalSkillsDir = () => join11(homedir6(), ".claude", "skills");
|
|
4258
|
+
var localAgentsDir = () => join11(process.cwd(), ".claude", "agents");
|
|
4259
|
+
var globalAgentsDir = () => join11(homedir6(), ".claude", "agents");
|
|
4116
4260
|
function removeMaterialisedSkills(dir) {
|
|
4117
4261
|
const removed = [];
|
|
4118
|
-
const lockPath =
|
|
4119
|
-
if (!
|
|
4262
|
+
const lockPath = join11(dir, SKILLS_LOCK2);
|
|
4263
|
+
if (!existsSync11(lockPath)) return removed;
|
|
4120
4264
|
try {
|
|
4121
4265
|
const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
|
|
4122
4266
|
for (const entry of Object.values(lock)) {
|
|
4123
4267
|
for (const name of entry.skills ?? []) {
|
|
4124
|
-
const p =
|
|
4125
|
-
if (
|
|
4268
|
+
const p = join11(dir, name);
|
|
4269
|
+
if (existsSync11(p)) {
|
|
4126
4270
|
rmSync3(p, { recursive: true, force: true });
|
|
4127
4271
|
removed.push(p);
|
|
4128
4272
|
}
|
|
@@ -4167,28 +4311,30 @@ function registerReset(program2) {
|
|
|
4167
4311
|
}
|
|
4168
4312
|
}
|
|
4169
4313
|
const removed = [];
|
|
4170
|
-
const stateDir =
|
|
4171
|
-
if (
|
|
4314
|
+
const stateDir = join11(process.cwd(), ".sechroom");
|
|
4315
|
+
if (existsSync11(stateDir)) {
|
|
4172
4316
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
4173
4317
|
removed.push(stateDir);
|
|
4174
4318
|
}
|
|
4175
|
-
const legacyCfg =
|
|
4176
|
-
if (
|
|
4319
|
+
const legacyCfg = join11(process.cwd(), ".sechroom.json");
|
|
4320
|
+
if (existsSync11(legacyCfg)) {
|
|
4177
4321
|
rmSync3(legacyCfg, { force: true });
|
|
4178
4322
|
removed.push(legacyCfg);
|
|
4179
4323
|
}
|
|
4180
|
-
const legacySem =
|
|
4181
|
-
if (
|
|
4324
|
+
const legacySem = join11(process.cwd(), ".sem");
|
|
4325
|
+
if (existsSync11(legacySem)) {
|
|
4182
4326
|
rmSync3(legacySem, { force: true });
|
|
4183
4327
|
removed.push(legacySem);
|
|
4184
4328
|
}
|
|
4185
4329
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
4330
|
+
removed.push(...removeMaterialisedSkills(localAgentsDir()));
|
|
4186
4331
|
if (global) {
|
|
4187
4332
|
const tok = clearToken();
|
|
4188
4333
|
if (tok) removed.push(tok);
|
|
4189
4334
|
const cfg = clearPersisted();
|
|
4190
4335
|
if (cfg) removed.push(cfg);
|
|
4191
4336
|
removed.push(...removeMaterialisedSkills(globalSkillsDir()));
|
|
4337
|
+
removed.push(...removeMaterialisedSkills(globalAgentsDir()));
|
|
4192
4338
|
}
|
|
4193
4339
|
if (json) return emit({ global, removed }, true);
|
|
4194
4340
|
if (removed.length === 0) {
|
|
@@ -4326,6 +4472,7 @@ registerSetup(program);
|
|
|
4326
4472
|
registerOnboard(program);
|
|
4327
4473
|
registerSweep(program);
|
|
4328
4474
|
registerSkills(program);
|
|
4475
|
+
registerLane(program);
|
|
4329
4476
|
registerReset(program);
|
|
4330
4477
|
program.parseAsync().catch((err2) => {
|
|
4331
4478
|
process.stderr.write(`error: ${err2 instanceof Error ? err2.message : String(err2)}
|
package/package.json
CHANGED