worclaude 2.5.1 → 2.6.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 CHANGED
@@ -4,6 +4,58 @@ All notable changes to worclaude are documented in this file. Format loosely fol
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.6.1] — 2026-04-22
8
+
9
+ Supply-chain scanner hygiene. Adds a `socket.yml` at the repo root so Socket (and any tool honoring the same schema) stops treating `tests/fixtures/scanner/**` manifests as real worclaude dependencies. The fixtures pin intentionally-outdated packages (`next@14.2.3`, `vitest@1.4.0`, `prisma@5.10.0`, etc.) as deterministic inputs to the Part A detectors — they are never installed (not referenced from root `package.json`), never shipped (`tests/` is excluded by the npm `files` whitelist), and never executed. Without the ignore, fixture deps surface on PR reviews as critical CVEs (CVE-2025-29927 Next.js middleware auth bypass, Vitest 1.4.0 RCE) that do not apply to worclaude. `SECURITY.md` is expanded with a "Supply Chain Scanner Findings" section documenting the fixture rationale, the real seven-package runtime dependency list, and the by-design `filesystemAccess` capability disclosure on `fs-extra`-heavy scaffolding code.
10
+
11
+ ### Added
12
+
13
+ - **`socket.yml` at repo root** (PR #107) — `version: 2` schema with `projectIgnorePaths: [tests/fixtures/**]`. Respected by Socket's GitHub App on every PR review and by the Socket CLI's `socket scan create` command. Verified locally via `socket scan create --report`: manifests discovered drop from 21 to 6, scan verdict goes from unhealthy (2 critical + many high/medium false positives) to `healthy: true, alerts: 0` at warn level.
14
+
15
+ ### Changed
16
+
17
+ - **`SECURITY.md` supported-versions row** bumped to `2.6.x` (from `2.4.x`) to reflect the current support window.
18
+
19
+ ### Docs
20
+
21
+ - **`SECURITY.md` — Supply Chain Scanner Findings section** (PR #107) documents (1) why `tests/fixtures/scanner/**` manifests are not real dependencies, (2) worclaude's real seven-package runtime dep list, and (3) the `filesystemAccess` capability flag as a by-design disclosure for a scaffolding CLI rather than a vulnerability. Intended as a standing reference for any future SCA tool that surfaces the same false positives.
22
+
23
+ ## [2.6.0] — 2026-04-22
24
+
25
+ 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.
26
+
27
+ ### Added
28
+
29
+ - **`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.
30
+ - **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).
31
+ - **`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.
32
+ - **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.
33
+ - **`/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).
34
+ - **`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.
35
+ - **`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.
36
+ - **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).
37
+ - **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`).
38
+ - **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.
39
+
40
+ ### Changed
41
+
42
+ - **`.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`.
43
+ - **`/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.
44
+
45
+ ### Tests
46
+
47
+ - **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/`.
48
+ - **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**.
49
+
50
+ ### Non-goals (explicitly deferred)
51
+
52
+ - No `/setup --edit <field>` flow for correcting prior answers (Tier 2).
53
+ - No automated state-machine test harness (mock-Claude driver walking every transition). Tier 2.
54
+ - No automated end-to-end testing of `/setup` across fixture projects — manual e2e per the 13-case checklist in the phase prompt.
55
+ - No schema migrator for `setup-state.json`. v1 policy is "reset on schema bump."
56
+ - No architecture classification, directory-tree module inference, CI required-check detection (GitHub API), or monorepo sub-package scanning — all Tier 2.
57
+ - No rename of the existing `src/core/detector.js` (scenario detection). The new scanner lives under `src/core/project-scanner/` to avoid name collision.
58
+
7
59
  ## [2.5.1] — 2026-04-21
8
60
 
9
61
  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`.
package/SECURITY.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | ------- | ------------------ |
7
- | 2.4.x | :white_check_mark: |
8
- | < 2.4 | :x: |
7
+ | 2.6.x | :white_check_mark: |
8
+ | < 2.6 | :x: |
9
9
 
10
10
  ## Reporting a Vulnerability
11
11
 
@@ -19,3 +19,50 @@ Please do **not** open a public issue for security vulnerabilities.
19
19
 
20
20
  You can expect an initial response within 48 hours.
21
21
  If the vulnerability is accepted, a fix will be prioritized and released as a patch version.
22
+
23
+ ## Supply Chain Scanner Findings
24
+
25
+ Automated SCA tools (Socket, Snyk, GitHub Dependabot) sometimes surface
26
+ alerts that are not real exposures for worclaude. The most common cases:
27
+
28
+ ### Test fixture manifests are not real dependencies
29
+
30
+ `tests/fixtures/scanner/**` contains static `package.json`, `pnpm-lock.yaml`,
31
+ `package-lock.json`, and `pyproject.toml` files used to exercise the
32
+ project-scanner detectors in `src/core/project-scanner/`. They pin
33
+ intentionally-outdated versions (e.g. `next@14.2.3`, `vitest@1.4.0`,
34
+ `prisma@5.10.0`) so the detectors have realistic inputs to match against.
35
+
36
+ These fixtures are:
37
+
38
+ - **Never installed.** They are not referenced from the root `package.json`.
39
+ - **Not shipped to npm.** `package.json`'s `files` whitelist publishes only
40
+ `src/`, `templates/`, and top-level docs. `tests/` is excluded.
41
+ - **Not executed.** The scanner reads them as JSON/TOML and inspects the
42
+ dependency lists; it never imports or runs the packages named inside.
43
+
44
+ Worclaude's repo includes `socket.yml` to stop Socket from scanning this
45
+ directory. Other SCA tools may need an equivalent `ignore` directive.
46
+
47
+ ### Real runtime dependencies
48
+
49
+ ```
50
+ chalk ^5.4.1
51
+ commander ^13.1.0
52
+ fs-extra ^11.3.0
53
+ inquirer ^12.5.0
54
+ ora ^8.2.0
55
+ smol-toml ^1.6.1
56
+ yaml ^2.8.3
57
+ ```
58
+
59
+ No Next.js, React, Express, Prisma, or Stripe appear at runtime despite
60
+ what a fixture-inclusive scan might suggest.
61
+
62
+ ### Filesystem access flag is by design
63
+
64
+ Worclaude scaffolds files into the user's project tree: templates → `.claude/`,
65
+ settings.json merges, timestamped backups under `.claude-backup-*/`, and
66
+ an opt-in `workflow-meta.json`. The `fs-extra`-based filesystem capability
67
+ flag is a disclosure, not a vulnerability — removing it would delete the
68
+ tool's core function.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worclaude",
3
- "version": "2.5.1",
3
+ "version": "2.6.1",
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",
@@ -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
+ }
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { fileExists, dirExists } from '../../../utils/file.js';
4
+
5
+ export default async function detectCi(projectRoot) {
6
+ const results = [];
7
+
8
+ const workflowsDir = path.join(projectRoot, '.github', 'workflows');
9
+ if (await dirExists(workflowsDir)) {
10
+ try {
11
+ const entries = await fs.readdir(workflowsDir, { withFileTypes: true });
12
+ const workflows = entries
13
+ .filter((e) => e.isFile() && /\.ya?ml$/.test(e.name))
14
+ .map((e) => e.name)
15
+ .sort();
16
+ if (workflows.length > 0) {
17
+ results.push({
18
+ field: 'ci',
19
+ value: { provider: 'GitHub Actions', workflows },
20
+ confidence: 'high',
21
+ source: '.github/workflows/',
22
+ candidates: null,
23
+ });
24
+ }
25
+ } catch {
26
+ /* missing or unreadable — non-fatal */
27
+ }
28
+ }
29
+
30
+ const other = [
31
+ { file: '.gitlab-ci.yml', provider: 'GitLab CI' },
32
+ { file: '.circleci/config.yml', provider: 'CircleCI' },
33
+ { file: 'azure-pipelines.yml', provider: 'Azure Pipelines' },
34
+ { file: 'Jenkinsfile', provider: 'Jenkins' },
35
+ { file: '.drone.yml', provider: 'Drone' },
36
+ { file: 'bitbucket-pipelines.yml', provider: 'Bitbucket Pipelines' },
37
+ ];
38
+ for (const { file, provider } of other) {
39
+ if (await fileExists(path.join(projectRoot, file))) {
40
+ results.push({
41
+ field: 'ci',
42
+ value: { provider, workflows: [path.basename(file)] },
43
+ confidence: 'high',
44
+ source: file,
45
+ candidates: null,
46
+ });
47
+ }
48
+ }
49
+
50
+ return results;
51
+ }
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import { fileExists } from '../../../utils/file.js';
3
+
4
+ const DEPLOYMENTS = [
5
+ { file: 'vercel.json', target: 'Vercel' },
6
+ { file: 'netlify.toml', target: 'Netlify' },
7
+ { file: 'Dockerfile', target: 'Docker' },
8
+ { file: 'fly.toml', target: 'Fly.io' },
9
+ { file: 'railway.toml', target: 'Railway' },
10
+ { file: 'app.yaml', target: 'Google App Engine' },
11
+ { file: 'serverless.yml', target: 'Serverless Framework' },
12
+ { file: 'render.yaml', target: 'Render' },
13
+ { file: '.platform.app.yaml', target: 'Platform.sh' },
14
+ { file: 'wrangler.toml', target: 'Cloudflare Workers' },
15
+ { file: 'amplify.yml', target: 'AWS Amplify' },
16
+ ];
17
+
18
+ export default async function detectDeployment(projectRoot) {
19
+ const results = [];
20
+ for (const { file, target } of DEPLOYMENTS) {
21
+ if (await fileExists(path.join(projectRoot, file))) {
22
+ results.push({
23
+ field: 'deployment',
24
+ value: target,
25
+ confidence: 'high',
26
+ source: file,
27
+ candidates: null,
28
+ });
29
+ }
30
+ }
31
+ return results;
32
+ }
@@ -0,0 +1,78 @@
1
+ import path from 'node:path';
2
+ import { fileExists, readFile } from '../../../utils/file.js';
3
+ import { getAllDeps, depMatches } from '../manifests.js';
4
+
5
+ const ENV_FILES = ['.env.example', '.env.template', '.env.sample'];
6
+
7
+ const SDK_SERVICES = [
8
+ { sdkPrefix: '@stripe/', service: 'Stripe', envPrefix: 'STRIPE_' },
9
+ { sdkPrefix: 'stripe', service: 'Stripe', envPrefix: 'STRIPE_' },
10
+ { sdkPrefix: '@aws-sdk/', service: 'AWS', envPrefix: 'AWS_' },
11
+ { sdkPrefix: '@sendgrid/', service: 'SendGrid', envPrefix: 'SENDGRID_' },
12
+ { sdkPrefix: 'twilio', service: 'Twilio', envPrefix: 'TWILIO_' },
13
+ { sdkPrefix: 'openai', service: 'OpenAI', envPrefix: 'OPENAI_' },
14
+ { sdkPrefix: '@anthropic-ai/', service: 'Anthropic', envPrefix: 'ANTHROPIC_' },
15
+ { sdkPrefix: '@google-cloud/', service: 'Google Cloud', envPrefix: 'GOOGLE_' },
16
+ { sdkPrefix: '@slack/', service: 'Slack', envPrefix: 'SLACK_' },
17
+ { sdkPrefix: 'postmark', service: 'Postmark', envPrefix: 'POSTMARK_' },
18
+ { sdkPrefix: 'resend', service: 'Resend', envPrefix: 'RESEND_' },
19
+ { sdkPrefix: '@sentry/', service: 'Sentry', envPrefix: 'SENTRY_' },
20
+ ];
21
+
22
+ function parseEnvNames(content) {
23
+ const names = [];
24
+ for (const rawLine of content.split(/\r?\n/)) {
25
+ const line = rawLine.trim();
26
+ if (line === '' || line.startsWith('#')) continue;
27
+ const match = line.match(/^(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=/);
28
+ if (match) names.push(match[1]);
29
+ }
30
+ return names;
31
+ }
32
+
33
+ export default async function detectEnvVariables(projectRoot) {
34
+ let sourceFile = null;
35
+ let content = null;
36
+ for (const f of ENV_FILES) {
37
+ const p = path.join(projectRoot, f);
38
+ if (await fileExists(p)) {
39
+ sourceFile = f;
40
+ content = await readFile(p);
41
+ break;
42
+ }
43
+ }
44
+ if (!sourceFile) return [];
45
+
46
+ const names = parseEnvNames(content);
47
+ if (names.length === 0) {
48
+ return [
49
+ {
50
+ field: 'envVariables',
51
+ value: { names: [], inferredServices: [] },
52
+ confidence: 'high',
53
+ source: sourceFile,
54
+ candidates: null,
55
+ },
56
+ ];
57
+ }
58
+
59
+ const { js, py } = await getAllDeps(projectRoot);
60
+ const allDeps = { ...js, ...py };
61
+ const inferredServices = new Set();
62
+ for (const { sdkPrefix, service, envPrefix } of SDK_SERVICES) {
63
+ const pattern = sdkPrefix.endsWith('/') ? `${sdkPrefix}*` : sdkPrefix;
64
+ if (depMatches(allDeps, pattern) && names.some((n) => n.startsWith(envPrefix))) {
65
+ inferredServices.add(service);
66
+ }
67
+ }
68
+
69
+ return [
70
+ {
71
+ field: 'envVariables',
72
+ value: { names, inferredServices: Array.from(inferredServices) },
73
+ confidence: 'high',
74
+ source: sourceFile,
75
+ candidates: null,
76
+ },
77
+ ];
78
+ }
@@ -0,0 +1,45 @@
1
+ import { getAllDeps, depMatches } from '../manifests.js';
2
+
3
+ const SDK_MAP = [
4
+ { match: 'stripe', service: 'Stripe' },
5
+ { match: '@sendgrid/', service: 'SendGrid' },
6
+ { match: '@aws-sdk/', service: 'AWS' },
7
+ { match: 'twilio', service: 'Twilio' },
8
+ { match: 'openai', service: 'OpenAI' },
9
+ { match: '@anthropic-ai/sdk', service: 'Anthropic' },
10
+ { match: '@google-cloud/', service: 'Google Cloud' },
11
+ { match: '@slack/', service: 'Slack' },
12
+ { match: 'postmark', service: 'Postmark' },
13
+ { match: 'resend', service: 'Resend' },
14
+ { match: 'algoliasearch', service: 'Algolia' },
15
+ { match: 'pusher', service: 'Pusher' },
16
+ { match: 'posthog-js', service: 'PostHog' },
17
+ { match: 'posthog-node', service: 'PostHog' },
18
+ { match: '@sentry/', service: 'Sentry' },
19
+ { match: 'mixpanel', service: 'Mixpanel' },
20
+ { match: 'mixpanel-browser', service: 'Mixpanel' },
21
+ ];
22
+
23
+ export default async function detectExternalApis(projectRoot) {
24
+ const { js, py, hasPackageJson, hasPyproject } = await getAllDeps(projectRoot);
25
+ if (!hasPackageJson && !hasPyproject) return [];
26
+
27
+ const allDeps = { ...js, ...py };
28
+ const services = new Set();
29
+ for (const { match, service } of SDK_MAP) {
30
+ const pattern = match.endsWith('/') ? `${match}*` : match;
31
+ if (depMatches(allDeps, pattern)) services.add(service);
32
+ }
33
+
34
+ if (services.size === 0) return [];
35
+
36
+ return [
37
+ {
38
+ field: 'externalApis',
39
+ value: Array.from(services),
40
+ confidence: 'high',
41
+ source: hasPackageJson ? 'package.json' : 'pyproject.toml',
42
+ candidates: null,
43
+ },
44
+ ];
45
+ }
@@ -0,0 +1,56 @@
1
+ import { getAllDeps } from '../manifests.js';
2
+
3
+ const FRAMEWORKS = [
4
+ { dep: 'next', name: 'Next.js' },
5
+ { dep: 'nuxt', name: 'Nuxt' },
6
+ { dep: 'astro', name: 'Astro' },
7
+ { dep: 'remix', name: 'Remix' },
8
+ { dep: 'sveltekit', name: 'SvelteKit' },
9
+ { dep: '@sveltejs/kit', name: 'SvelteKit' },
10
+ { dep: 'react', name: 'React' },
11
+ { dep: 'vue', name: 'Vue' },
12
+ { dep: 'svelte', name: 'Svelte' },
13
+ { dep: 'solid-js', name: 'SolidJS' },
14
+ { dep: 'express', name: 'Express' },
15
+ { dep: 'fastify', name: 'Fastify' },
16
+ { dep: 'hono', name: 'Hono' },
17
+ { dep: 'koa', name: 'Koa' },
18
+ { dep: '@nestjs/core', name: 'NestJS' },
19
+ { dep: 'fastapi', name: 'FastAPI' },
20
+ { dep: 'starlette', name: 'Starlette' },
21
+ { dep: 'django', name: 'Django' },
22
+ { dep: 'flask', name: 'Flask' },
23
+ ];
24
+
25
+ export default async function detectFrameworks(projectRoot) {
26
+ const { js, py, hasPackageJson, hasPyproject } = await getAllDeps(projectRoot);
27
+ if (!hasPackageJson && !hasPyproject) return [];
28
+
29
+ const detected = [];
30
+ const sources = new Set();
31
+ const seen = new Set();
32
+ for (const { dep, name } of FRAMEWORKS) {
33
+ if (seen.has(name)) continue;
34
+ if (js[dep] !== undefined) {
35
+ detected.push({ name, version: typeof js[dep] === 'string' ? js[dep] : '' });
36
+ sources.add('package.json');
37
+ seen.add(name);
38
+ } else if (py[dep] !== undefined) {
39
+ detected.push({ name, version: typeof py[dep] === 'string' ? py[dep] : '' });
40
+ sources.add('pyproject.toml');
41
+ seen.add(name);
42
+ }
43
+ }
44
+
45
+ if (detected.length === 0) return [];
46
+
47
+ return [
48
+ {
49
+ field: 'frameworks',
50
+ value: detected,
51
+ confidence: 'high',
52
+ source: Array.from(sources).join(', '),
53
+ candidates: null,
54
+ },
55
+ ];
56
+ }