worclaude 2.5.0 → 2.6.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/CHANGELOG.md +65 -0
- package/package.json +4 -2
- package/src/commands/doctor.js +4 -6
- package/src/commands/init.js +9 -6
- package/src/commands/scan.js +126 -0
- package/src/commands/setup-state.js +113 -0
- package/src/commands/status.js +6 -4
- package/src/commands/upgrade.js +32 -20
- package/src/core/drift-checks.js +2 -1
- package/src/core/file-categorizer.js +38 -0
- package/src/core/merger.js +14 -7
- package/src/core/migration.js +77 -1
- package/src/core/project-scanner/detectors/ci.js +51 -0
- package/src/core/project-scanner/detectors/deployment.js +32 -0
- package/src/core/project-scanner/detectors/env-variables.js +78 -0
- package/src/core/project-scanner/detectors/external-apis.js +45 -0
- package/src/core/project-scanner/detectors/frameworks.js +56 -0
- package/src/core/project-scanner/detectors/language.js +131 -0
- package/src/core/project-scanner/detectors/linting.js +58 -0
- package/src/core/project-scanner/detectors/monorepo.js +93 -0
- package/src/core/project-scanner/detectors/orm.js +72 -0
- package/src/core/project-scanner/detectors/package-manager.js +57 -0
- package/src/core/project-scanner/detectors/readme.js +123 -0
- package/src/core/project-scanner/detectors/scripts.js +128 -0
- package/src/core/project-scanner/detectors/spec-docs.js +110 -0
- package/src/core/project-scanner/detectors/testing.js +88 -0
- package/src/core/project-scanner/index.js +126 -0
- package/src/core/project-scanner/manifests.js +120 -0
- package/src/core/remover.js +6 -3
- package/src/core/scaffolder.js +1 -0
- package/src/core/setup-state.js +213 -0
- package/src/index.js +39 -0
- package/templates/commands/setup.md +512 -113
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,71 @@ All notable changes to worclaude are documented in this file. Format loosely fol
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.6.0] — 2026-04-22
|
|
8
|
+
|
|
9
|
+
Diagnose-first `/setup`. This release lands both halves of Phase Setup Diagnose in a single version: Part A (PR #103) ships the static project scanner and the new `worclaude scan` subcommand, and Part B (PR #104) rewrites `/setup` on top of it as a deterministic 12-state state machine with on-disk persistence, a tool-call whitelist, and a Claude-rendered selectable UI. Running `/setup` against a mature project now scans first (14 Tier 1 detectors produce a `DetectionReport`), presents the high-confidence facts as a numbered checklist for the user to confirm or uncheck, handles multi-candidate medium-confidence items (e.g., competing lockfiles), and only asks residual questions during the interview — cutting the interview from ~30 questions to whatever detection didn't cover. State survives interruption via `.claude/cache/setup-state.json`, persisted after every mutation through the new `worclaude setup-state` CLI (the sole write path `setup.md` is permitted to use under its tool whitelist). WRITE merges into existing output files conservatively: `CLAUDE.md` replaces `## Tech Stack` and `## Commands` sections by ATX heading; `SPEC.md` and SKILL files are rewritten only when template-only per CRLF-normalized SHA-256 match against `workflow-meta.json`, otherwise append a timestamped section; `PROGRESS.md` is append-only.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`worclaude scan` subcommand + detection engine** (PR #103, Part A) — new CLI subcommand that statically scans a project and produces a structured `DetectionReport` describing what it found. The report lands at `.claude/cache/detection-report.json` (machine-generated, gitignored, overwritten on every run). Ships 14 Tier 1 detectors under `src/core/project-scanner/detectors/` — one file per concern: `package-manager`, `language`, `frameworks`, `testing`, `linting`, `orm`, `deployment`, `ci`, `scripts`, `env-variables`, `external-apis`, `readme`, `spec-docs`, `monorepo`. Detector registration is directory-scan-based — adding a new detector means adding a new `.js` file, not editing the scanner. Each detector runs in parallel with a 5-second timeout; failures and timeouts are recorded in a per-report `errors` array without aborting the scan.
|
|
14
|
+
- **CLI options for `scan`** (PR #103): `--path <dir>` (defaults to `process.cwd()`), `--json` (prints the full report to stdout, suppresses summary), `--quiet` (suppresses summary, still writes the cache file). Exit codes: `0` on any completed scan (even with per-detector errors), `1` on fatal errors (invalid path, un-writable cache).
|
|
15
|
+
- **`pyproject.toml` dep flattening** (PR #103) — `frameworks`/`testing`/`linting`/`orm` detectors read all five dep locations (`[project.dependencies]`, `[project.optional-dependencies.*]`, `[tool.poetry.dependencies]`, `[tool.poetry.group.*.dependencies]`, `[dependency-groups.*]`) and flatten into a single map before matching. Fixtures exist for PEP 621, PEP 735, and Poetry-group layouts.
|
|
16
|
+
- **Runtime dependencies** (PR #103): `smol-toml` (TOML parsing for `pyproject.toml`, `Cargo.toml`) and `yaml` (for `pnpm-workspace.yaml`, `Taskfile.yml`). Both are small, pure-JS, and zero-runtime-cost.
|
|
17
|
+
- **`/setup` state machine rewrite** (PR #104, Part B) — `templates/commands/setup.md` is rewritten top-to-bottom as a 12-state state machine (`INIT → SCAN → CONFIRM_HIGH → CONFIRM_MEDIUM → INTERVIEW_STORY/ARCH/FEATURES/WORKFLOW/CONVENTIONS/VERIFICATION → WRITE → DONE`). `/setup` now invokes the Part A scanner at SCAN, presents detected facts via a Claude-rendered numbered checklist at CONFIRM_HIGH (user confirms or unchecks), handles multi-candidate medium-confidence items (`package-manager` with multiple lockfiles → numbered candidate list) at CONFIRM_MEDIUM, asks only residual questions during the six INTERVIEW states, and merge-writes the six output files at WRITE (CLAUDE.md is section-replaced by ATX heading, SPEC/SKILL files are rewritten only when template-only per CRLF-normalized SHA-256 match against `workflow-meta.json`, PROGRESS.md is append-only).
|
|
18
|
+
- **`src/core/setup-state.js`** (PR #104) — persistence module for `.claude/cache/setup-state.json` (schemaVersion 1). Exports `loadSetupState`, `saveSetupState`, `clearSetupState`, `isSetupStateStale`, plus the `STATE_NAMES`, `QUESTION_IDS`, and `UNCHECKED_ROUTING` contract constants. Schema validation rejects unknown `currentState` values, `interviewAnswers` keys outside the QuestionId enumeration, mis-routed `<state>.unchecked.<field>` prefixes, and non-string answer values. `saveSetupState` preserves `startedAt` from an existing file across re-saves and refreshes `updatedAt` automatically.
|
|
19
|
+
- **`worclaude setup-state` CLI subcommand** (PR #104) — four sub-subcommands: `show` (prints state JSON or `no state`), `save --stdin` (reads JSON from stdin, validates, persists — the ONLY mechanism `/setup` uses to write state), `reset` (idempotent delete), `resume-info` (pre-formatted `state: X, age: N unit(s), staleness: fresh|stale` line so Claude echoes rather than computes relative time). Exit codes: `0` success, `1` fatal, `2` invalid args.
|
|
20
|
+
- **Anti-drift rules embedded in `setup.md`** (PR #104) — seven CRITICAL EXECUTION RULES pinned at the top of the command file. Sequential state advance only; no backward advance within an invocation; off-topic input triggers a restate, never an answer; `cancel setup` matches regex `/^(cancel|stop|abort)( setup)?[.!?\s]*$/i` and preserves state; tool use is whitelisted (scanner + `setup-state` CLI + two cache reads between SCAN and WRITE; the six target file reads/writes only at WRITE, plus `workflow-meta.json` for hash lookup); no memory pre-fill from prior sessions; prompts render verbatim in fenced code blocks, including state-machine control prose (resume preamble, back rejection, off-topic restate prefix, cancel acknowledgment).
|
|
21
|
+
- **QuestionId enumeration** (PR #104) as the load-bearing `interviewAnswers` key contract (22 IDs across 6 INTERVIEW states) plus a `<state>.unchecked.<field>` prefix namespace for rejected high-confidence field re-asks, routed per a documented table (e.g., `scripts` rejected at CONFIRM_HIGH → re-asked in INTERVIEW_WORKFLOW under key `workflow.unchecked.scripts`).
|
|
22
|
+
- **Field rendering table** reproduced in `setup.md` (PR #104) — the contract between detection report shapes and the human-readable value strings rendered at CONFIRM_HIGH and CONFIRM_MEDIUM. Mirrors `src/commands/scan.js:summarizeValue` semantics for all 14 detector fields plus the fallback scalar/array/object cases.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **`.gitignore` scaffolder** (PR #103) — `updateGitignore()` in `src/core/scaffolder.js` now writes `.claude/cache/` in addition to the existing seven entries. `cleanGitignore()` in `src/core/remover.js` removes the new entry on `worclaude delete`.
|
|
27
|
+
- **`/setup` precondition** (PR #104) — `.claude/workflow-meta.json` must exist (canonical Worclaude init marker). Non-init'd projects get a clear "Run `worclaude init` first" message and exit, replacing the prior behavior of proceeding with undefined semantics.
|
|
28
|
+
|
|
29
|
+
### Tests
|
|
30
|
+
|
|
31
|
+
- **107 new scanner tests** (PR #103) across `tests/core/project-scanner/detectors/*.test.js` (14 files, 83 tests — positive, negative, and edge cases including multiple lockfiles, all four pyproject dep locations, quoted/prefixed env lines, monorepo with and without workspaces), `tests/core/project-scanner/index.test.js` (12 tests — happy path against six real fixtures, broken-detector and slow-detector handling via the `overrideDetectors` test seam, JSON round-trip), `tests/core/project-scanner/write-report.test.js` (7 tests), `tests/commands/scan.test.js` (5 tests — `--path`, `--json`, `--quiet`, cwd default, and exit-code-1 paths). 8 fixtures under `tests/fixtures/scanner/`: `nextjs-pnpm/`, `fastapi-poetry/`, `fastapi-pep621/`, `fastapi-pep735/`, `rust-cli/`, `monorepo-pnpm/`, `empty/`, `mixed-lockfiles/`.
|
|
32
|
+
- **47 new setup-state tests** (PR #104) — `tests/core/setup-state.test.js` (24 unit tests covering load/save/clear/isStale happy paths, corrupt JSON, unsupported schemaVersion, unknown currentState, unknown interviewAnswers keys, mis-routed unchecked prefixes, startedAt preservation, updatedAt refresh, custom staleHours) and `tests/commands/setup-state.test.js` (23 CLI tests — four subcommands' happy and error paths, `save --stdin` round-trip with an in-process `inputStream` injection seam plus one end-to-end `spawnSync` smoke test, `resume-info` unit picking at minute/hour/day boundaries, invalid-arg exit 2). Total suite: **729/729 pass**.
|
|
33
|
+
|
|
34
|
+
### Non-goals (explicitly deferred)
|
|
35
|
+
|
|
36
|
+
- No `/setup --edit <field>` flow for correcting prior answers (Tier 2).
|
|
37
|
+
- No automated state-machine test harness (mock-Claude driver walking every transition). Tier 2.
|
|
38
|
+
- No automated end-to-end testing of `/setup` across fixture projects — manual e2e per the 13-case checklist in the phase prompt.
|
|
39
|
+
- No schema migrator for `setup-state.json`. v1 policy is "reset on schema bump."
|
|
40
|
+
- No architecture classification, directory-tree module inference, CI required-check detection (GitHub API), or monorepo sub-package scanning — all Tier 2.
|
|
41
|
+
- No rename of the existing `src/core/detector.js` (scenario detection). The new scanner lives under `src/core/project-scanner/` to avoid name collision.
|
|
42
|
+
|
|
43
|
+
## [2.5.1] — 2026-04-21
|
|
44
|
+
|
|
45
|
+
Patch release fixing a user-visible bug found while dogfooding the v2.5.0 upgrade: reference-copy template files (the copies worclaude writes when your customized file and the shipped template both change) were being placed as `.workflow-ref.md` siblings next to the live file — including inside `.claude/commands/`, where Claude Code's command loader discovered them as phantom slash commands like `/sync.workflow-ref`, and inside `.claude/agents/`, where they could shadow live agents. Reference copies now live in a dedicated `.claude/workflow-ref/` tree that Claude Code does not scan. Installs upgrading from pre-v2.5.1 have their legacy reference files automatically relocated on the next `worclaude upgrade`.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- **Phantom slash commands from reference-copy siblings** (PR #101) — ref files are now written under `.claude/workflow-ref/<original-path>/` with the original filename preserved, instead of as `<name>.workflow-ref.md` siblings next to the live file. `.claude/commands/` and `.claude/agents/` are no longer polluted by `.workflow-ref` artifacts, so Claude Code's command and subagent loaders don't register them as live. `diff .claude/workflow-ref/commands/sync.md .claude/commands/sync.md` now reads cleanly.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **`migrateWorkflowRefLocation()` in `src/core/migration.js`** (PR #101) — runs unconditionally at the start of every `worclaude upgrade`. Sweeps legacy `*.workflow-ref.md` siblings under `.claude/{commands,agents,skills}/` (plus root-level `CLAUDE.md.workflow-ref.md` and `AGENTS.md.workflow-ref`) into the new `.claude/workflow-ref/<original-path>/` tree. Idempotent (skips when target already exists, never overwrites). Scoped scan avoids re-stat'ing `.claude/sessions/` on every upgrade. Deliberately not version-gated — semver-gating this kind of cleanup migration creates a new bug class for users who downgrade then re-upgrade.
|
|
54
|
+
- **Shared helpers in `src/core/file-categorizer.js`** (PR #101) — `WORKFLOW_REF_DIR` constant, `workflowRefRelPath(keyOrRelPath)` returning project-root-relative paths for `scaffoldFile`, `resolveRefPath(keyOrRelPath, projectRoot)` for absolute writes, and `isWorkflowRefFile(absPath, claudeDir)` predicate that unifies new-location-or-legacy-suffix detection so `status`, `doctor`, and `remover` stay in sync across the transition window.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **`worclaude upgrade` report output** (PR #101) — "Conflicts: N files (saved as .workflow-ref.md)" → "Conflicts: N files (saved under .claude/workflow-ref/)". New "Relocated: N legacy ref file(s) → .claude/workflow-ref/" line surfaces the one-time migration when it fires. Final hint reads "Review files under .claude/workflow-ref/ and merge what's useful."
|
|
59
|
+
- **`worclaude init` and `worclaude status` output** (PR #101) — Scenario B conflict block, Next Steps, and Pending Review list all updated to reference the new location. Status continues to surface legacy `*.workflow-ref.md` siblings anywhere under `.claude/` so users upgrading from pre-v2.5.1 still see what they need to resolve before the next upgrade migrates them.
|
|
60
|
+
- **`src/core/remover.js`** (PR #101) — the entire `.claude/workflow-ref/` subtree is classified as safe-to-delete on `worclaude delete`; the empty-subdir cleanup list extended to include `workflow-ref`.
|
|
61
|
+
|
|
62
|
+
### Docs
|
|
63
|
+
|
|
64
|
+
- **Upgrading guide** (PR #101) — 7 inline references updated plus a new "Upgrading from pre-v2.5.1 installs" subsection explaining what the automatic relocation does and how the skip-on-collision rule works.
|
|
65
|
+
- **Existing-projects guide** (PR #101) — Tier 2 "Safe Alongside" section rewritten to describe the new `.claude/workflow-ref/` layout; post-merge report sample updated; the "Reviewing reference files" section now shows the clean `diff workflow-ref/commands/sync.md commands/sync.md` idiom.
|
|
66
|
+
- **Commands reference, agents reference, SPEC.md, project-patterns skill** (PR #101) — all references to `.workflow-ref.md` sibling layout updated; `docs/spec/SPEC.md` edited on the feature branch as a shared-state override (PR #79 precedent: the SPEC update describes the very behavior the PR adds).
|
|
67
|
+
|
|
68
|
+
### Tests
|
|
69
|
+
|
|
70
|
+
- **Relocation migration coverage** (PR #101) — 8 new cases in `tests/core/migration.test.js` covering command/agent/skill subdir sweeps, root-level `CLAUDE.md.workflow-ref.md` / `AGENTS.md.workflow-ref` handling, target-collision skip behavior, idempotency on repeated runs, no-op on projects with files already at the new location, and clean-project no-ops. Regression assertions in `tests/commands/upgrade.test.js`, `tests/commands/init.test.js`, and `tests/core/merger.test.js` explicitly verify that `.workflow-ref.md` siblings are NEVER created inside `.claude/commands/` or `.claude/agents/`, so the phantom-command class of bug cannot silently return. 575/575 pass (567 prior + 8 new).
|
|
71
|
+
|
|
7
72
|
## [2.5.0] — 2026-04-21
|
|
8
73
|
|
|
9
74
|
First release under the new per-PR bump declaration workflow. Shifts release mechanism from "every `/sync` publishes" to "every PR declares a bump, `/sync` aggregates, and only user-visible work ships." PR authors now declare `Version bump: {major|minor|patch|none}` in their PR body; `/sync` picks the highest declared bump since the last tag using precedence `major > minor > patch > none`. All-`none` batches update shared-state files on develop but never cut a release — internal-only work (docs, CI, tests) accumulates without triggering noisy publishes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worclaude",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "The Workflow Layer for Claude Code — scaffold agents, commands, skills, hooks, and memory into any project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -68,7 +68,9 @@
|
|
|
68
68
|
"commander": "^13.1.0",
|
|
69
69
|
"fs-extra": "^11.3.0",
|
|
70
70
|
"inquirer": "^12.5.0",
|
|
71
|
-
"ora": "^8.2.0"
|
|
71
|
+
"ora": "^8.2.0",
|
|
72
|
+
"smol-toml": "^1.6.1",
|
|
73
|
+
"yaml": "^2.8.3"
|
|
72
74
|
},
|
|
73
75
|
"devDependencies": {
|
|
74
76
|
"eslint": "^9.22.0",
|
package/src/commands/doctor.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
TEMPLATE_SKILLS,
|
|
12
12
|
} from '../data/agents.js';
|
|
13
13
|
import { hasClaudeMdMemoryGuidance, readClaudeMd } from '../core/drift-checks.js';
|
|
14
|
-
import { resolveKeyPath } from '../core/file-categorizer.js';
|
|
14
|
+
import { resolveKeyPath, isWorkflowRefFile } from '../core/file-categorizer.js';
|
|
15
15
|
import * as display from '../utils/display.js';
|
|
16
16
|
|
|
17
17
|
// Check categories
|
|
@@ -202,7 +202,7 @@ async function checkClaudeMdMemoryGuidance(projectRoot) {
|
|
|
202
202
|
result(
|
|
203
203
|
WARN,
|
|
204
204
|
'CLAUDE.md memory guidance',
|
|
205
|
-
'CLAUDE.md lacks memory-architecture guidance. Run worclaude upgrade to write a
|
|
205
|
+
'CLAUDE.md lacks memory-architecture guidance. Run worclaude upgrade to write a sidecar under .claude/workflow-ref/CLAUDE.md with suggested additions.'
|
|
206
206
|
),
|
|
207
207
|
];
|
|
208
208
|
}
|
|
@@ -828,10 +828,8 @@ async function checkPendingReviewFiles(projectRoot) {
|
|
|
828
828
|
const claudeDir = path.join(projectRoot, '.claude');
|
|
829
829
|
const allFiles = await listFilesRecursive(claudeDir);
|
|
830
830
|
for (const fp of allFiles) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
pending.push(rel);
|
|
834
|
-
}
|
|
831
|
+
if (!isWorkflowRefFile(fp, claudeDir)) continue;
|
|
832
|
+
pending.push(path.relative(claudeDir, fp).split(path.sep).join('/'));
|
|
835
833
|
}
|
|
836
834
|
} catch {
|
|
837
835
|
// .claude dir might not exist
|
package/src/commands/init.js
CHANGED
|
@@ -528,7 +528,7 @@ function displayMergeReport(report, backupPath) {
|
|
|
528
528
|
}
|
|
529
529
|
if (report.added.skills.length > 0) {
|
|
530
530
|
display.barLine(
|
|
531
|
-
` ${display.green('✓')} ${report.added.skills.length} skills added${report.conflicts.skills.length > 0 ? ` (${report.conflicts.skills.length} conflicts saved
|
|
531
|
+
` ${display.green('✓')} ${report.added.skills.length} skills added${report.conflicts.skills.length > 0 ? ` (${report.conflicts.skills.length} conflicts saved under .claude/workflow-ref/)` : ''}`
|
|
532
532
|
);
|
|
533
533
|
}
|
|
534
534
|
if (report.added.permissions > 0) {
|
|
@@ -549,10 +549,11 @@ function displayMergeReport(report, backupPath) {
|
|
|
549
549
|
...report.conflicts.commands,
|
|
550
550
|
];
|
|
551
551
|
if (allConflicts.length > 0) {
|
|
552
|
-
display.barLine(
|
|
552
|
+
display.barLine(
|
|
553
|
+
`${display.yellow('~')} Conflicts (template copy saved under .claude/workflow-ref/):`
|
|
554
|
+
);
|
|
553
555
|
for (const file of allConflicts) {
|
|
554
|
-
|
|
555
|
-
display.barLine(` ${display.yellow('⚠')} ${file} → ${refName}`);
|
|
556
|
+
display.barLine(` ${display.yellow('⚠')} ${file}`);
|
|
556
557
|
}
|
|
557
558
|
display.newline();
|
|
558
559
|
}
|
|
@@ -606,12 +607,14 @@ function displayMergeReport(report, backupPath) {
|
|
|
606
607
|
display.divider('NEXT');
|
|
607
608
|
display.newline();
|
|
608
609
|
if (allConflicts.length > 0) {
|
|
609
|
-
console.log(
|
|
610
|
+
console.log(
|
|
611
|
+
` ${display.white('1.')} Review files under .claude/workflow-ref/ and merge what's useful`
|
|
612
|
+
);
|
|
610
613
|
}
|
|
611
614
|
if (report.claudeMdHandling === 'suggestions-generated') {
|
|
612
615
|
console.log(` ${display.white('2.')} Review CLAUDE.md.workflow-suggestions`);
|
|
613
616
|
console.log(
|
|
614
|
-
` ${display.white('3.')} Delete .workflow-ref
|
|
617
|
+
` ${display.white('3.')} Delete .claude/workflow-ref/ and .workflow-suggestions when done`
|
|
615
618
|
);
|
|
616
619
|
}
|
|
617
620
|
console.log(
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { scanProject, writeDetectionReport } from '../core/project-scanner/index.js';
|
|
3
|
+
import * as display from '../utils/display.js';
|
|
4
|
+
|
|
5
|
+
function truncate(text, max) {
|
|
6
|
+
if (text.length <= max) return text;
|
|
7
|
+
return text.slice(0, max - 1) + '…';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function summarizeValue(result) {
|
|
11
|
+
const { value, field } = result;
|
|
12
|
+
|
|
13
|
+
switch (field) {
|
|
14
|
+
case 'readme':
|
|
15
|
+
return truncate(value.projectDescription || '(no description extracted)', 80);
|
|
16
|
+
case 'ci': {
|
|
17
|
+
const n = value.workflows?.length || 0;
|
|
18
|
+
return `${value.provider}, ${n} workflow${n === 1 ? '' : 's'}`;
|
|
19
|
+
}
|
|
20
|
+
case 'testing':
|
|
21
|
+
return value.framework + (value.configFile ? ` (${value.configFile})` : '');
|
|
22
|
+
case 'scripts': {
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const key of ['dev', 'test', 'build', 'lint']) {
|
|
25
|
+
if (value[key]) parts.push(`${key}=${value[key].key}`);
|
|
26
|
+
}
|
|
27
|
+
return parts.join(' ') || '(no standard scripts)';
|
|
28
|
+
}
|
|
29
|
+
case 'envVariables':
|
|
30
|
+
return `${value.names.length} variables`;
|
|
31
|
+
case 'orm':
|
|
32
|
+
return value.name;
|
|
33
|
+
case 'monorepo':
|
|
34
|
+
return `${value.tool} (${value.packagePaths.length} packages)`;
|
|
35
|
+
case 'specDocs':
|
|
36
|
+
return `${value.length} doc${value.length === 1 ? '' : 's'}`;
|
|
37
|
+
case 'frameworks':
|
|
38
|
+
return value.map((v) => (v.version ? `${v.name} ${v.version}` : v.name)).join(', ');
|
|
39
|
+
default:
|
|
40
|
+
if (typeof value === 'string') return value;
|
|
41
|
+
if (Array.isArray(value)) return value.join(', ');
|
|
42
|
+
return JSON.stringify(value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatField(field) {
|
|
47
|
+
return field.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderSummary(report, reportPath) {
|
|
51
|
+
const byConfidence = { high: [], medium: [], low: [] };
|
|
52
|
+
for (const r of report.results) {
|
|
53
|
+
byConfidence[r.confidence]?.push(r);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
display.newline();
|
|
57
|
+
display.sectionHeader('WORCLAUDE SCAN');
|
|
58
|
+
display.dim(`Path: ${report.projectRoot}`);
|
|
59
|
+
display.newline();
|
|
60
|
+
|
|
61
|
+
for (const tier of ['high', 'medium', 'low']) {
|
|
62
|
+
const items = byConfidence[tier];
|
|
63
|
+
display.barLine(
|
|
64
|
+
display.white(`${tier[0].toUpperCase() + tier.slice(1)} confidence (${items.length})`)
|
|
65
|
+
);
|
|
66
|
+
if (items.length === 0) {
|
|
67
|
+
display.dim('(none)');
|
|
68
|
+
} else {
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
const label = formatField(item.field).padEnd(20);
|
|
71
|
+
const summary = summarizeValue(item);
|
|
72
|
+
const source = item.source ? display.dimColor(`(${item.source})`) : '';
|
|
73
|
+
console.log(` ${display.green('✓')} ${label} ${summary} ${source}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
display.newline();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
display.barLine(display.white(`Errors (${report.errors.length})`));
|
|
80
|
+
if (report.errors.length === 0) {
|
|
81
|
+
display.dim('(none)');
|
|
82
|
+
} else {
|
|
83
|
+
for (const err of report.errors) {
|
|
84
|
+
console.log(` ${display.red('✗')} ${err.detector}: ${err.kind} — ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
display.newline();
|
|
88
|
+
|
|
89
|
+
display.success(
|
|
90
|
+
`Report written to ${path.relative(report.projectRoot, reportPath) || reportPath}`
|
|
91
|
+
);
|
|
92
|
+
display.newline();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function scanCommand(options = {}) {
|
|
96
|
+
const projectRoot = path.resolve(options.path || process.cwd());
|
|
97
|
+
const quiet = !!options.quiet;
|
|
98
|
+
const jsonMode = !!options.json;
|
|
99
|
+
|
|
100
|
+
let report;
|
|
101
|
+
try {
|
|
102
|
+
report = await scanProject(projectRoot);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`Error: ${err.message}`);
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let reportPath;
|
|
110
|
+
try {
|
|
111
|
+
reportPath = await writeDetectionReport(report, projectRoot);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`Error writing detection report: ${err.message}`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (jsonMode) {
|
|
119
|
+
console.log(JSON.stringify(report, null, 2));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!quiet) {
|
|
124
|
+
renderSummary(report, reportPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { text } from 'node:stream/consumers';
|
|
3
|
+
import {
|
|
4
|
+
loadSetupState,
|
|
5
|
+
saveSetupState,
|
|
6
|
+
clearSetupState,
|
|
7
|
+
isSetupStateStale,
|
|
8
|
+
} from '../core/setup-state.js';
|
|
9
|
+
|
|
10
|
+
function formatAge(updatedAtIso) {
|
|
11
|
+
const ageMs = Date.now() - Date.parse(updatedAtIso);
|
|
12
|
+
const minutes = Math.floor(ageMs / (60 * 1000));
|
|
13
|
+
if (minutes < 90) {
|
|
14
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
15
|
+
}
|
|
16
|
+
const hours = Math.floor(ageMs / (60 * 60 * 1000));
|
|
17
|
+
if (hours < 48) {
|
|
18
|
+
return `${hours} hour${hours === 1 ? '' : 's'}`;
|
|
19
|
+
}
|
|
20
|
+
const days = Math.floor(ageMs / (24 * 60 * 60 * 1000));
|
|
21
|
+
return `${days} day${days === 1 ? '' : 's'}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function runOrExit(fn, { errorPrefix = 'Error' } = {}) {
|
|
25
|
+
try {
|
|
26
|
+
await fn();
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(`${errorPrefix}: ${err.message}`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function showSubcommand(projectRoot) {
|
|
34
|
+
await runOrExit(async () => {
|
|
35
|
+
const state = await loadSetupState(projectRoot);
|
|
36
|
+
if (state === null) {
|
|
37
|
+
console.log('no state');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log(JSON.stringify(state, null, 2));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function saveSubcommand(projectRoot, options) {
|
|
45
|
+
if (!options.stdin) {
|
|
46
|
+
console.error('Error: save requires --stdin');
|
|
47
|
+
process.exitCode = 2;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const inputStream = options.inputStream || process.stdin;
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = await text(inputStream);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(`Error reading stdin: ${err.message}`);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(raw);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`Error: invalid JSON on stdin: ${err.message}`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await runOrExit(() => saveSetupState(projectRoot, parsed));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resetSubcommand(projectRoot) {
|
|
74
|
+
await runOrExit(() => clearSetupState(projectRoot));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function resumeInfoSubcommand(projectRoot) {
|
|
78
|
+
await runOrExit(async () => {
|
|
79
|
+
const state = await loadSetupState(projectRoot);
|
|
80
|
+
if (state === null) {
|
|
81
|
+
console.log('no state');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const age = formatAge(state.updatedAt);
|
|
85
|
+
const staleness = isSetupStateStale(state) ? 'stale' : 'fresh';
|
|
86
|
+
console.log(`state: ${state.currentState}, age: ${age}, staleness: ${staleness}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const SUBCOMMANDS = new Set(['show', 'save', 'reset', 'resume-info']);
|
|
91
|
+
|
|
92
|
+
export async function setupStateCommand(subcommand, options = {}) {
|
|
93
|
+
if (!SUBCOMMANDS.has(subcommand)) {
|
|
94
|
+
console.error(
|
|
95
|
+
`Error: unknown subcommand: ${subcommand} (expected one of show, save, reset, resume-info)`
|
|
96
|
+
);
|
|
97
|
+
process.exitCode = 2;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const projectRoot = path.resolve(options.path || process.cwd());
|
|
102
|
+
|
|
103
|
+
switch (subcommand) {
|
|
104
|
+
case 'show':
|
|
105
|
+
return showSubcommand(projectRoot);
|
|
106
|
+
case 'save':
|
|
107
|
+
return saveSubcommand(projectRoot, options);
|
|
108
|
+
case 'reset':
|
|
109
|
+
return resetSubcommand(projectRoot);
|
|
110
|
+
case 'resume-info':
|
|
111
|
+
return resumeInfoSubcommand(projectRoot);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -3,6 +3,7 @@ import { requireWorkflowMeta, getPackageVersion } from '../core/config.js';
|
|
|
3
3
|
import { hashFile } from '../utils/hash.js';
|
|
4
4
|
import { fileExists, readFile, listFilesRecursive } from '../utils/file.js';
|
|
5
5
|
import { getLatestNpmVersion } from '../utils/npm.js';
|
|
6
|
+
import { isWorkflowRefFile } from '../core/file-categorizer.js';
|
|
6
7
|
import { TECH_STACKS } from '../data/agents.js';
|
|
7
8
|
import * as display from '../utils/display.js';
|
|
8
9
|
|
|
@@ -99,16 +100,17 @@ export async function statusCommand() {
|
|
|
99
100
|
display.newline();
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
// Pending review files
|
|
103
|
+
// Pending review files — anything isWorkflowRefFile() recognizes. That
|
|
104
|
+
// covers the v2.5.1+ .claude/workflow-ref/ tree plus legacy .workflow-ref.md
|
|
105
|
+
// siblings, so users mid-migration still see what they need to resolve.
|
|
103
106
|
const pendingReview = [];
|
|
104
107
|
try {
|
|
105
108
|
const claudeDir = path.join(projectRoot, '.claude');
|
|
106
109
|
const allFiles = await listFilesRecursive(claudeDir);
|
|
107
110
|
for (const fp of allFiles) {
|
|
111
|
+
if (!isWorkflowRefFile(fp, claudeDir)) continue;
|
|
108
112
|
const rel = path.relative(claudeDir, fp).split(path.sep).join('/');
|
|
109
|
-
|
|
110
|
-
pendingReview.push(`.claude/${rel}`);
|
|
111
|
-
}
|
|
113
|
+
pendingReview.push(`.claude/${rel}`);
|
|
112
114
|
}
|
|
113
115
|
} catch {
|
|
114
116
|
// .claude dir might not exist
|
package/src/commands/upgrade.js
CHANGED
|
@@ -5,7 +5,7 @@ import inquirer from 'inquirer';
|
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import { requireWorkflowMeta, writeWorkflowMeta, getPackageVersion } from '../core/config.js';
|
|
7
7
|
import { createBackup } from '../core/backup.js';
|
|
8
|
-
import { categorizeFiles, resolveKeyPath } from '../core/file-categorizer.js';
|
|
8
|
+
import { categorizeFiles, resolveKeyPath, resolveRefPath } from '../core/file-categorizer.js';
|
|
9
9
|
import { buildSettingsJson, mergeSettingsPermissionsAndHooks } from '../core/merger.js';
|
|
10
10
|
import { readTemplate, substituteVariables, updateGitignore } from '../core/scaffolder.js';
|
|
11
11
|
import { buildAgentsMdVariables } from '../core/variables.js';
|
|
@@ -19,7 +19,12 @@ import { writeFile, readFile, fileExists } from '../utils/file.js';
|
|
|
19
19
|
import { hashFile } from '../utils/hash.js';
|
|
20
20
|
import { getLatestNpmVersion } from '../utils/npm.js';
|
|
21
21
|
import * as display from '../utils/display.js';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
semverLessThan,
|
|
24
|
+
migrateSkillFormat,
|
|
25
|
+
patchAgentDescriptions,
|
|
26
|
+
migrateWorkflowRefLocation,
|
|
27
|
+
} from '../core/migration.js';
|
|
23
28
|
|
|
24
29
|
const CONFLICT_CHECK_TYPES = new Set(['hook', 'root-file']);
|
|
25
30
|
|
|
@@ -41,12 +46,6 @@ function selfUpdate(latestVersion) {
|
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
function sidecarPathFor(dest) {
|
|
45
|
-
const ext = path.extname(dest);
|
|
46
|
-
const base = ext ? dest.slice(0, dest.length - ext.length) : dest;
|
|
47
|
-
return `${base}.workflow-ref${ext}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
49
|
async function renderTemplate({ templatePath, type }, variables) {
|
|
51
50
|
const templateContent = await readTemplate(templatePath);
|
|
52
51
|
return type === 'root-file' ? substituteVariables(templateContent, variables) : templateContent;
|
|
@@ -57,8 +56,8 @@ async function writeTemplateToDest(entry, dest, variables) {
|
|
|
57
56
|
await writeFile(dest, await renderTemplate(entry, variables));
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
async function writeSidecarFor(entry,
|
|
61
|
-
const sidecarPath =
|
|
59
|
+
async function writeSidecarFor(entry, projectRoot, variables) {
|
|
60
|
+
const sidecarPath = resolveRefPath(entry.key, projectRoot);
|
|
62
61
|
await writeFile(sidecarPath, await renderTemplate(entry, variables));
|
|
63
62
|
return sidecarPath;
|
|
64
63
|
}
|
|
@@ -122,7 +121,7 @@ function renderRepairPreview(plan) {
|
|
|
122
121
|
}
|
|
123
122
|
if (plan.claudeMdNeedsSidecar) {
|
|
124
123
|
sidecarLines.push(
|
|
125
|
-
' ~ CLAUDE.md memory guidance missing (will write
|
|
124
|
+
' ~ CLAUDE.md memory guidance missing (will write .claude/workflow-ref/CLAUDE.md sidecar)'
|
|
126
125
|
);
|
|
127
126
|
}
|
|
128
127
|
if (sidecarLines.length > 0) {
|
|
@@ -197,7 +196,7 @@ async function applyRepairPass(projectRoot, plan, variables) {
|
|
|
197
196
|
if (await fileExists(dest)) {
|
|
198
197
|
const matches = await diskContentMatchesTemplate(entry, dest, variables);
|
|
199
198
|
if (!matches) {
|
|
200
|
-
const sidecarPath = await writeSidecarFor(entry,
|
|
199
|
+
const sidecarPath = await writeSidecarFor(entry, projectRoot, variables);
|
|
201
200
|
result.migrationConflicts.push({ key: entry.key, dest, sidecarPath });
|
|
202
201
|
continue;
|
|
203
202
|
}
|
|
@@ -282,7 +281,7 @@ async function runRepairOnlyFlow({ projectRoot, meta, plan, variables, dryRun, y
|
|
|
282
281
|
}
|
|
283
282
|
if (result.migrationConflicts.length > 0) {
|
|
284
283
|
display.barLine(
|
|
285
|
-
`Conflicts: ${result.migrationConflicts.length} files ${display.dimColor('(saved
|
|
284
|
+
`Conflicts: ${result.migrationConflicts.length} files ${display.dimColor('(saved under .claude/workflow-ref/)')}`
|
|
286
285
|
);
|
|
287
286
|
}
|
|
288
287
|
if (result.createdDirs.length > 0) {
|
|
@@ -295,7 +294,7 @@ async function runRepairOnlyFlow({ projectRoot, meta, plan, variables, dryRun, y
|
|
|
295
294
|
|
|
296
295
|
if (result.migrationConflicts.length > 0 || result.sidecars.length > 0) {
|
|
297
296
|
display.newline();
|
|
298
|
-
display.barLine(`Review .workflow-ref
|
|
297
|
+
display.barLine(`Review files under .claude/workflow-ref/ and merge what's useful.`);
|
|
299
298
|
}
|
|
300
299
|
} catch (err) {
|
|
301
300
|
spinner.fail('Repair failed.');
|
|
@@ -440,17 +439,25 @@ export async function upgradeCommand(options = {}) {
|
|
|
440
439
|
spinner.start('Applying updates...');
|
|
441
440
|
}
|
|
442
441
|
|
|
442
|
+
// v2.5.1 migration: relocate legacy ref files out of Claude-Code-scanned
|
|
443
|
+
// dirs. Runs unconditionally since it's idempotent and the fix applies
|
|
444
|
+
// to any project with legacy refs, regardless of source version.
|
|
445
|
+
const refRelocationReport = await migrateWorkflowRefLocation(projectRoot);
|
|
446
|
+
if (refRelocationReport.moved > 0) {
|
|
447
|
+
spinner.text = `Relocated ${refRelocationReport.moved} legacy ref file(s)...`;
|
|
448
|
+
}
|
|
449
|
+
|
|
443
450
|
// Auto-update files
|
|
444
451
|
for (const { key, templatePath } of categories.autoUpdate) {
|
|
445
452
|
const content = await readTemplate(templatePath);
|
|
446
453
|
await writeFile(resolveKeyPath(key, projectRoot), content);
|
|
447
454
|
}
|
|
448
455
|
|
|
449
|
-
// Conflict files: save
|
|
456
|
+
// Conflict files: save under .claude/workflow-ref/ so they never collide
|
|
457
|
+
// with Claude Code's command/agent discovery.
|
|
450
458
|
for (const { key, templatePath } of categories.conflict) {
|
|
451
459
|
const content = await readTemplate(templatePath);
|
|
452
|
-
|
|
453
|
-
await writeFile(resolveKeyPath(refKey, projectRoot), content);
|
|
460
|
+
await writeFile(resolveRefPath(key, projectRoot), content);
|
|
454
461
|
}
|
|
455
462
|
|
|
456
463
|
// New files (non-migration types handled here; migration types were
|
|
@@ -510,7 +517,7 @@ export async function upgradeCommand(options = {}) {
|
|
|
510
517
|
}
|
|
511
518
|
if (repairResult.migrationConflicts.length > 0) {
|
|
512
519
|
display.barLine(
|
|
513
|
-
`Migration conflicts: ${repairResult.migrationConflicts.length} ${display.dimColor('(saved
|
|
520
|
+
`Migration conflicts: ${repairResult.migrationConflicts.length} ${display.dimColor('(saved under .claude/workflow-ref/)')}`
|
|
514
521
|
);
|
|
515
522
|
}
|
|
516
523
|
if (categories.autoUpdate.length > 0) {
|
|
@@ -518,7 +525,12 @@ export async function upgradeCommand(options = {}) {
|
|
|
518
525
|
}
|
|
519
526
|
if (categories.conflict.length > 0) {
|
|
520
527
|
display.barLine(
|
|
521
|
-
`Conflicts: ${categories.conflict.length} files ${display.dimColor('(saved
|
|
528
|
+
`Conflicts: ${categories.conflict.length} files ${display.dimColor('(saved under .claude/workflow-ref/)')}`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (refRelocationReport.moved > 0) {
|
|
532
|
+
display.barLine(
|
|
533
|
+
`Relocated: ${refRelocationReport.moved} legacy ref file(s) ${display.dimColor('→ .claude/workflow-ref/')}`
|
|
522
534
|
);
|
|
523
535
|
}
|
|
524
536
|
if (plan.templateNewFiles.length > 0) {
|
|
@@ -558,7 +570,7 @@ export async function upgradeCommand(options = {}) {
|
|
|
558
570
|
repairResult.sidecars.length > 0
|
|
559
571
|
) {
|
|
560
572
|
display.newline();
|
|
561
|
-
display.barLine(`Review .workflow-ref
|
|
573
|
+
display.barLine(`Review files under .claude/workflow-ref/ and merge what's useful.`);
|
|
562
574
|
}
|
|
563
575
|
} catch (err) {
|
|
564
576
|
spinner.fail('Upgrade failed.');
|
package/src/core/drift-checks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { fileExists, readFile, writeFile } from '../utils/file.js';
|
|
4
|
+
import { resolveRefPath } from './file-categorizer.js';
|
|
4
5
|
|
|
5
6
|
export const MEMORY_GUIDANCE_KEYWORDS = [
|
|
6
7
|
'memory architecture',
|
|
@@ -47,7 +48,7 @@ export function buildMemoryGuidanceSidecar() {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export async function writeMemoryGuidanceSidecar(projectRoot) {
|
|
50
|
-
const dest =
|
|
51
|
+
const dest = resolveRefPath('root/CLAUDE.md', projectRoot);
|
|
51
52
|
await writeFile(dest, buildMemoryGuidanceSidecar());
|
|
52
53
|
return dest;
|
|
53
54
|
}
|
|
@@ -36,6 +36,7 @@ export function isAlwaysScaffolded(entry, meta) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export const ROOT_KEY_PREFIX = 'root/';
|
|
39
|
+
export const WORKFLOW_REF_DIR = 'workflow-ref';
|
|
39
40
|
|
|
40
41
|
export function resolveKeyPath(key, projectRoot) {
|
|
41
42
|
if (key.startsWith(ROOT_KEY_PREFIX)) {
|
|
@@ -45,6 +46,43 @@ export function resolveKeyPath(key, projectRoot) {
|
|
|
45
46
|
return path.join(projectRoot, '.claude', ...key.split('/'));
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Project-root-relative path for a ref file. Useful for scaffoldFile() which
|
|
51
|
+
* takes a destRelativePath. Returns e.g. ".claude/workflow-ref/commands/sync.md".
|
|
52
|
+
* Accepts a workflow key ("commands/sync.md", "root/CLAUDE.md") or a plain
|
|
53
|
+
* relative path. Root-level files (CLAUDE.md, AGENTS.md) land directly
|
|
54
|
+
* under workflow-ref/, not under workflow-ref/root/.
|
|
55
|
+
*/
|
|
56
|
+
export function workflowRefRelPath(keyOrRelPath) {
|
|
57
|
+
const rel = keyOrRelPath.startsWith(ROOT_KEY_PREFIX)
|
|
58
|
+
? keyOrRelPath.slice(ROOT_KEY_PREFIX.length)
|
|
59
|
+
: keyOrRelPath;
|
|
60
|
+
return path.join('.claude', WORKFLOW_REF_DIR, ...rel.split('/'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve an absolute ref file destination for a given relative path.
|
|
65
|
+
* Wraps workflowRefRelPath with projectRoot.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveRefPath(keyOrRelPath, projectRoot) {
|
|
68
|
+
return path.join(projectRoot, workflowRefRelPath(keyOrRelPath));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Predicate for "is this file a reference copy?". Matches both the v2.5.1+
|
|
73
|
+
* layout (anywhere under .claude/workflow-ref/) and the legacy sibling
|
|
74
|
+
* convention (.workflow-ref.md suffix). Used by status, doctor, and remover
|
|
75
|
+
* so they stay in sync across the two location schemes.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} absPath — absolute path on disk
|
|
78
|
+
* @param {string} claudeDir — absolute path to the project's .claude/ directory
|
|
79
|
+
*/
|
|
80
|
+
export function isWorkflowRefFile(absPath, claudeDir) {
|
|
81
|
+
const refDirPrefix = path.join(claudeDir, WORKFLOW_REF_DIR) + path.sep;
|
|
82
|
+
if (absPath.startsWith(refDirPrefix)) return true;
|
|
83
|
+
return absPath.endsWith('.workflow-ref.md');
|
|
84
|
+
}
|
|
85
|
+
|
|
48
86
|
/**
|
|
49
87
|
* Build a map of all workflow template files to their hash keys, template paths, and hashes.
|
|
50
88
|
* Hash keys match the format stored in workflow-meta.json (relative to .claude/).
|