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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +335 -0
  3. package/README.ru.md +333 -0
  4. package/bin/the-frame +5 -0
  5. package/bin/the-frame-ai +5 -0
  6. package/package.json +29 -0
  7. package/src/cli.js +84 -0
  8. package/src/doctor.js +164 -0
  9. package/src/init.js +178 -0
  10. package/src/languages.js +141 -0
  11. package/src/manifest.js +55 -0
  12. package/src/update.js +87 -0
  13. package/src/utils.js +55 -0
  14. package/templates/agents/builder.md +240 -0
  15. package/templates/agents/devils-advocate.md +136 -0
  16. package/templates/agents/planner.md +277 -0
  17. package/templates/agents/researcher.md +195 -0
  18. package/templates/agents/reviewer.md +300 -0
  19. package/templates/commands/frame:add-task.md +57 -0
  20. package/templates/commands/frame:build.md +170 -0
  21. package/templates/commands/frame:check-deps.md +118 -0
  22. package/templates/commands/frame:checkpoint.md +158 -0
  23. package/templates/commands/frame:cleanup-memory.md +80 -0
  24. package/templates/commands/frame:context.md +64 -0
  25. package/templates/commands/frame:daily.md +77 -0
  26. package/templates/commands/frame:debug.md +146 -0
  27. package/templates/commands/frame:doctor.md +170 -0
  28. package/templates/commands/frame:estimate.md +105 -0
  29. package/templates/commands/frame:explain.md +84 -0
  30. package/templates/commands/frame:fast.md +89 -0
  31. package/templates/commands/frame:forensics.md +139 -0
  32. package/templates/commands/frame:headless.md +118 -0
  33. package/templates/commands/frame:health.md +86 -0
  34. package/templates/commands/frame:init.md +231 -0
  35. package/templates/commands/frame:migrate.md +107 -0
  36. package/templates/commands/frame:note.md +32 -0
  37. package/templates/commands/frame:pause.md +145 -0
  38. package/templates/commands/frame:performance.md +228 -0
  39. package/templates/commands/frame:plan.md +198 -0
  40. package/templates/commands/frame:refactor.md +161 -0
  41. package/templates/commands/frame:research.md +131 -0
  42. package/templates/commands/frame:resume.md +137 -0
  43. package/templates/commands/frame:retrospective.md +196 -0
  44. package/templates/commands/frame:review.md +174 -0
  45. package/templates/commands/frame:rollback.md +207 -0
  46. package/templates/commands/frame:ship.md +148 -0
  47. package/templates/commands/frame:sprint-check.md +111 -0
  48. package/templates/commands/frame:status.md +103 -0
  49. package/templates/commands/frame:unstuck.md +102 -0
  50. package/templates/commands/frame:wave.md +312 -0
  51. package/templates/commands/frame:where.md +5 -0
  52. package/templates/commands/frame:why.md +57 -0
  53. package/templates/commands/frame:worktree.md +219 -0
  54. package/templates/hooks/git-safety.sh +33 -0
  55. package/templates/hooks/quality-gate.sh +52 -0
  56. package/templates/hooks/safety-net.sh +13 -0
  57. package/templates/hooks/session-init.sh +81 -0
  58. package/templates/planning/pause-state.json +1 -0
  59. package/templates/project/CLAUDE.md +63 -0
  60. package/templates/project/CONTEXT.md +16 -0
  61. package/templates/project/MAP.md +35 -0
  62. package/templates/project/ROADMAP.md +12 -0
  63. package/templates/project/STATE.md +13 -0
  64. package/templates/project/config.json +74 -0
  65. package/templates/project/memory/anti-patterns.md +14 -0
  66. package/templates/project/memory/context.md +23 -0
  67. package/templates/project/memory/conventions.md +19 -0
  68. package/templates/project/memory/decisions.md +20 -0
  69. package/templates/project/memory/dependencies.md +23 -0
  70. package/templates/project/memory/metrics.md +22 -0
  71. package/templates/project/memory/patterns.md +30 -0
  72. package/templates/project/memory/wins.md +11 -0
  73. package/templates/project/settings.local.json +50 -0
  74. package/templates/project/specs/_template/PRD.md +24 -0
  75. package/templates/project/specs/_template/plan.md +25 -0
  76. package/templates/project/specs/_template/spec.md +27 -0
  77. package/templates/project/specs/_template/subagent-prompt.md +43 -0
package/bin/the-frame ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/cli.js';
4
+
5
+ run(process.argv.slice(2));
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/cli.js';
4
+
5
+ run(process.argv.slice(2));
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
+ }
@@ -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
+ }
@@ -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
+ }