@bluestep-systems/bspecs 0.10.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.
Files changed (83) hide show
  1. package/README.md +129 -0
  2. package/cli.js +74 -0
  3. package/package.json +30 -0
  4. package/src/prompts.js +74 -0
  5. package/src/scaffold.js +152 -0
  6. package/src/sync.js +123 -0
  7. package/src/utils.js +95 -0
  8. package/templates/claude/agents/b6p-code-review.md +81 -0
  9. package/templates/claude/agents/b6p-commenter.md +59 -0
  10. package/templates/claude/agents/b6p-task-implementer.md +77 -0
  11. package/templates/claude/hooks/block-generated-files.sh +16 -0
  12. package/templates/claude/hooks/block-tsc.sh +16 -0
  13. package/templates/claude/hooks/prettier-on-save.sh +21 -0
  14. package/templates/claude/instructions/b6p-platform.md.template +185 -0
  15. package/templates/claude/instructions/bsjs-development.md.template +430 -0
  16. package/templates/claude/instructions/conventions/always-snapshot.md.template +25 -0
  17. package/templates/claude/instructions/conventions/blueiq-no-ai-branding.md.template +11 -0
  18. package/templates/claude/instructions/conventions/date-format.md.template +27 -0
  19. package/templates/claude/instructions/conventions/endpoint-approach.md.template +9 -0
  20. package/templates/claude/instructions/conventions/formula-patterns.md.template +71 -0
  21. package/templates/claude/instructions/conventions/no-global-dollar.md.template +9 -0
  22. package/templates/claude/instructions/conventions/push-inner-draft.md.template +21 -0
  23. package/templates/claude/instructions/conventions/separate-files.md.template +17 -0
  24. package/templates/claude/instructions/conventions/single-script.md.template +28 -0
  25. package/templates/claude/instructions/conventions/snapshot-integrity.md.template +23 -0
  26. package/templates/claude/instructions/conventions/top-level-const-tdz.md.template +33 -0
  27. package/templates/claude/instructions/conventions/ts-in-template-literal.md.template +48 -0
  28. package/templates/claude/instructions/conventions/tsc-rootdir.md.template +17 -0
  29. package/templates/claude/instructions/gotchas/common-gotchas.md.template +91 -0
  30. package/templates/claude/instructions/gotchas/fetched-resource-code.md.template +9 -0
  31. package/templates/claude/instructions/index.md.template +82 -0
  32. package/templates/claude/instructions/reference/api-patterns.md.template +487 -0
  33. package/templates/claude/instructions/reference/blueiq-credit-integration-playbook.md.template +31 -0
  34. package/templates/claude/instructions/reference/chronounit-months.md.template +37 -0
  35. package/templates/claude/instructions/reference/code-patterns.md.template +265 -0
  36. package/templates/claude/instructions/reference/component-library.md.template +217 -0
  37. package/templates/claude/instructions/reference/crm-dashboard-inspo.md.template +17 -0
  38. package/templates/claude/instructions/reference/csv-parsing.md.template +18 -0
  39. package/templates/claude/instructions/reference/dashboard-design-system.md.template +38 -0
  40. package/templates/claude/instructions/reference/datetime-field-write.md.template +27 -0
  41. package/templates/claude/instructions/reference/design-system.md.template +150 -0
  42. package/templates/claude/instructions/reference/dpn-dashboard-framework.md.template +29 -0
  43. package/templates/claude/instructions/reference/endpoint-method-call.md.template +10 -0
  44. package/templates/claude/instructions/reference/endpoint-no-delete-method.md.template +9 -0
  45. package/templates/claude/instructions/reference/endpoint-output-channel.md.template +23 -0
  46. package/templates/claude/instructions/reference/endpoint-urls.md.template +15 -0
  47. package/templates/claude/instructions/reference/entry-delete.md.template +40 -0
  48. package/templates/claude/instructions/reference/file-execution.md.template +113 -0
  49. package/templates/claude/instructions/reference/http-requester.md.template +37 -0
  50. package/templates/claude/instructions/reference/id-full-vs-short.md.template +15 -0
  51. package/templates/claude/instructions/reference/internal-loopback-fetch.md.template +24 -0
  52. package/templates/claude/instructions/reference/localdate-parse.md.template +16 -0
  53. package/templates/claude/instructions/reference/merge-report-memo-json.md.template +25 -0
  54. package/templates/claude/instructions/reference/merge-report-static-index.md.template +29 -0
  55. package/templates/claude/instructions/reference/merge-report-urls.md.template +67 -0
  56. package/templates/claude/instructions/reference/multi-entry-in-multi-entry.md.template +21 -0
  57. package/templates/claude/instructions/reference/named-controls-submit.md.template +11 -0
  58. package/templates/claude/instructions/reference/new-entry-id.md.template +30 -0
  59. package/templates/claude/instructions/reference/relationship-field-set.md.template +37 -0
  60. package/templates/claude/instructions/reference/send-message-abort.md.template +37 -0
  61. package/templates/claude/instructions/reference/session-cookie-forwarding.md.template +31 -0
  62. package/templates/claude/instructions/reference/singleselect-null-copy.md.template +21 -0
  63. package/templates/claude/instructions/reference/staff-query-permission-gating.md.template +27 -0
  64. package/templates/claude/instructions/reference/timefield-vs-datetimefield.md.template +13 -0
  65. package/templates/claude/instructions/reference/user-zone-id.md.template +16 -0
  66. package/templates/claude/settings.json.template +46 -0
  67. package/templates/claude/skills/b6p-audit/SKILL.md +82 -0
  68. package/templates/claude/skills/b6p-pull/SKILL.md +123 -0
  69. package/templates/claude/skills/b6p-push/SKILL.md +70 -0
  70. package/templates/claude/skills/bug-fix/SKILL.md +28 -0
  71. package/templates/claude/skills/spec-create/SKILL.md +60 -0
  72. package/templates/claude/skills/spec-execute/SKILL.md +51 -0
  73. package/templates/claude/skills/spec-status/SKILL.md +20 -0
  74. package/templates/claude/skills/task-comment/SKILL.md +96 -0
  75. package/templates/claude/spec-templates/design.template.md +36 -0
  76. package/templates/claude/spec-templates/requirements.template.md +26 -0
  77. package/templates/claude/spec-templates/tasks.template.md +37 -0
  78. package/templates/module/README.md.template +46 -0
  79. package/templates/root/.gitignore.template +14 -0
  80. package/templates/root/.prettierrc.template +8 -0
  81. package/templates/root/CLAUDE.md.template +157 -0
  82. package/templates/root/README.md.template +58 -0
  83. package/templates/root/package.json.template +15 -0
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @bluestep-systems/bspecs
2
+
3
+ CLI for scaffolding BlueStep projects with spec-driven development conventions for Claude Code.
4
+
5
+ ## What it does
6
+
7
+ `bspecs` generates a project directory ready to use with:
8
+
9
+ - Claude Code skills (`/spec-create`, `/spec-execute`, `/b6p-pull`, `/b6p-push`, and more)
10
+ - BlueStep subagents — `b6p-task-implementer` (isolated task execution; `/spec-execute` delegates to it), `b6p-commenter` (component README), `b6p-code-review` (report-only review)
11
+ - Automatic hooks (prettier on save, generated-file blocking, `b6p` integration)
12
+ - Instructions for Claude Code (the template tree is the single source of truth)
13
+ - Spec templates (`requirements.md`, `design.md`, `tasks.md`)
14
+ - The `b6p` CLI wired into each project as a devDependency, invoked via `npx b6p` (no global install or shell/PATH detection)
15
+
16
+ ## Installation
17
+
18
+ `bspecs` and the `b6p` CLI it depends on are published to the **public npm registry**, so there is no
19
+ token or `~/.npmrc` setup — install in one command:
20
+
21
+ ```sh
22
+ npm install -g @bluestep-systems/bspecs
23
+ ```
24
+
25
+ This gives you the `bspecs` command for scaffolding projects. It does **not** put a `b6p` binary on
26
+ your global `PATH` — a dependency's bin is never globally reachable. Instead, every project you
27
+ scaffold declares `@bluestep-systems/b6p-cli` as a devDependency and the skills invoke it via
28
+ `npx b6p`. The scaffolder runs `npm install` in the new project for you (best-effort) to fetch `b6p`;
29
+ if it can't — e.g. you're offline — it prints the command to run by hand. That per-project install
30
+ resolves `@bluestep-systems/b6p-cli` anonymously from public npm, no token needed.
31
+
32
+ > **Not "zero setup."** Removing the npm token does **not** remove the one-time **BlueStep platform**
33
+ > credential step. Before the `/b6p-pull`, `/b6p-push`, or `/b6p-audit` skills work in a scaffolded
34
+ > project, run `npx b6p auth set` **once per machine** (credentials are stored globally in `~/.b6p`,
35
+ > not per project). This is unrelated to the npm registry and is still required — see the scaffolded
36
+ > project's own README.
37
+
38
+ > **Migrating from the old GitHub Packages install?** If you previously installed `bspecs` you likely
39
+ > have a line in `~/.npmrc` mapping the scope to GitHub Packages:
40
+ >
41
+ > ```ini
42
+ > @bluestep-systems:registry=https://npm.pkg.github.com
43
+ > ```
44
+ >
45
+ > Remove it (and the matching `//npm.pkg.github.com/:_authToken=...` line). Left in place it keeps
46
+ > routing `@bluestep-systems/*` to GitHub Packages and the public install will 404.
47
+
48
+ ## Usage
49
+
50
+ ### Scaffold a new project
51
+
52
+ From the parent directory where you want to create the project:
53
+
54
+ ```sh
55
+ bspecs
56
+ ```
57
+
58
+ The interactive wizard asks for the project name, client, and an optional description. When done, it generates the project directory with the full structure and (unless you opt out) runs `git init`.
59
+
60
+ ### Keep a project up to date
61
+
62
+ When a new version of `bspecs` is published with improvements to skills, hooks, or instructions, update your global install and sync the project:
63
+
64
+ ```sh
65
+ npm update -g @bluestep-systems/bspecs
66
+ cd my-project
67
+ bspecs sync
68
+ ```
69
+
70
+ `bspecs sync` compares each infrastructure file against the state it was in when scaffolded. Files you have not modified locally are updated; files you have edited are left untouched with a warning. If you believe your local changes would be useful across all BlueStep projects, open an issue in this repo so they can be incorporated into the scaffolder.
71
+
72
+ Projects scaffolded with `bspecs 0.5.0` or later run `bspecs sync` automatically every time Claude Code opens the workspace — no manual action needed.
73
+
74
+ ## Prerequisites
75
+
76
+ - **Node.js 18+**
77
+ - **`b6p` CLI** — required for the `/b6p-pull`, `/b6p-push`, and `/b6p-audit` skills. Scaffolded
78
+ projects declare it as a devDependency (`@bluestep-systems/b6p-cli`, resolved from public npm with
79
+ no token); the scaffolder runs `npm install` for you (re-run it by hand if that failed) and the
80
+ skills invoke it via `npx b6p` — no global install, no shell/PATH detection. Set your platform
81
+ credentials once per machine with `npx b6p auth set` (see Installation).
82
+ - **prettier** — required for the auto-format hook. `bspecs` warns if it is not found.
83
+
84
+ ## Generated structure
85
+
86
+ ```
87
+ my-project/
88
+ ├── CLAUDE.md ← project instructions for Claude
89
+ ├── README.md ← project documentation
90
+ ├── .prettierrc
91
+ ├── .gitignore
92
+ ├── package.json ← declares the b6p-cli devDependency
93
+ └── .claude/
94
+ ├── bspecs.lock ← lock file for bspecs sync
95
+ ├── settings.json ← Claude Code permissions and hooks
96
+ ├── hooks/ ← 3 scripts executed by Claude Code
97
+ ├── skills/ ← 8 skills (/spec-create, /b6p-pull, etc.)
98
+ ├── agents/ ← 3 BlueStep subagents (implementer, commenter, reviewer)
99
+ ├── instructions/ ← development rules for Claude
100
+ ├── spec-templates/ ← spec file templates
101
+ └── templates/ ← component scaffolding templates
102
+ ```
103
+
104
+ ## Proposing changes
105
+
106
+ ### Global changes (improvements for all projects)
107
+
108
+ If you find something that should be improved in the skills, hooks, instructions, or templates — something useful across all BlueStep projects — open an issue or PR in this repo. Once merged and published as a new version, `bspecs sync` propagates the change to all existing projects automatically.
109
+
110
+ ### Local changes (specific to your project)
111
+
112
+ If you need to adjust something only for your project (an extra permission in `settings.json`, a custom skill, changes to your `CLAUDE.md`), edit it directly in your repo. `bspecs sync` detects that those files were locally modified and leaves them untouched on future syncs.
113
+
114
+ ## Publishing
115
+
116
+ The package is published to the **public npm registry** under the `@bluestep-systems` organization
117
+ (`access: public`). Only `cli.js`, `src/`, and `templates/` are included in the published package.
118
+
119
+ Releases are automated by GitHub Actions — there is no manual `npm publish`:
120
+
121
+ 1. Bump `version` in `package.json` and commit.
122
+ 2. Tag the commit `vX.Y.Z` (matching the new version) and push the tag.
123
+ 3. `.github/workflows/publish.yml` fires on the tag, verifies the tag matches `package.json`, runs the
124
+ smoke checks, and publishes with `npm publish --provenance --access public`.
125
+
126
+ Publishing needs the `NPM_TOKEN` repo secret (an npm automation token with publish rights to
127
+ `@bluestep-systems`); the version guard fails the run early if the tag and `package.json` disagree.
128
+ `.github/workflows/ci.yml` runs the same smoke checks on every pull request and push to the default
129
+ branch.
package/cli.js ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ import { intro, outro, cancel, log } from '@clack/prompts';
3
+ import { readFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { runPrompts } from './src/prompts.js';
7
+ import { scaffold } from './src/scaffold.js';
8
+ import { sync } from './src/sync.js';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
12
+
13
+ const HELP = `bspecs — spec-driven BlueStep development with AI agents
14
+
15
+ Scaffold a new BlueStep project with Claude Code skills, hooks, and
16
+ project conventions for spec-driven development.
17
+
18
+ Usage:
19
+ bspecs Run the interactive scaffolder in the current directory.
20
+ bspecs sync Sync infrastructure files in the current project.
21
+ bspecs -v Print version.
22
+ bspecs -h Print this help.
23
+
24
+ Options for bspecs sync:
25
+ --silent Suppress all output (used by the SessionStart hook).
26
+ `;
27
+
28
+ function parseArgs(argv) {
29
+ const flags = new Set(argv.filter(a => a.startsWith('-')));
30
+ const positional = argv.filter(a => !a.startsWith('-'));
31
+ if (flags.has('-v') || flags.has('--version')) return { mode: 'version' };
32
+ if (flags.has('-h') || flags.has('--help')) return { mode: 'help' };
33
+ if (positional[0] === 'sync') return { mode: 'sync', silent: flags.has('--silent') };
34
+ return { mode: 'interactive' };
35
+ }
36
+
37
+ async function main() {
38
+ const { mode, silent } = parseArgs(process.argv.slice(2));
39
+
40
+ if (mode === 'version') {
41
+ console.log(pkg.version);
42
+ return;
43
+ }
44
+ if (mode === 'help') {
45
+ console.log(HELP);
46
+ return;
47
+ }
48
+ if (mode === 'sync') {
49
+ await sync({ silent });
50
+ return;
51
+ }
52
+
53
+ intro('bspecs — spec-driven BlueStep development with AI agents');
54
+
55
+ const answers = await runPrompts();
56
+ if (!answers) {
57
+ cancel('Cancelled.');
58
+ process.exit(0);
59
+ }
60
+
61
+ await scaffold(answers);
62
+
63
+ outro(
64
+ `Project created at ${answers.projectName}\n` +
65
+ ` Next steps:\n` +
66
+ ` cd ${answers.projectName}\n` +
67
+ ` wsl bash -lc 'b6p pull "<DAV URL>"' (creates the U-folder and component skeleton)`
68
+ );
69
+ }
70
+
71
+ main().catch((err) => {
72
+ log.error(err.message || String(err));
73
+ process.exit(1);
74
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@bluestep-systems/bspecs",
3
+ "version": "0.10.0",
4
+ "description": "Spec-driven BlueStep development with AI agents — scaffolder and project conventions for Claude Code",
5
+ "type": "module",
6
+ "bin": {
7
+ "bspecs": "./cli.js"
8
+ },
9
+ "files": [
10
+ "cli.js",
11
+ "src/",
12
+ "templates/"
13
+ ],
14
+ "scripts": {
15
+ "start": "node cli.js"
16
+ },
17
+ "dependencies": {
18
+ "@clack/prompts": "^0.9.0"
19
+ },
20
+ "engines": {
21
+ "node": ">=18.0.0"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/Bluestep-Systems/bspecs.git"
29
+ }
30
+ }
package/src/prompts.js ADDED
@@ -0,0 +1,74 @@
1
+ import { text, confirm, isCancel, cancel } from '@clack/prompts';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ function titleCase(s) {
6
+ return s
7
+ .replace(/[-_]+/g, ' ')
8
+ .split(' ')
9
+ .filter(Boolean)
10
+ .map((w) => w[0].toUpperCase() + w.slice(1))
11
+ .join(' ');
12
+ }
13
+
14
+ function bail(value) {
15
+ if (isCancel(value)) {
16
+ cancel('Cancelled.');
17
+ process.exit(0);
18
+ }
19
+ return value;
20
+ }
21
+
22
+ export async function runPrompts() {
23
+ const projectName = bail(
24
+ await text({
25
+ message: 'Project folder name',
26
+ placeholder: 'my-bluestep-project',
27
+ validate: (v) => {
28
+ if (!v || !v.trim()) return 'Project name is required';
29
+ if (/[<>:"/\\|?*]/.test(v)) return 'Invalid characters in name';
30
+ if (existsSync(join(process.cwd(), v))) return `Folder "${v}" already exists`;
31
+ return undefined;
32
+ },
33
+ })
34
+ );
35
+
36
+ const clientName = bail(
37
+ await text({
38
+ message: 'Client name',
39
+ initialValue: titleCase(projectName),
40
+ validate: (v) => (v && v.trim() ? undefined : 'Client name is required'),
41
+ })
42
+ );
43
+
44
+ const projectDescription = bail(
45
+ await text({
46
+ message: 'Project description (optional — gives Claude project context)',
47
+ placeholder: 'What does this project do? Press Enter to skip.',
48
+ })
49
+ );
50
+
51
+ const initGit = bail(
52
+ await confirm({
53
+ message:
54
+ 'Initialize a git repository? (skipping degrades the implementer agent, which relies on git diff)',
55
+ initialValue: true,
56
+ })
57
+ );
58
+
59
+ const proceed = bail(
60
+ await confirm({
61
+ message: `Create project "${projectName}" in ${process.cwd()}?`,
62
+ initialValue: true,
63
+ })
64
+ );
65
+
66
+ if (!proceed) return null;
67
+
68
+ return {
69
+ projectName,
70
+ clientName,
71
+ projectDescription: projectDescription || '',
72
+ initGit,
73
+ };
74
+ }
@@ -0,0 +1,152 @@
1
+ import { execSync } from 'child_process';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
5
+ import { log } from '@clack/prompts';
6
+ import { ensureDir, copyTemplateTree, applyTemplate, TEMPLATES_DIR, sha256 } from './utils.js';
7
+ import { SYNC_TARGETS } from './sync.js';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
11
+
12
+ function writeBspecsLock(projectDir, vars) {
13
+ const files = {};
14
+ for (const target of SYNC_TARGETS) {
15
+ const templatePath = join(TEMPLATES_DIR, target.templateSrc);
16
+ if (!existsSync(templatePath)) continue;
17
+ const rendered = applyTemplate(readFileSync(templatePath, 'utf8'), vars);
18
+ files[target.destRel] = sha256(rendered);
19
+ }
20
+
21
+ const lock = {
22
+ bspecs_version: pkg.version,
23
+ synced_at: new Date().toISOString().split('T')[0],
24
+ vars: {
25
+ PROJECT_NAME: vars.PROJECT_NAME,
26
+ CLIENT_NAME: vars.CLIENT_NAME,
27
+ PROJECT_DESCRIPTION: vars.PROJECT_DESCRIPTION,
28
+ SCAFFOLD_DATE: vars.SCAFFOLD_DATE,
29
+ },
30
+ files,
31
+ };
32
+
33
+ writeFileSync(join(projectDir, '.claude', 'bspecs.lock'), JSON.stringify(lock, null, 2) + '\n', 'utf8');
34
+ }
35
+
36
+ export async function scaffold(answers) {
37
+ const projectDir = join(process.cwd(), answers.projectName);
38
+ ensureDir(projectDir);
39
+
40
+ const vars = {
41
+ PROJECT_NAME: answers.projectName,
42
+ CLIENT_NAME: answers.clientName,
43
+ PROJECT_DESCRIPTION: answers.projectDescription,
44
+ SCAFFOLD_DATE: new Date().toISOString().split('T')[0],
45
+ };
46
+
47
+ copyTemplateTree('root', projectDir, vars);
48
+ copyTemplateTree('claude', join(projectDir, '.claude'), vars, { makeExecutable: true });
49
+ copyTemplateTree('module', join(projectDir, '.claude', 'templates'), vars);
50
+
51
+ writeBspecsLock(projectDir, vars);
52
+
53
+ log.success('Files generated.');
54
+
55
+ checkPrettierOnPath();
56
+ installDependencies(answers.projectName, projectDir);
57
+
58
+ if (answers.initGit) {
59
+ if (isInsideGitRepo(projectDir)) {
60
+ log.warn(
61
+ 'An existing git repository was found in a parent directory. Skipping git init to avoid nesting a repository inside another — initialize manually if that was intended.'
62
+ );
63
+ } else {
64
+ try {
65
+ execSync('git init', { cwd: projectDir, stdio: 'ignore' });
66
+ execSync('git add -A', { cwd: projectDir, stdio: 'ignore' });
67
+ execSync('git commit -m "chore: initial scaffold via bspecs"', {
68
+ cwd: projectDir,
69
+ stdio: 'ignore',
70
+ });
71
+ log.success('Git repository initialized.');
72
+ } catch (err) {
73
+ log.warn('Could not initialize git repository (git may not be installed).');
74
+ }
75
+ }
76
+ } else {
77
+ log.warn(
78
+ 'Skipped git init. The implementer agent relies on `git diff` to summarize its work — run `git init` in the project before using /spec-execute.'
79
+ );
80
+ }
81
+ }
82
+
83
+ // Detect whether the freshly created project directory sits inside an existing
84
+ // git repository (a parent has a .git). Running `git init` here would nest a
85
+ // repo inside another, which surprises users and breaks the implementer agent's
86
+ // `git diff`. Returns false if git is unavailable so the normal init path runs.
87
+ function isInsideGitRepo(dir) {
88
+ try {
89
+ const out = execSync('git rev-parse --is-inside-work-tree', {
90
+ cwd: dir,
91
+ stdio: ['ignore', 'pipe', 'ignore'],
92
+ });
93
+ return out.toString().trim() === 'true';
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ // Best-effort check that prettier is reachable, so we can warn that the
100
+ // prettier-on-save hook will be a no-op until it is installed. The hook runs
101
+ // inside WSL on Windows, so we probe WSL there in addition to the native PATH.
102
+ // Stderr is silenced because interactive shells often emit banners/warnings.
103
+ // Warning only — this never blocks the scaffold.
104
+ function checkPrettierOnPath() {
105
+ const probes = process.platform === 'win32'
106
+ ? ['where prettier', 'wsl bash -lc "command -v prettier"', 'wsl zsh -ic "command -v prettier"']
107
+ : ['command -v prettier'];
108
+ for (const cmd of probes) {
109
+ try {
110
+ execSync(cmd, { stdio: ['ignore', 'ignore', 'ignore'] });
111
+ return; // found
112
+ } catch {
113
+ // try the next probe
114
+ }
115
+ }
116
+ const installCmd = process.platform === 'linux' || process.platform === 'darwin'
117
+ ? 'npm i -g prettier'
118
+ : 'npm i -g prettier (in PowerShell) or wsl bash -lc "npm i -g prettier" (in WSL)';
119
+ log.warn(`prettier not found in either the native or WSL PATH. The prettier-on-save hook will be a no-op until you run: ${installCmd}`);
120
+ }
121
+
122
+ // Install the project's dependencies on a best-effort basis. We attempt
123
+ // `npm install` so the b6p CLI (a devDependency) is present without a manual
124
+ // step, but it can legitimately fail and we must NOT assume it will succeed:
125
+ // @bluestep-systems/b6p-cli installs anonymously from the public npm registry
126
+ // (no token, no ~/.npmrc), so the realistic failure mode is the machine being
127
+ // offline at scaffold time. On any failure we fall back to printing the manual
128
+ // install reminder rather than failing the scaffold or leaving the project
129
+ // half-installed. The skills invoke `npx b6p`, so `node_modules/.bin/b6p` must
130
+ // exist before the first b6p skill runs — hence the reminder on failure.
131
+ function installDependencies(projectName, projectDir) {
132
+ log.info(`Installing dependencies in ${projectName} (npm install)…`);
133
+ try {
134
+ execSync('npm install', { cwd: projectDir, stdio: 'ignore' });
135
+ log.success('Dependencies installed — b6p is ready via `npx b6p`.');
136
+ } catch {
137
+ log.warn(
138
+ [
139
+ 'Could not run `npm install` automatically. Install dependencies by hand',
140
+ 'before using the b6p skills:',
141
+ '',
142
+ ` cd ${projectName}`,
143
+ ' npm install',
144
+ '',
145
+ 'This fetches @bluestep-systems/b6p-cli (a devDependency) so the /b6p-pull,',
146
+ '/b6p-push, and /b6p-audit skills can run `npx b6p …`. It installs from the',
147
+ 'public npm registry with no token or ~/.npmrc setup — the most common cause',
148
+ 'of this failure is simply being offline.',
149
+ ].join('\n')
150
+ );
151
+ }
152
+ }
package/src/sync.js ADDED
@@ -0,0 +1,123 @@
1
+ import { readFileSync, writeFileSync, existsSync, chmodSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { log } from '@clack/prompts';
5
+ import { TEMPLATES_DIR, applyTemplate, writeFile, sha256, enumerateClaudeTargets } from './utils.js';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
9
+
10
+ // Files the scaffolder writes once but sync must NOT manage afterwards.
11
+ // Empty today; add a templateSrc path (forward-slashed, e.g.
12
+ // 'claude/skills/foo/SKILL.md') to opt a future scaffold-once file out of sync.
13
+ const SYNC_EXCLUDE = [];
14
+
15
+ // Every file under templates/claude/** is synced infrastructure (skills, hooks,
16
+ // settings, spec-templates, instructions). Derived by walking the tree rather
17
+ // than a hardcoded list, so new files flow into `bspecs sync` and bspecs.lock
18
+ // automatically — no hand-maintained array to drift. templates/root/ (user-owned
19
+ // CLAUDE.md/README) and templates/module/ (scaffold-once) live outside this tree
20
+ // and are excluded by construction. Claude-only: no .github mirror.
21
+ export const SYNC_TARGETS = enumerateClaudeTargets(SYNC_EXCLUDE);
22
+
23
+ function findProjectRoot(startDir) {
24
+ let dir = startDir;
25
+ while (true) {
26
+ if (existsSync(join(dir, '.claude', 'bspecs.lock'))) return dir;
27
+ const parent = dirname(dir);
28
+ if (parent === dir) return null;
29
+ dir = parent;
30
+ }
31
+ }
32
+
33
+ function readLock(projectRoot) {
34
+ return JSON.parse(readFileSync(join(projectRoot, '.claude', 'bspecs.lock'), 'utf8'));
35
+ }
36
+
37
+ function writeLock(projectRoot, obj) {
38
+ writeFileSync(join(projectRoot, '.claude', 'bspecs.lock'), JSON.stringify(obj, null, 2) + '\n', 'utf8');
39
+ }
40
+
41
+ function syncFiles(projectRoot, lock, silent) {
42
+ const vars = lock.vars || {};
43
+ const newFiles = {};
44
+ let updated = 0;
45
+ let skipped = 0;
46
+
47
+ for (const target of SYNC_TARGETS) {
48
+ const templatePath = join(TEMPLATES_DIR, target.templateSrc);
49
+ if (!existsSync(templatePath)) continue;
50
+
51
+ const newContent = applyTemplate(readFileSync(templatePath, 'utf8'), vars);
52
+ const newHash = sha256(newContent);
53
+ const destAbs = join(projectRoot, target.destRel);
54
+ const lockHash = lock.files[target.destRel];
55
+
56
+ if (!existsSync(destAbs)) {
57
+ writeFile(destAbs, newContent);
58
+ if (destAbs.endsWith('.sh')) {
59
+ try { chmodSync(destAbs, 0o755); } catch { /* Windows */ }
60
+ }
61
+ newFiles[target.destRel] = newHash;
62
+ updated++;
63
+ if (!silent) log.info(` added ${target.destRel}`);
64
+ continue;
65
+ }
66
+
67
+ const diskHash = sha256(readFileSync(destAbs, 'utf8'));
68
+
69
+ if (lockHash !== undefined && diskHash !== lockHash) {
70
+ // Usuario editó este archivo localmente — no tocar
71
+ newFiles[target.destRel] = lockHash;
72
+ skipped++;
73
+ if (!silent) log.warn(` skipped ${target.destRel} (locally modified)`);
74
+ } else {
75
+ writeFile(destAbs, newContent);
76
+ if (destAbs.endsWith('.sh')) {
77
+ try { chmodSync(destAbs, 0o755); } catch { /* Windows */ }
78
+ }
79
+ newFiles[target.destRel] = newHash;
80
+ if (diskHash !== newHash) {
81
+ updated++;
82
+ if (!silent) log.info(` updated ${target.destRel}`);
83
+ }
84
+ }
85
+ }
86
+
87
+ return { updated, skipped, newFiles };
88
+ }
89
+
90
+ export async function sync({ silent = false } = {}) {
91
+ try {
92
+ const projectRoot = findProjectRoot(process.cwd());
93
+
94
+ if (!projectRoot) {
95
+ if (!silent) {
96
+ console.error(
97
+ 'No bspecs.lock found. Run bspecs sync from a project scaffolded with bspecs 0.5.0 or later.\n' +
98
+ 'If this project was scaffolded earlier, re-run bspecs in the parent directory to re-scaffold.'
99
+ );
100
+ }
101
+ return;
102
+ }
103
+
104
+ const lock = readLock(projectRoot);
105
+ if (!silent) log.info(`Syncing infrastructure files in: ${projectRoot}`);
106
+
107
+ const { updated, skipped, newFiles } = syncFiles(projectRoot, lock, silent);
108
+
109
+ writeLock(projectRoot, {
110
+ bspecs_version: pkg.version,
111
+ synced_at: new Date().toISOString().split('T')[0],
112
+ vars: lock.vars || {},
113
+ files: newFiles,
114
+ });
115
+
116
+ if (!silent) {
117
+ log.success(`Sync complete. Updated ${updated} file${updated !== 1 ? 's' : ''}, skipped ${skipped} (locally modified).`);
118
+ }
119
+ } catch (err) {
120
+ if (!silent) throw err;
121
+ // En modo --silent (hook SessionStart): nunca bloquear el startup de Claude Code
122
+ }
123
+ }
package/src/utils.js ADDED
@@ -0,0 +1,95 @@
1
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, readdirSync, statSync } from 'fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ export const TEMPLATES_DIR = join(__dirname, '..', 'templates');
8
+
9
+ export function applyTemplate(str, vars) {
10
+ return str.replace(/\{\{(\w+)\}\}/g, (match, key) =>
11
+ Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match
12
+ );
13
+ }
14
+
15
+ export function ensureDir(dir) {
16
+ mkdirSync(dir, { recursive: true });
17
+ }
18
+
19
+ export function writeFile(path, content) {
20
+ ensureDir(dirname(path));
21
+ writeFileSync(path, content, 'utf8');
22
+ }
23
+
24
+ export function readTemplate(relativePath) {
25
+ return readFileSync(join(TEMPLATES_DIR, relativePath), 'utf8');
26
+ }
27
+
28
+ export function copyTemplateTree(srcRel, destAbs, vars, opts = {}) {
29
+ const { makeExecutable = false, stripTemplateExt = true } = opts;
30
+ const srcAbs = join(TEMPLATES_DIR, srcRel);
31
+ if (!existsSync(srcAbs)) return;
32
+ walk(srcAbs, srcAbs, destAbs, vars, { makeExecutable, stripTemplateExt });
33
+ }
34
+
35
+ function walk(rootSrc, src, dest, vars, opts) {
36
+ for (const entry of readdirSync(src)) {
37
+ const srcEntry = join(src, entry);
38
+ const stats = statSync(srcEntry);
39
+ if (stats.isDirectory()) {
40
+ walk(rootSrc, srcEntry, join(dest, entry), vars, opts);
41
+ } else {
42
+ const targetName = opts.stripTemplateExt && entry.endsWith('.template')
43
+ ? entry.slice(0, -'.template'.length)
44
+ : entry;
45
+ const destFile = join(dest, targetName);
46
+ const raw = readFileSync(srcEntry, 'utf8');
47
+ const rendered = applyTemplate(raw, vars);
48
+ writeFile(destFile, rendered);
49
+ if (opts.makeExecutable && destFile.endsWith('.sh')) {
50
+ try { chmodSync(destFile, 0o755); } catch { /* Windows can't chmod, ignore */ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ // Walk templates/claude/** and return one sync target per file:
57
+ // { templateSrc, destRel }
58
+ // templateSrc is relative to TEMPLATES_DIR (forward-slashed, e.g.
59
+ // 'claude/instructions/reference/foo.md.template'); destRel maps it into the
60
+ // scaffolded project ('.claude/instructions/reference/foo.md') — leading
61
+ // 'claude/' becomes '.claude/', a trailing '.template' is stripped, subfolders
62
+ // preserved. This is the same transform copyTemplateTree applies, so it
63
+ // reproduces the formerly-hardcoded skills/hooks/settings/spec-template entries
64
+ // exactly and picks up the instructions tree automatically. Claude-only: no
65
+ // .github mirror target is emitted. `exclude` lists templateSrc paths to skip —
66
+ // the escape hatch for any future scaffold-once file under claude/.
67
+ export function enumerateClaudeTargets(exclude = []) {
68
+ const root = join(TEMPLATES_DIR, 'claude');
69
+ if (!existsSync(root)) return [];
70
+ const skip = new Set(exclude);
71
+ const targets = [];
72
+ walkClaude(root, 'claude', skip, targets);
73
+ return targets;
74
+ }
75
+
76
+ function walkClaude(absDir, relDir, skip, targets) {
77
+ for (const entry of readdirSync(absDir).sort()) {
78
+ const abs = join(absDir, entry);
79
+ const rel = `${relDir}/${entry}`;
80
+ if (statSync(abs).isDirectory()) {
81
+ walkClaude(abs, rel, skip, targets);
82
+ } else if (!skip.has(rel)) {
83
+ const destRel = '.claude/' + rel.slice('claude/'.length).replace(/\.template$/, '');
84
+ targets.push({ templateSrc: rel, destRel });
85
+ }
86
+ }
87
+ }
88
+
89
+ export function exists(path) {
90
+ return existsSync(path);
91
+ }
92
+
93
+ export function sha256(str) {
94
+ return createHash('sha256').update(str, 'utf8').digest('hex');
95
+ }