the-frame-ai 0.1.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/LICENSE +21 -0
- package/README.md +335 -0
- package/README.ru.md +333 -0
- package/bin/the-frame +5 -0
- package/bin/the-frame-ai +5 -0
- package/package.json +29 -0
- package/src/cli.js +84 -0
- package/src/doctor.js +164 -0
- package/src/init.js +178 -0
- package/src/languages.js +141 -0
- package/src/manifest.js +55 -0
- package/src/update.js +87 -0
- package/src/utils.js +55 -0
- package/templates/agents/builder.md +240 -0
- package/templates/agents/devils-advocate.md +136 -0
- package/templates/agents/planner.md +277 -0
- package/templates/agents/researcher.md +195 -0
- package/templates/agents/reviewer.md +300 -0
- package/templates/commands/frame:add-task.md +57 -0
- package/templates/commands/frame:build.md +170 -0
- package/templates/commands/frame:check-deps.md +118 -0
- package/templates/commands/frame:checkpoint.md +158 -0
- package/templates/commands/frame:cleanup-memory.md +80 -0
- package/templates/commands/frame:context.md +64 -0
- package/templates/commands/frame:daily.md +77 -0
- package/templates/commands/frame:debug.md +146 -0
- package/templates/commands/frame:doctor.md +170 -0
- package/templates/commands/frame:estimate.md +105 -0
- package/templates/commands/frame:explain.md +84 -0
- package/templates/commands/frame:fast.md +89 -0
- package/templates/commands/frame:forensics.md +139 -0
- package/templates/commands/frame:headless.md +118 -0
- package/templates/commands/frame:health.md +86 -0
- package/templates/commands/frame:init.md +231 -0
- package/templates/commands/frame:migrate.md +107 -0
- package/templates/commands/frame:note.md +32 -0
- package/templates/commands/frame:pause.md +145 -0
- package/templates/commands/frame:performance.md +228 -0
- package/templates/commands/frame:plan.md +198 -0
- package/templates/commands/frame:refactor.md +161 -0
- package/templates/commands/frame:research.md +131 -0
- package/templates/commands/frame:resume.md +137 -0
- package/templates/commands/frame:retrospective.md +196 -0
- package/templates/commands/frame:review.md +174 -0
- package/templates/commands/frame:rollback.md +207 -0
- package/templates/commands/frame:ship.md +148 -0
- package/templates/commands/frame:sprint-check.md +111 -0
- package/templates/commands/frame:status.md +103 -0
- package/templates/commands/frame:unstuck.md +102 -0
- package/templates/commands/frame:wave.md +312 -0
- package/templates/commands/frame:where.md +5 -0
- package/templates/commands/frame:why.md +57 -0
- package/templates/commands/frame:worktree.md +219 -0
- package/templates/hooks/git-safety.sh +33 -0
- package/templates/hooks/quality-gate.sh +52 -0
- package/templates/hooks/safety-net.sh +13 -0
- package/templates/hooks/session-init.sh +81 -0
- package/templates/planning/pause-state.json +1 -0
- package/templates/project/CLAUDE.md +63 -0
- package/templates/project/CONTEXT.md +16 -0
- package/templates/project/MAP.md +35 -0
- package/templates/project/ROADMAP.md +12 -0
- package/templates/project/STATE.md +13 -0
- package/templates/project/config.json +74 -0
- package/templates/project/memory/anti-patterns.md +14 -0
- package/templates/project/memory/context.md +23 -0
- package/templates/project/memory/conventions.md +19 -0
- package/templates/project/memory/decisions.md +20 -0
- package/templates/project/memory/dependencies.md +23 -0
- package/templates/project/memory/metrics.md +22 -0
- package/templates/project/memory/patterns.md +30 -0
- package/templates/project/memory/wins.md +11 -0
- package/templates/project/settings.local.json +50 -0
- package/templates/project/specs/_template/PRD.md +24 -0
- package/templates/project/specs/_template/plan.md +25 -0
- package/templates/project/specs/_template/spec.md +27 -0
- package/templates/project/specs/_template/subagent-prompt.md +43 -0
package/bin/the-frame
ADDED
package/bin/the-frame-ai
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "the-frame-ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "FRAME — Framework for AI-Assisted Solo Development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"the-frame-ai": "./bin/the-frame-ai"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"templates/"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude",
|
|
16
|
+
"ai",
|
|
17
|
+
"development",
|
|
18
|
+
"framework",
|
|
19
|
+
"solo",
|
|
20
|
+
"tdd"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test test/*.test.js"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { VERSION, log } from './manifest.js';
|
|
2
|
+
import { resolveTarget } from './manifest.js';
|
|
3
|
+
import { init } from './init.js';
|
|
4
|
+
import { update } from './update.js';
|
|
5
|
+
import { doctor } from './doctor.js';
|
|
6
|
+
|
|
7
|
+
const HELP = `
|
|
8
|
+
FRAME — Framework for AI-Assisted Solo Development v${VERSION}
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
the-frame init [target-dir] Install FRAME into a project
|
|
12
|
+
the-frame update [target-dir] Update FRAME files in a project
|
|
13
|
+
the-frame doctor [target-dir] Check FRAME installation health
|
|
14
|
+
the-frame version Show CLI version
|
|
15
|
+
the-frame help Show this help message
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--lang <code> Set response language (e.g. en, ru, zh). Overrides FRAME_LANG env var.
|
|
19
|
+
--dry-run (update only) Show what would be updated without making changes.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
npx the-frame init Install in current directory
|
|
23
|
+
npx the-frame init ../my-app Install in specific directory
|
|
24
|
+
npx the-frame init --lang ru Install with Russian language preset
|
|
25
|
+
npx the-frame update Update in current directory
|
|
26
|
+
npx the-frame update --dry-run Preview update without applying
|
|
27
|
+
npx the-frame doctor Check health in current directory
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
function parseFlags(args) {
|
|
31
|
+
const flags = { lang: null, dryRun: false, yes: false };
|
|
32
|
+
const rest = [];
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
if (args[i] === '--lang' && args[i + 1]) {
|
|
35
|
+
flags.lang = args[++i];
|
|
36
|
+
} else if (args[i] === '--dry-run') {
|
|
37
|
+
flags.dryRun = true;
|
|
38
|
+
} else if (args[i] === '--yes' || args[i] === '-y') {
|
|
39
|
+
flags.yes = true;
|
|
40
|
+
} else {
|
|
41
|
+
rest.push(args[i]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { flags, rest };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function run(args) {
|
|
48
|
+
const [command, ...rest] = args;
|
|
49
|
+
|
|
50
|
+
switch (command) {
|
|
51
|
+
case 'init': {
|
|
52
|
+
const { flags, rest: r } = parseFlags(rest);
|
|
53
|
+
const target = resolveTarget(r);
|
|
54
|
+
await init(target, flags);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case 'update': {
|
|
58
|
+
const { flags, rest: r } = parseFlags(rest);
|
|
59
|
+
const target = resolveTarget(r);
|
|
60
|
+
await update(target, flags);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'doctor': {
|
|
64
|
+
const target = resolveTarget(rest);
|
|
65
|
+
await doctor(target);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'version':
|
|
69
|
+
case '--version':
|
|
70
|
+
case '-v':
|
|
71
|
+
log(`the-frame v${VERSION}`);
|
|
72
|
+
break;
|
|
73
|
+
case 'help':
|
|
74
|
+
case '--help':
|
|
75
|
+
case '-h':
|
|
76
|
+
case undefined:
|
|
77
|
+
log(HELP);
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
log(`Unknown command: ${command}`);
|
|
81
|
+
log(HELP);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFileSync, statSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { TEMPLATES_DIR, VERSION, log, logSuccess, logWarn, logError } from './manifest.js';
|
|
4
|
+
import { fileExists } from './utils.js';
|
|
5
|
+
|
|
6
|
+
const REQUIRED_DIRS = [
|
|
7
|
+
'.claude/commands',
|
|
8
|
+
'.claude/agents',
|
|
9
|
+
'.claude/hooks',
|
|
10
|
+
'.planning',
|
|
11
|
+
'.planning/memory',
|
|
12
|
+
'.frame',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const REQUIRED_FILES = [
|
|
16
|
+
'CLAUDE.md',
|
|
17
|
+
'.frame/config.json',
|
|
18
|
+
'.frame/.frame-version',
|
|
19
|
+
'.claude/settings.local.json',
|
|
20
|
+
'.planning/STATE.md',
|
|
21
|
+
'.planning/MAP.md',
|
|
22
|
+
'.planning/pause-state.json',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const HOOK_FILES = [
|
|
26
|
+
'.claude/hooks/safety-net.sh',
|
|
27
|
+
'.claude/hooks/git-safety.sh',
|
|
28
|
+
'.claude/hooks/quality-gate.sh',
|
|
29
|
+
'.claude/hooks/session-init.sh',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export async function doctor(target) {
|
|
33
|
+
log('\nFRAME Doctor — Checking installation health...\n');
|
|
34
|
+
|
|
35
|
+
let errors = 0;
|
|
36
|
+
let warnings = 0;
|
|
37
|
+
|
|
38
|
+
// Node version
|
|
39
|
+
log('Node.js:');
|
|
40
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
41
|
+
if (major >= 18) {
|
|
42
|
+
logSuccess(` v${process.versions.node}`);
|
|
43
|
+
} else {
|
|
44
|
+
logError(` v${process.versions.node} — requires >=18`);
|
|
45
|
+
errors++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Directories
|
|
49
|
+
log('\nDirectories:');
|
|
50
|
+
for (const dir of REQUIRED_DIRS) {
|
|
51
|
+
if (fileExists(join(target, dir))) {
|
|
52
|
+
logSuccess(` ${dir}/`);
|
|
53
|
+
} else {
|
|
54
|
+
logError(` ${dir}/ — MISSING`);
|
|
55
|
+
errors++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Files
|
|
60
|
+
log('\nFiles:');
|
|
61
|
+
for (const file of REQUIRED_FILES) {
|
|
62
|
+
if (fileExists(join(target, file))) {
|
|
63
|
+
logSuccess(` ${file}`);
|
|
64
|
+
} else {
|
|
65
|
+
logError(` ${file} — MISSING`);
|
|
66
|
+
errors++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// config.json validation
|
|
71
|
+
const configPath = join(target, '.frame', 'config.json');
|
|
72
|
+
if (fileExists(configPath)) {
|
|
73
|
+
try {
|
|
74
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
75
|
+
if (config.language) {
|
|
76
|
+
logSuccess(` .frame/config.json — valid (language: ${config.language})`);
|
|
77
|
+
} else {
|
|
78
|
+
logWarn(' .frame/config.json — missing "language" field');
|
|
79
|
+
warnings++;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
logError(' .frame/config.json — invalid JSON');
|
|
83
|
+
errors++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Hooks executability
|
|
88
|
+
log('\nHooks:');
|
|
89
|
+
for (const hook of HOOK_FILES) {
|
|
90
|
+
const fullPath = join(target, hook);
|
|
91
|
+
if (!fileExists(fullPath)) {
|
|
92
|
+
logError(` ${hook} — MISSING`);
|
|
93
|
+
errors++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const isExec = (statSync(fullPath).mode & 0o111) !== 0;
|
|
98
|
+
if (isExec) {
|
|
99
|
+
logSuccess(` ${hook} — executable`);
|
|
100
|
+
} else {
|
|
101
|
+
logWarn(` ${hook} — not executable`);
|
|
102
|
+
warnings++;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
logWarn(` ${hook} — cannot check permissions`);
|
|
106
|
+
warnings++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Version
|
|
111
|
+
log('\nVersion:');
|
|
112
|
+
const installedVersion = fileExists(join(target, '.frame', '.frame-version'))
|
|
113
|
+
? readFileSync(join(target, '.frame', '.frame-version'), 'utf-8').trim()
|
|
114
|
+
: 'unknown';
|
|
115
|
+
|
|
116
|
+
if (installedVersion === VERSION) {
|
|
117
|
+
logSuccess(` Installed: ${installedVersion} (latest)`);
|
|
118
|
+
} else {
|
|
119
|
+
logWarn(` Installed: ${installedVersion}, CLI: ${VERSION} — run \`the-frame update\``);
|
|
120
|
+
warnings++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Counts
|
|
124
|
+
log('\nComponents:');
|
|
125
|
+
const commandsDir = join(target, '.claude', 'commands');
|
|
126
|
+
if (fileExists(commandsDir)) {
|
|
127
|
+
const count = readdirSync(commandsDir).filter((f) => f.endsWith('.md')).length;
|
|
128
|
+
const templateCommandsDir = join(TEMPLATES_DIR, 'commands');
|
|
129
|
+
const expectedCount = fileExists(templateCommandsDir)
|
|
130
|
+
? readdirSync(templateCommandsDir).filter((f) => f.endsWith('.md')).length
|
|
131
|
+
: count;
|
|
132
|
+
if (count >= expectedCount) {
|
|
133
|
+
logSuccess(` Commands: ${count}/${expectedCount}`);
|
|
134
|
+
} else {
|
|
135
|
+
logWarn(` Commands: ${count}/${expectedCount}`);
|
|
136
|
+
warnings++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const agentsDir = join(target, '.claude', 'agents');
|
|
141
|
+
if (fileExists(agentsDir)) {
|
|
142
|
+
const count = readdirSync(agentsDir).filter((f) => f.endsWith('.md')).length;
|
|
143
|
+
const templateAgentsDir = join(TEMPLATES_DIR, 'agents');
|
|
144
|
+
const expectedAgents = fileExists(templateAgentsDir)
|
|
145
|
+
? readdirSync(templateAgentsDir).filter((f) => f.endsWith('.md')).length
|
|
146
|
+
: count;
|
|
147
|
+
if (count >= expectedAgents) {
|
|
148
|
+
logSuccess(` Agents: ${count}/${expectedAgents}`);
|
|
149
|
+
} else {
|
|
150
|
+
logWarn(` Agents: ${count}/${expectedAgents}`);
|
|
151
|
+
warnings++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Summary
|
|
156
|
+
log('\n' + '─'.repeat(50));
|
|
157
|
+
if (errors === 0 && warnings === 0) {
|
|
158
|
+
logSuccess('All checks passed! FRAME is healthy.');
|
|
159
|
+
} else {
|
|
160
|
+
if (errors > 0) logError(`${errors} error(s) found`);
|
|
161
|
+
if (warnings > 0) logWarn(`${warnings} warning(s) found`);
|
|
162
|
+
}
|
|
163
|
+
log('');
|
|
164
|
+
}
|
package/src/init.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { join, basename } from 'node:path';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { TEMPLATES_DIR, VERSION, log, logSuccess, logWarn, logError, detectProjectName } from './manifest.js';
|
|
4
|
+
import {
|
|
5
|
+
ensureDir,
|
|
6
|
+
copyDir,
|
|
7
|
+
makeExecutable,
|
|
8
|
+
fileExists,
|
|
9
|
+
listFilesRecursive,
|
|
10
|
+
writeFile,
|
|
11
|
+
applyVars,
|
|
12
|
+
} from './utils.js';
|
|
13
|
+
import { LANGUAGES, getLanguageInstruction, promptLanguage, promptConfig } from './languages.js';
|
|
14
|
+
import { doctor } from './doctor.js';
|
|
15
|
+
|
|
16
|
+
const PLANNING_DIRS = [
|
|
17
|
+
'.planning/memory',
|
|
18
|
+
'.planning/pause-history',
|
|
19
|
+
'.planning/reports/daily',
|
|
20
|
+
'.planning/reports/deps',
|
|
21
|
+
'.planning/reports/quality',
|
|
22
|
+
'.planning/reports/sprint',
|
|
23
|
+
'.planning/reports/cleanup',
|
|
24
|
+
'.planning/reviews',
|
|
25
|
+
'.planning/specs/archive',
|
|
26
|
+
'.planning/forensics',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const CLAUDE_DIRS = [
|
|
30
|
+
'.claude/commands',
|
|
31
|
+
'.claude/agents',
|
|
32
|
+
'.claude/hooks',
|
|
33
|
+
'.claude/skills',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Files in templates/project/ that should be mapped to root-level destinations
|
|
37
|
+
const ROOT_FILE_MAP = {
|
|
38
|
+
'CLAUDE.md': 'CLAUDE.md',
|
|
39
|
+
'settings.local.json': '.claude/settings.local.json',
|
|
40
|
+
'config.json': '.frame/config.json',
|
|
41
|
+
'STATE.md': '.planning/STATE.md',
|
|
42
|
+
'MAP.md': '.planning/MAP.md',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const SKIP_PROJECT_FILES = new Set(['CONTEXT.md']);
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export async function init(target, flags = {}) {
|
|
49
|
+
if (fileExists(join(target, '.frame', 'config.json'))) {
|
|
50
|
+
logWarn('FRAME already installed in this project.');
|
|
51
|
+
log('Use `the-frame update` to update framework files.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const projectName = detectProjectName(target);
|
|
56
|
+
log(`\nFRAME v${VERSION} — Initializing in ${basename(target)}...\n`);
|
|
57
|
+
|
|
58
|
+
const language = await promptLanguage(flags.lang, flags.yes);
|
|
59
|
+
const langLabel = LANGUAGES.find((l) => l.code === language)?.name || language;
|
|
60
|
+
logSuccess(`Language: ${langLabel}\n`);
|
|
61
|
+
|
|
62
|
+
// 1. Create directories
|
|
63
|
+
log('Creating directories...');
|
|
64
|
+
for (const dir of [...CLAUDE_DIRS, ...PLANNING_DIRS]) {
|
|
65
|
+
ensureDir(join(target, dir));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const defaultConfig = JSON.parse(readFileSync(join(TEMPLATES_DIR, 'project', 'config.json'), 'utf-8'));
|
|
69
|
+
const resolvedConfig = await promptConfig(defaultConfig, flags.yes);
|
|
70
|
+
const qualityVars = Object.fromEntries(
|
|
71
|
+
Object.entries(resolvedConfig.quality.commands).map(([k, v]) => [`quality.commands.${k}`, v])
|
|
72
|
+
);
|
|
73
|
+
const vars = { PROJECT_NAME: projectName, LANGUAGE: language };
|
|
74
|
+
|
|
75
|
+
// 2. Copy commands and apply quality.commands substitution
|
|
76
|
+
const commandsSrc = join(TEMPLATES_DIR, 'commands');
|
|
77
|
+
const commandsDest = join(target, '.claude', 'commands');
|
|
78
|
+
copyDir(commandsSrc, commandsDest);
|
|
79
|
+
for (const f of readdirSync(commandsDest).filter((f) => f.endsWith('.md'))) {
|
|
80
|
+
const p = join(commandsDest, f);
|
|
81
|
+
const replaced = applyVars(readFileSync(p, 'utf-8'), vars, qualityVars);
|
|
82
|
+
writeFile(p, replaced);
|
|
83
|
+
}
|
|
84
|
+
const commandCount = readdirSync(commandsSrc).filter((f) => f.endsWith('.md')).length;
|
|
85
|
+
logSuccess(`${commandCount} commands → .claude/commands/`);
|
|
86
|
+
|
|
87
|
+
// 3. Copy agents and apply quality.commands substitution
|
|
88
|
+
const agentsSrc = join(TEMPLATES_DIR, 'agents');
|
|
89
|
+
const agentsDest = join(target, '.claude', 'agents');
|
|
90
|
+
copyDir(agentsSrc, agentsDest);
|
|
91
|
+
for (const f of readdirSync(agentsDest).filter((f) => f.endsWith('.md'))) {
|
|
92
|
+
const p = join(agentsDest, f);
|
|
93
|
+
const replaced = applyVars(readFileSync(p, 'utf-8'), vars, qualityVars);
|
|
94
|
+
writeFile(p, replaced);
|
|
95
|
+
}
|
|
96
|
+
const agentCount = readdirSync(agentsSrc).filter((f) => f.endsWith('.md')).length;
|
|
97
|
+
logSuccess(`${agentCount} agents → .claude/agents/`);
|
|
98
|
+
|
|
99
|
+
// 4. Copy hooks
|
|
100
|
+
const hooksSrc = join(TEMPLATES_DIR, 'hooks');
|
|
101
|
+
const hooksDest = join(target, '.claude', 'hooks');
|
|
102
|
+
copyDir(hooksSrc, hooksDest);
|
|
103
|
+
const hookFiles = readdirSync(hooksSrc);
|
|
104
|
+
for (const hook of hookFiles) {
|
|
105
|
+
makeExecutable(join(hooksDest, hook));
|
|
106
|
+
}
|
|
107
|
+
logSuccess(`${hookFiles.length} hooks → .claude/hooks/`);
|
|
108
|
+
|
|
109
|
+
// 5. Copy planning templates
|
|
110
|
+
const pauseStateSrc = join(TEMPLATES_DIR, 'planning', 'pause-state.json');
|
|
111
|
+
if (fileExists(pauseStateSrc)) {
|
|
112
|
+
writeFile(join(target, '.planning', 'pause-state.json'), readFileSync(pauseStateSrc, 'utf-8'));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 6. Generate project-specific files
|
|
116
|
+
const projectSrc = join(TEMPLATES_DIR, 'project');
|
|
117
|
+
const projectFiles = listFilesRecursive(projectSrc);
|
|
118
|
+
|
|
119
|
+
let fileCount = 0;
|
|
120
|
+
for (const srcPath of projectFiles) {
|
|
121
|
+
const relPath = srcPath.replace(projectSrc + '/', '');
|
|
122
|
+
|
|
123
|
+
if (SKIP_PROJECT_FILES.has(relPath)) continue;
|
|
124
|
+
|
|
125
|
+
// Determine destination
|
|
126
|
+
let destPath;
|
|
127
|
+
if (ROOT_FILE_MAP[relPath]) {
|
|
128
|
+
destPath = join(target, ROOT_FILE_MAP[relPath]);
|
|
129
|
+
} else {
|
|
130
|
+
destPath = join(target, relPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ensureDir(join(destPath, '..'));
|
|
134
|
+
const content = readFileSync(srcPath, 'utf-8');
|
|
135
|
+
const replaced = applyVars(content, vars, qualityVars);
|
|
136
|
+
writeFile(destPath, replaced);
|
|
137
|
+
fileCount++;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 7. Write version tracking
|
|
141
|
+
writeFile(join(target, '.frame', '.frame-version'), VERSION);
|
|
142
|
+
logSuccess(`${fileCount} project files generated`);
|
|
143
|
+
logSuccess(`Version ${VERSION} recorded`);
|
|
144
|
+
logSuccess(`Language: ${langLabel}`);
|
|
145
|
+
|
|
146
|
+
// 8. Save resolved config (with user's stack choices + language)
|
|
147
|
+
const configPath = join(target, '.frame', 'config.json');
|
|
148
|
+
if (fileExists(configPath)) {
|
|
149
|
+
resolvedConfig.language = language;
|
|
150
|
+
writeFile(configPath, JSON.stringify(resolvedConfig, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 9. Inject language instruction into CLAUDE.md
|
|
154
|
+
const claudeMdPath = join(target, 'CLAUDE.md');
|
|
155
|
+
if (fileExists(claudeMdPath)) {
|
|
156
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
157
|
+
writeFile(claudeMdPath, content + getLanguageInstruction(language));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 10. Success
|
|
161
|
+
log('\n' + '═'.repeat(60));
|
|
162
|
+
log(' FRAME initialized successfully!');
|
|
163
|
+
log('═'.repeat(60));
|
|
164
|
+
log('');
|
|
165
|
+
log(` Commands: ${commandCount} in .claude/commands/`);
|
|
166
|
+
log(` Agents: ${agentCount} in .claude/agents/`);
|
|
167
|
+
log(` Hooks: ${hookFiles.length} in .claude/hooks/`);
|
|
168
|
+
log(` Planning: files in .planning/`);
|
|
169
|
+
log(` Config: .frame/config.json`);
|
|
170
|
+
log('');
|
|
171
|
+
|
|
172
|
+
// 11. Auto-run doctor
|
|
173
|
+
log('\n--- Проверка установки ---');
|
|
174
|
+
await doctor(target);
|
|
175
|
+
|
|
176
|
+
log(' Next step: open Claude Code and run `/frame:daily`');
|
|
177
|
+
log('');
|
|
178
|
+
}
|
package/src/languages.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
|
|
3
|
+
function ask(rl, question) {
|
|
4
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const LANGUAGES = [
|
|
8
|
+
{ code: 'auto', name: 'Auto-detect', label: 'Auto-detect (mirror user language)' },
|
|
9
|
+
{ code: 'en', name: 'English', label: 'English' },
|
|
10
|
+
{ code: 'es', name: 'Español', label: 'Español (Spanish)' },
|
|
11
|
+
{ code: 'de', name: 'Deutsch', label: 'Deutsch (German)' },
|
|
12
|
+
{ code: 'ru', name: 'Russian', label: 'Русский (Russian)' },
|
|
13
|
+
{ code: 'zh', name: 'Chinese', label: '中文 (Chinese)' },
|
|
14
|
+
{ code: 'pt', name: 'Portuguese', label: 'Português (Portuguese)' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const LANGUAGE_NAMES = {
|
|
18
|
+
en: 'English',
|
|
19
|
+
es: 'Spanish',
|
|
20
|
+
de: 'German',
|
|
21
|
+
ru: 'Russian',
|
|
22
|
+
zh: 'Chinese',
|
|
23
|
+
pt: 'Portuguese',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function getLanguageInstruction(language) {
|
|
27
|
+
if (language === 'auto') {
|
|
28
|
+
return `
|
|
29
|
+
## Response Language
|
|
30
|
+
|
|
31
|
+
Respond in the same language the user writes in. Mirror their language automatically.
|
|
32
|
+
Always match the language of the current user message.
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const langName = LANGUAGE_NAMES[language] || language;
|
|
37
|
+
return `
|
|
38
|
+
## Response Language
|
|
39
|
+
|
|
40
|
+
Always respond in ${langName}. Write all specs, plans, reports, comments, and generated files in ${langName}.
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function promptLanguage(langOverride, yes = false) {
|
|
45
|
+
if (langOverride) return langOverride;
|
|
46
|
+
if (process.env.FRAME_LANG) return process.env.FRAME_LANG;
|
|
47
|
+
if (!process.stdin.isTTY || yes) return 'auto';
|
|
48
|
+
|
|
49
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
50
|
+
|
|
51
|
+
const prompt = '\n? Select response language:\n\n';
|
|
52
|
+
const options = LANGUAGES.map((l, i) => ` ${i + 1}) ${l.label}`).join('\n');
|
|
53
|
+
const footer = `\n Enter number [1-${LANGUAGES.length}] (or press Enter for auto): `;
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
rl.on('close', () => resolve('auto'));
|
|
57
|
+
|
|
58
|
+
rl.question(prompt + options + footer, (answer) => {
|
|
59
|
+
const choice = answer.trim();
|
|
60
|
+
if (choice === '' || choice === '1') {
|
|
61
|
+
rl.close();
|
|
62
|
+
return resolve('auto');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const idx = parseInt(choice, 10) - 1;
|
|
66
|
+
if (idx >= 0 && idx < LANGUAGES.length) {
|
|
67
|
+
rl.close();
|
|
68
|
+
return resolve(LANGUAGES[idx].code);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
rl.question(' Enter custom language code (e.g., "ja", "ko", "fr"): ', (code) => {
|
|
72
|
+
rl.close();
|
|
73
|
+
resolve(code.trim().toLowerCase() || 'auto');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const STACK_PRESETS = {
|
|
80
|
+
typescript: { typecheck: 'npx tsc --noEmit', test: 'npx vitest run', lint: 'npx eslint .', build: 'npm run build' },
|
|
81
|
+
javascript: { typecheck: '', test: 'npx vitest run', lint: 'npx eslint .', build: 'npm run build' },
|
|
82
|
+
python: { typecheck: 'mypy .', test: 'pytest', lint: 'ruff check .', build: '' },
|
|
83
|
+
go: { typecheck: 'go vet ./...', test: 'go test ./...', lint: 'golangci-lint run', build: 'go build ./...' },
|
|
84
|
+
rust: { typecheck: 'cargo check', test: 'cargo test', lint: 'cargo clippy', build: 'cargo build' },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const MODEL_DESCRIPTIONS = {
|
|
88
|
+
opus: 'opus — best quality, slower (recommended for architecture/security)',
|
|
89
|
+
sonnet: 'sonnet — faster, good for most tasks',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export async function promptConfig(defaultConfig, yes = false) {
|
|
93
|
+
if (!process.stdin.isTTY || yes) return defaultConfig;
|
|
94
|
+
|
|
95
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
96
|
+
const config = JSON.parse(JSON.stringify(defaultConfig));
|
|
97
|
+
|
|
98
|
+
console.log('\n? Project stack:\n');
|
|
99
|
+
const stacks = Object.keys(STACK_PRESETS);
|
|
100
|
+
stacks.forEach((s, i) => console.log(` ${i + 1}) ${s}`));
|
|
101
|
+
console.log(` ${stacks.length + 1}) custom`);
|
|
102
|
+
|
|
103
|
+
const stackAnswer = (await ask(rl, `\n Enter number [1-${stacks.length + 1}] (or press Enter for typescript): `)).trim();
|
|
104
|
+
const stackIdx = parseInt(stackAnswer, 10) - 1;
|
|
105
|
+
|
|
106
|
+
if (stackIdx >= 0 && stackIdx < stacks.length) {
|
|
107
|
+
const preset = STACK_PRESETS[stacks[stackIdx]];
|
|
108
|
+
Object.assign(config.quality.commands, preset);
|
|
109
|
+
console.log(`\x1b[32m✓\x1b[0m Stack: ${stacks[stackIdx]}`);
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(' Quality commands that will be used:');
|
|
112
|
+
for (const [k, v] of Object.entries(config.quality.commands)) {
|
|
113
|
+
if (v) console.log(` ${k}: ${v}`);
|
|
114
|
+
}
|
|
115
|
+
const confirm = (await ask(rl, '\n Looks good? [Y/n]: ')).trim().toLowerCase();
|
|
116
|
+
if (confirm === 'n') {
|
|
117
|
+
for (const key of ['typecheck', 'test', 'lint', 'build']) {
|
|
118
|
+
const current = config.quality.commands[key];
|
|
119
|
+
const val = (await ask(rl, ` ${key} command [${current}]: `)).trim();
|
|
120
|
+
if (val) config.quality.commands[key] = val;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else if (stackIdx === stacks.length) {
|
|
124
|
+
for (const key of ['typecheck', 'test', 'lint', 'build']) {
|
|
125
|
+
const current = config.quality.commands[key];
|
|
126
|
+
const val = (await ask(rl, ` ${key} command [${current}]: `)).trim();
|
|
127
|
+
if (val) config.quality.commands[key] = val;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('\n? Preferred model for agents:\n');
|
|
132
|
+
Object.values(MODEL_DESCRIPTIONS).forEach((d, i) => console.log(` ${i + 1}) ${d}`));
|
|
133
|
+
const modelAnswer = (await ask(rl, '\n Enter number [1-2] (or press Enter for opus): ')).trim().toLowerCase();
|
|
134
|
+
if (modelAnswer === '2' || modelAnswer === 'sonnet') {
|
|
135
|
+
// Note: model preference is stored for future use when agent routing is implemented
|
|
136
|
+
console.log('\x1b[32m✓\x1b[0m Model preference: sonnet');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
rl.close();
|
|
140
|
+
return config;
|
|
141
|
+
}
|
package/src/manifest.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname, basename } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const ROOT = join(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
export const VERSION = JSON.parse(
|
|
9
|
+
readFileSync(join(ROOT, 'package.json'), 'utf-8')
|
|
10
|
+
).version;
|
|
11
|
+
|
|
12
|
+
export const TEMPLATES_DIR = join(ROOT, 'templates');
|
|
13
|
+
|
|
14
|
+
export function resolveTarget(args) {
|
|
15
|
+
const target = args[0] || process.cwd();
|
|
16
|
+
let dir = target;
|
|
17
|
+
while (true) {
|
|
18
|
+
if (existsSync(join(dir, '.git'))) return target;
|
|
19
|
+
const parent = dirname(dir);
|
|
20
|
+
if (parent === dir) break;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
console.error(`Error: ${target} is not inside a git repository.`);
|
|
24
|
+
console.error('FRAME requires git history for checkpoints and rollbacks.');
|
|
25
|
+
console.error('Run: git init && git commit --allow-empty -m "init"');
|
|
26
|
+
console.error('(The empty commit is needed so FRAME has a base point for checkpoints.)');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function detectProjectName(target) {
|
|
31
|
+
const pkgPath = join(target, 'package.json');
|
|
32
|
+
if (existsSync(pkgPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
35
|
+
if (pkg.name) return pkg.name;
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
return basename(target);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function log(msg) {
|
|
42
|
+
console.log(msg);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function logSuccess(msg) {
|
|
46
|
+
console.log(`\x1b[32m✓\x1b[0m ${msg}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function logWarn(msg) {
|
|
50
|
+
console.log(`\x1b[33m⚠\x1b[0m ${msg}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function logError(msg) {
|
|
54
|
+
console.error(`\x1b[31m✗\x1b[0m ${msg}`);
|
|
55
|
+
}
|