speckit-assistant 0.1.3 → 0.1.4
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/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +57 -21
- package/.next/app-path-routes-manifest.json +7 -2
- package/.next/build-manifest.json +5 -5
- package/.next/cache/next-devtools-config.json +1 -0
- package/.next/cache/webpack/client-development/0.pack.gz +0 -0
- package/.next/cache/webpack/client-development/index.pack.gz +0 -0
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/11.pack +0 -0
- package/.next/cache/webpack/client-production/12.pack +0 -0
- package/.next/cache/webpack/client-production/13.pack +0 -0
- package/.next/cache/webpack/client-production/15.pack +0 -0
- package/.next/cache/webpack/client-production/16.pack +0 -0
- package/.next/cache/webpack/client-production/17.pack +0 -0
- package/.next/cache/webpack/client-production/18.pack +0 -0
- package/.next/cache/webpack/client-production/19.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/20.pack +0 -0
- package/.next/cache/webpack/client-production/21.pack +0 -0
- package/.next/cache/webpack/client-production/22.pack +0 -0
- package/.next/cache/webpack/client-production/23.pack +0 -0
- package/.next/cache/webpack/client-production/24.pack +0 -0
- package/.next/cache/webpack/client-production/25.pack +0 -0
- package/.next/cache/webpack/client-production/26.pack +0 -0
- package/.next/cache/webpack/client-production/27.pack +0 -0
- package/.next/cache/webpack/client-production/28.pack +0 -0
- package/.next/cache/webpack/client-production/29.pack +0 -0
- package/.next/cache/webpack/client-production/3.pack +0 -0
- package/.next/cache/webpack/client-production/30.pack +0 -0
- package/.next/cache/webpack/client-production/31.pack +0 -0
- package/.next/cache/webpack/client-production/32.pack +0 -0
- package/.next/cache/webpack/client-production/33.pack +0 -0
- package/.next/cache/webpack/client-production/34.pack +0 -0
- package/.next/cache/webpack/client-production/4.pack +0 -0
- package/.next/cache/webpack/client-production/6.pack +0 -0
- package/.next/cache/webpack/client-production/7.pack +0 -0
- package/.next/cache/webpack/client-production/8.pack +0 -0
- package/.next/cache/webpack/client-production/9.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/1.pack +0 -0
- package/.next/cache/webpack/server-production/10.pack +0 -0
- package/.next/cache/webpack/server-production/11.pack +0 -0
- package/.next/cache/webpack/server-production/12.pack +0 -0
- package/.next/cache/webpack/server-production/13.pack +0 -0
- package/.next/cache/webpack/server-production/14.pack +0 -0
- package/.next/cache/webpack/server-production/16.pack +0 -0
- package/.next/cache/webpack/server-production/18.pack +0 -0
- package/.next/cache/webpack/server-production/19.pack +0 -0
- package/.next/cache/webpack/server-production/2.pack +0 -0
- package/.next/cache/webpack/server-production/20.pack +0 -0
- package/.next/cache/webpack/server-production/21.pack +0 -0
- package/.next/cache/webpack/server-production/23.pack +0 -0
- package/.next/cache/webpack/server-production/24.pack +0 -0
- package/.next/cache/webpack/server-production/25.pack +0 -0
- package/.next/cache/webpack/server-production/26.pack +0 -0
- package/.next/cache/webpack/server-production/27.pack +0 -0
- package/.next/cache/webpack/server-production/28.pack +0 -0
- package/.next/cache/webpack/server-production/29.pack +0 -0
- package/.next/cache/webpack/server-production/3.pack +0 -0
- package/.next/cache/webpack/server-production/30.pack +0 -0
- package/.next/cache/webpack/server-production/31.pack +0 -0
- package/.next/cache/webpack/server-production/4.pack +0 -0
- package/.next/cache/webpack/server-production/5.pack +0 -0
- package/.next/cache/webpack/server-production/6.pack +0 -0
- package/.next/cache/webpack/server-production/7.pack +0 -0
- package/.next/cache/webpack/server-production/9.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/.next/prerender-manifest.json +7 -7
- package/.next/react-loadable-manifest.json +8 -1
- package/.next/required-server-files.json +3 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/api/feature/route.js +1 -1
- package/.next/server/app/api/feature/route.js.nft.json +1 -1
- package/.next/server/app/api/feature/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/file/route.js +1 -1
- package/.next/server/app/api/file/route.js.nft.json +1 -1
- package/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/git/route.js +1 -0
- package/.next/server/app/api/git/route.js.nft.json +1 -0
- package/.next/server/app/api/git/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/personas/route.js +1 -0
- package/.next/server/app/api/personas/route.js.nft.json +1 -0
- package/.next/server/app/api/personas/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/phase/route.js +3 -3
- package/.next/server/app/api/phase/route.js.nft.json +1 -1
- package/.next/server/app/api/phase/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/state/route.js +1 -1
- package/.next/server/app/api/state/route.js.nft.json +1 -1
- package/.next/server/app/api/state/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/state/watch/route.js +2 -2
- package/.next/server/app/api/state/watch/route.js.nft.json +1 -1
- package/.next/server/app/api/state/watch/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/task/route.js +1 -1
- package/.next/server/app/api/task/route.js.nft.json +1 -1
- package/.next/server/app/api/task/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/terminal/input/route.js +1 -0
- package/.next/server/app/api/terminal/input/route.js.nft.json +1 -0
- package/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/terminal/resize/route.js +1 -0
- package/.next/server/app/api/terminal/resize/route.js.nft.json +1 -0
- package/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/terminal/stream/route.js +3 -0
- package/.next/server/app/api/terminal/stream/route.js.nft.json +1 -0
- package/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -0
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +4 -4
- package/.next/server/app/page.js +26 -7
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +7 -2
- package/.next/server/chunks/607.js +2 -2
- package/.next/server/chunks/897.js +13 -7
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/ELGpn5csIINP2EpeEMCo6/_buildManifest.js +1 -0
- package/.next/static/chunks/111acf76-29d5e5905666f1e0.js +18 -0
- package/.next/static/chunks/343.91af0d46f6df0f05.js +1 -0
- package/.next/static/chunks/873-a995367ae371a5e4.js +1 -0
- package/.next/static/chunks/app/api/feature/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/file/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/git/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/personas/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/phase/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/state/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/state/watch/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/task/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/terminal/input/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/terminal/resize/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/api/terminal/stream/route-b4fbc89d13fef983.js +1 -0
- package/.next/static/chunks/app/page-63ac4ff563941667.js +1 -0
- package/.next/static/chunks/webpack-c997c7bd3e8f7cdf.js +1 -0
- package/.next/static/css/31544403e38a69f8.css +3 -0
- package/.next/static/css/{008a05b0ad6b854a.css → af3f40f14f702ff7.css} +1 -1
- package/.next/trace +3 -3
- package/.next/types/app/api/git/route.ts +347 -0
- package/.next/types/app/api/personas/route.ts +347 -0
- package/.next/types/app/api/terminal/input/route.ts +347 -0
- package/.next/types/app/api/terminal/resize/route.ts +347 -0
- package/.next/types/app/api/terminal/stream/route.ts +347 -0
- package/.next/types/routes.d.ts +6 -1
- package/.next/types/validator.ts +45 -0
- package/bin/adapters/primary/api/terminalManager.js +92 -0
- package/bin/adapters/secondary/agent/ProcessAgentRunner.js +124 -33
- package/bin/adapters/secondary/fs/FSWorkspaceRepository.js +40 -5
- package/bin/adapters/secondary/pty/ptyLoader.js +128 -0
- package/bin/app/api/git/route.js +156 -0
- package/bin/app/api/personas/route.js +76 -0
- package/bin/app/api/phase/route.js +18 -5
- package/bin/app/api/state/watch/route.js +7 -2
- package/bin/app/api/terminal/input/route.js +18 -0
- package/bin/app/api/terminal/resize/route.js +18 -0
- package/bin/app/api/terminal/stream/route.js +44 -0
- package/bin/domain/models/personas.js +65 -0
- package/bin/domain/services/WorkflowService.js +79 -5
- package/next.config.mjs +3 -0
- package/package.json +9 -1
- package/.next/static/Bxo1ckOxvQcndtSKD9Yna/_buildManifest.js +0 -1
- package/.next/static/chunks/590-a6568595ecd2a994.js +0 -1
- package/.next/static/chunks/app/api/feature/route-bb3c1a82e892ab58.js +0 -1
- package/.next/static/chunks/app/api/file/route-bb3c1a82e892ab58.js +0 -1
- package/.next/static/chunks/app/api/phase/route-bb3c1a82e892ab58.js +0 -1
- package/.next/static/chunks/app/api/state/route-bb3c1a82e892ab58.js +0 -1
- package/.next/static/chunks/app/api/state/watch/route-bb3c1a82e892ab58.js +0 -1
- package/.next/static/chunks/app/api/task/route-bb3c1a82e892ab58.js +0 -1
- package/.next/static/chunks/app/page-8a5248f7704cde29.js +0 -1
- package/.next/static/chunks/webpack-c460f8e58a9e9eff.js +0 -1
- package/.next/static/css/6fd2e11db3a59771.css +0 -3
- /package/.next/static/{Bxo1ckOxvQcndtSKD9Yna → ELGpn5csIINP2EpeEMCo6}/_ssgManifest.js +0 -0
|
@@ -34,7 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ProcessAgentRunner = void 0;
|
|
37
|
-
const
|
|
37
|
+
const ptyLoader_1 = require("../pty/ptyLoader");
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const PHASE_COMMANDS = {
|
|
@@ -49,43 +49,138 @@ const PHASE_COMMANDS = {
|
|
|
49
49
|
implementation: '/speckit.implement',
|
|
50
50
|
};
|
|
51
51
|
class ProcessAgentRunner {
|
|
52
|
+
activeProcesses = new Map();
|
|
53
|
+
phaseKey(featureName, phase) {
|
|
54
|
+
return `${featureName || 'global'}-${phase}`;
|
|
55
|
+
}
|
|
56
|
+
personaKey(featureName, id) {
|
|
57
|
+
return `${featureName}-impl-persona-${id}`;
|
|
58
|
+
}
|
|
59
|
+
async writeStdin(phase, featureName, text, personaId) {
|
|
60
|
+
const procKey = personaId && featureName
|
|
61
|
+
? this.personaKey(featureName, personaId)
|
|
62
|
+
: this.phaseKey(featureName, phase);
|
|
63
|
+
const child = this.activeProcesses.get(procKey);
|
|
64
|
+
if (child) {
|
|
65
|
+
// The frontend xterm sends raw keystrokes (arrow keys, Enter as '\r', etc.)
|
|
66
|
+
// so we forward them verbatim — this is what lets interactive pickers like
|
|
67
|
+
// the clarify Q&A be navigated, not just line-based answers.
|
|
68
|
+
child.write(text);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
async resize(phase, featureName, cols, rows, personaId) {
|
|
74
|
+
const procKey = personaId && featureName
|
|
75
|
+
? this.personaKey(featureName, personaId)
|
|
76
|
+
: this.phaseKey(featureName, phase);
|
|
77
|
+
const child = this.activeProcesses.get(procKey);
|
|
78
|
+
if (!child)
|
|
79
|
+
return false;
|
|
80
|
+
try {
|
|
81
|
+
child.resize(cols, rows);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async stop(phase, featureName, personaId) {
|
|
89
|
+
const procKey = personaId && featureName
|
|
90
|
+
? this.personaKey(featureName, personaId)
|
|
91
|
+
: this.phaseKey(featureName, phase);
|
|
92
|
+
const child = this.activeProcesses.get(procKey);
|
|
93
|
+
if (child) {
|
|
94
|
+
try {
|
|
95
|
+
child.kill();
|
|
96
|
+
this.activeProcesses.delete(procKey);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
52
105
|
async runPhase(workspacePath, phase, featureName, agentConfig, userPrompt, onData) {
|
|
53
106
|
const specArg = phase !== 'constitution' && featureName ? `specs/${featureName}` : null;
|
|
54
107
|
const slashCmd = `${PHASE_COMMANDS[phase]}${specArg ? ` ${specArg}` : ''}`;
|
|
55
108
|
const context = userPrompt?.trim();
|
|
56
109
|
const fullPrompt = context ? `${context}\n\n${slashCmd}` : slashCmd;
|
|
110
|
+
return this.spawnAgent({
|
|
111
|
+
workspacePath,
|
|
112
|
+
procKey: this.phaseKey(featureName, phase),
|
|
113
|
+
doneTag: phase,
|
|
114
|
+
agentConfig,
|
|
115
|
+
fullPrompt,
|
|
116
|
+
onData,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async runPersona(workspacePath, featureName, persona, agentConfig, onData) {
|
|
120
|
+
// Personas always operate on a specific feature's artifacts.
|
|
121
|
+
const fullPrompt = `${persona.command} specs/${featureName}`;
|
|
122
|
+
return this.spawnAgent({
|
|
123
|
+
workspacePath,
|
|
124
|
+
procKey: this.personaKey(featureName, persona.id),
|
|
125
|
+
doneTag: `persona:${persona.id}`,
|
|
126
|
+
agentConfig,
|
|
127
|
+
fullPrompt,
|
|
128
|
+
onData,
|
|
129
|
+
persona,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Shared spawn path for both phase and persona runs: spawns the agent CLI
|
|
133
|
+
// under a PTY (so interactive CLIs detect a TTY), streams output, and resolves
|
|
134
|
+
// with the exit code.
|
|
135
|
+
spawnAgent(opts) {
|
|
136
|
+
const { workspacePath, procKey, doneTag, agentConfig, fullPrompt, onData } = opts;
|
|
57
137
|
const { cmd, args, stdin } = this.buildSpawnArgs(agentConfig, fullPrompt);
|
|
58
|
-
const isWin = process.platform === 'win32';
|
|
59
138
|
return new Promise((resolve) => {
|
|
60
139
|
onData?.(`Running: ${cmd} ${args.join(' ')}\n\n`);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
140
|
+
// Spawn the agent under a real pseudo-terminal. This is what makes
|
|
141
|
+
// interactive CLIs (e.g. the clarify Q&A) detect a TTY via isatty() and
|
|
142
|
+
// actually prompt the user instead of running single-shot. We still go
|
|
143
|
+
// through a login shell so PATH/aliases resolve the agent binary the same
|
|
144
|
+
// way they would in the user's own terminal.
|
|
145
|
+
const isWin = process.platform === 'win32';
|
|
146
|
+
const ptyFile = isWin
|
|
147
|
+
? (process.env.COMSPEC || 'cmd.exe')
|
|
148
|
+
: (process.env.SHELL || '/bin/bash');
|
|
149
|
+
const ptyArgs = isWin
|
|
150
|
+
? ['/c', [cmd, ...args].join(' ')]
|
|
151
|
+
: ['-l', '-c', [cmd, ...args].map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')];
|
|
152
|
+
const child = (0, ptyLoader_1.loadPty)().spawn(ptyFile, ptyArgs, {
|
|
153
|
+
name: 'xterm-256color',
|
|
154
|
+
cols: 120,
|
|
155
|
+
rows: 30,
|
|
156
|
+
cwd: workspacePath,
|
|
157
|
+
env: {
|
|
158
|
+
...process.env,
|
|
159
|
+
TERM: 'xterm-256color',
|
|
160
|
+
...(opts.persona ? {
|
|
161
|
+
SPECKIT_PERSONA_ID: opts.persona.id || '',
|
|
162
|
+
SPECKIT_PERSONA_LABEL: opts.persona.label || '',
|
|
163
|
+
SPECKIT_PERSONA_MODEL: opts.persona.model || '',
|
|
164
|
+
SPECKIT_PERSONA_SYSTEM_PROMPT: opts.persona.systemPrompt || '',
|
|
165
|
+
SPECKIT_PERSONA_CAPABILITIES: Array.isArray(opts.persona.capabilities) ? opts.persona.capabilities.join(',') : '',
|
|
166
|
+
SPECKIT_PERSONA_TOOLS: Array.isArray(opts.persona.tools) ? opts.persona.tools.join(',') : '',
|
|
167
|
+
} : {})
|
|
168
|
+
},
|
|
82
169
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
170
|
+
this.activeProcesses.set(procKey, child);
|
|
171
|
+
// For agents that receive their prompt over stdin, feed it now and send
|
|
172
|
+
// EOF (Ctrl-D) so the single-shot CLI starts processing. Agents that take
|
|
173
|
+
// the prompt as an argument keep the channel open so the user can answer
|
|
174
|
+
// interactive prompts (e.g. clarify) through writeStdin().
|
|
175
|
+
if (stdin !== undefined) {
|
|
176
|
+
child.write(stdin);
|
|
177
|
+
child.write('\x04');
|
|
178
|
+
}
|
|
179
|
+
child.onData((data) => {
|
|
180
|
+
onData?.(data);
|
|
86
181
|
});
|
|
87
|
-
child.
|
|
88
|
-
|
|
182
|
+
child.onExit(({ exitCode }) => {
|
|
183
|
+
this.activeProcesses.delete(procKey);
|
|
89
184
|
onData?.(`\nProcess exited with code ${exitCode}\n`);
|
|
90
185
|
// Write the phase done file so local watch logs it
|
|
91
186
|
try {
|
|
@@ -93,17 +188,13 @@ class ProcessAgentRunner {
|
|
|
93
188
|
if (!fs.existsSync(runtimeDir)) {
|
|
94
189
|
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
95
190
|
}
|
|
96
|
-
fs.writeFileSync(path.join(runtimeDir, 'phase-done.txt'), `${
|
|
191
|
+
fs.writeFileSync(path.join(runtimeDir, 'phase-done.txt'), `${doneTag}:${exitCode}`, 'utf-8');
|
|
97
192
|
}
|
|
98
193
|
catch {
|
|
99
194
|
// ignore
|
|
100
195
|
}
|
|
101
196
|
resolve(exitCode);
|
|
102
197
|
});
|
|
103
|
-
child.on('error', (err) => {
|
|
104
|
-
onData?.(`\nFailed to start process: ${err.message}\n`);
|
|
105
|
-
resolve(-1);
|
|
106
|
-
});
|
|
107
198
|
});
|
|
108
199
|
}
|
|
109
200
|
buildSpawnArgs(config, prompt) {
|
|
@@ -61,7 +61,12 @@ class FSWorkspaceRepository {
|
|
|
61
61
|
else {
|
|
62
62
|
state.constitutionPhase.filePath = null;
|
|
63
63
|
state.constitutionPhase.content = null;
|
|
64
|
-
|
|
64
|
+
// Don't clobber explicit user/runtime states when no file is on disk:
|
|
65
|
+
// 'running' (agent still generating) and 'approved' (a stored user decision,
|
|
66
|
+
// e.g. advanced via Kanban drag) must survive reconciliation.
|
|
67
|
+
if (!this.isUserState(state.constitutionPhase.status)) {
|
|
68
|
+
state.constitutionPhase.status = 'idle';
|
|
69
|
+
}
|
|
65
70
|
}
|
|
66
71
|
// 2. Reconcile Features in specs/
|
|
67
72
|
const specsDir = path.join(workspacePath, 'specs');
|
|
@@ -100,7 +105,9 @@ class FSWorkspaceRepository {
|
|
|
100
105
|
else {
|
|
101
106
|
ps.filePath = null;
|
|
102
107
|
ps.content = null;
|
|
103
|
-
ps.status
|
|
108
|
+
if (!this.isUserState(ps.status)) {
|
|
109
|
+
ps.status = 'idle';
|
|
110
|
+
}
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
// Reconcile clarification (clarification.md or clarify.md)
|
|
@@ -118,7 +125,9 @@ class FSWorkspaceRepository {
|
|
|
118
125
|
else {
|
|
119
126
|
clarifyPhase.filePath = null;
|
|
120
127
|
clarifyPhase.content = null;
|
|
121
|
-
clarifyPhase.status
|
|
128
|
+
if (!this.isUserState(clarifyPhase.status)) {
|
|
129
|
+
clarifyPhase.status = 'idle';
|
|
130
|
+
}
|
|
122
131
|
}
|
|
123
132
|
// Implementation phase doesn't have its own file, but we can resolve its status.
|
|
124
133
|
// If specs/[feature]/tasks.md exists, and all checkboxes are checked, it could be complete.
|
|
@@ -127,6 +136,24 @@ class FSWorkspaceRepository {
|
|
|
127
136
|
// Make sure its filePath is null (since there is no implementation.md file)
|
|
128
137
|
implPhase.filePath = null;
|
|
129
138
|
implPhase.content = null;
|
|
139
|
+
// Reconcile review-gate personas against their report files. Statuses
|
|
140
|
+
// saved by the gate are kept; this is a safety net that also reflects a
|
|
141
|
+
// report written/edited out of band. Never downgrade a 'running' persona.
|
|
142
|
+
if (implPhase.personas) {
|
|
143
|
+
for (const persona of implPhase.personas) {
|
|
144
|
+
const reportFp = path.join(specsDir, feature.name, 'reviews', `${persona.id}.md`);
|
|
145
|
+
if (fs.existsSync(reportFp)) {
|
|
146
|
+
persona.reportPath = path.join('specs', feature.name, 'reviews', `${persona.id}.md`);
|
|
147
|
+
if (persona.status !== 'running') {
|
|
148
|
+
const verdict = fs.readFileSync(reportFp, 'utf-8').toUpperCase();
|
|
149
|
+
if (/VERDICT:\s*FAIL/.test(verdict))
|
|
150
|
+
persona.status = 'failed';
|
|
151
|
+
else if (/VERDICT:\s*PASS/.test(verdict))
|
|
152
|
+
persona.status = 'passed';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
130
157
|
}
|
|
131
158
|
}
|
|
132
159
|
else {
|
|
@@ -156,7 +183,9 @@ class FSWorkspaceRepository {
|
|
|
156
183
|
phases: f.phases.map(p => ({
|
|
157
184
|
phase: p.phase,
|
|
158
185
|
status: p.status,
|
|
159
|
-
stale: p.stale
|
|
186
|
+
stale: p.stale,
|
|
187
|
+
// Persist the implementation review-gate persona statuses.
|
|
188
|
+
...(p.personas ? { personas: p.personas.map(ps => ({ id: ps.id, status: ps.status })) } : {})
|
|
160
189
|
}))
|
|
161
190
|
})),
|
|
162
191
|
activeFeatureName: state.activeFeatureName
|
|
@@ -245,6 +274,9 @@ class FSWorkspaceRepository {
|
|
|
245
274
|
phases: FEATURE_PHASES.map(p => this.makePhaseState(p))
|
|
246
275
|
};
|
|
247
276
|
}
|
|
277
|
+
isUserState(status) {
|
|
278
|
+
return status === 'running' || status === 'approved';
|
|
279
|
+
}
|
|
248
280
|
loadStateFile(workspacePath) {
|
|
249
281
|
const statePath = path.join(workspacePath, '.specify', '.runtime', 'workflow-state.json');
|
|
250
282
|
if (!fs.existsSync(statePath)) {
|
|
@@ -267,7 +299,10 @@ class FSWorkspaceRepository {
|
|
|
267
299
|
status: savedPhase?.status || 'idle',
|
|
268
300
|
stale: savedPhase?.stale || false,
|
|
269
301
|
filePath: null,
|
|
270
|
-
content: null
|
|
302
|
+
content: null,
|
|
303
|
+
...(Array.isArray(savedPhase?.personas)
|
|
304
|
+
? { personas: savedPhase.personas.map((ps) => ({ id: ps.id, status: ps.status || 'idle' })) }
|
|
305
|
+
: {})
|
|
271
306
|
};
|
|
272
307
|
})
|
|
273
308
|
}));
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadPty = loadPty;
|
|
37
|
+
exports._clearPtyCacheForTesting = _clearPtyCacheForTesting;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
let cached = null;
|
|
42
|
+
class PureJsPtyLike {
|
|
43
|
+
child;
|
|
44
|
+
constructor(child) {
|
|
45
|
+
this.child = child;
|
|
46
|
+
}
|
|
47
|
+
write(data) {
|
|
48
|
+
this.child.stdin?.write(data);
|
|
49
|
+
}
|
|
50
|
+
resize(cols, rows) {
|
|
51
|
+
// No-op for standard child_process pipes
|
|
52
|
+
}
|
|
53
|
+
kill(signal) {
|
|
54
|
+
this.child.kill(signal);
|
|
55
|
+
}
|
|
56
|
+
onData(cb) {
|
|
57
|
+
this.child.stdout?.on('data', (chunk) => cb(chunk.toString()));
|
|
58
|
+
this.child.stderr?.on('data', (chunk) => cb(chunk.toString()));
|
|
59
|
+
}
|
|
60
|
+
onExit(cb) {
|
|
61
|
+
this.child.on('exit', (code, signal) => {
|
|
62
|
+
cb({ exitCode: code ?? 0, signal: signal ? 1 : undefined });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const pureJsPtyModule = {
|
|
67
|
+
spawn(file, args, options) {
|
|
68
|
+
const cp = (0, child_process_1.spawn)(file, args, {
|
|
69
|
+
cwd: options.cwd,
|
|
70
|
+
env: options.env,
|
|
71
|
+
});
|
|
72
|
+
return new PureJsPtyLike(cp);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Loads node-pty defensively.
|
|
77
|
+
*
|
|
78
|
+
* node-pty relies on a native `spawn-helper` binary (on POSIX) that must be
|
|
79
|
+
* executable. Some package managers / extraction paths drop the exec bit when
|
|
80
|
+
* unpacking the prebuilt binaries, which makes pty.spawn fail with
|
|
81
|
+
* "posix_spawnp failed". We restore the exec bit before first use so the
|
|
82
|
+
* workspace shell and interactive agent phases work regardless of how the
|
|
83
|
+
* dependency was installed.
|
|
84
|
+
*/
|
|
85
|
+
function loadPty() {
|
|
86
|
+
if (cached)
|
|
87
|
+
return cached;
|
|
88
|
+
if (process.env.SPECKIT_TEST_FORCE_FALLBACK === 'true') {
|
|
89
|
+
cached = pureJsPtyModule;
|
|
90
|
+
return pureJsPtyModule;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
94
|
+
const pty = require('node-pty');
|
|
95
|
+
if (process.platform !== 'win32') {
|
|
96
|
+
try {
|
|
97
|
+
const ptyDir = path.dirname(require.resolve('node-pty/package.json'));
|
|
98
|
+
const prebuilds = path.join(ptyDir, 'prebuilds');
|
|
99
|
+
if (fs.existsSync(prebuilds)) {
|
|
100
|
+
for (const entry of fs.readdirSync(prebuilds)) {
|
|
101
|
+
const helper = path.join(prebuilds, entry, 'spawn-helper');
|
|
102
|
+
if (fs.existsSync(helper)) {
|
|
103
|
+
try {
|
|
104
|
+
fs.chmodSync(helper, 0o755);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// best effort
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// best effort — if anything fails we still return the module
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
cached = pty;
|
|
118
|
+
return pty;
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.warn('node-pty failed to load, falling back to pure child_process pty emulation.');
|
|
122
|
+
cached = pureJsPtyModule;
|
|
123
|
+
return pureJsPtyModule;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function _clearPtyCacheForTesting() {
|
|
127
|
+
cached = null;
|
|
128
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GET = GET;
|
|
37
|
+
const server_1 = require("next/server");
|
|
38
|
+
const utils_1 = require("../../../adapters/primary/api/utils");
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
function runGit(cmd, cwd) {
|
|
43
|
+
try {
|
|
44
|
+
return (0, child_process_1.execSync)(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getRefBranch(cwd) {
|
|
51
|
+
const output = runGit('git branch --format="%(refname:short)"', cwd);
|
|
52
|
+
const branches = output.split('\n').map(b => b.trim());
|
|
53
|
+
if (branches.includes('main'))
|
|
54
|
+
return 'main';
|
|
55
|
+
if (branches.includes('master'))
|
|
56
|
+
return 'master';
|
|
57
|
+
return 'HEAD';
|
|
58
|
+
}
|
|
59
|
+
async function GET(req) {
|
|
60
|
+
try {
|
|
61
|
+
const { searchParams } = new URL(req.url);
|
|
62
|
+
const action = searchParams.get('action');
|
|
63
|
+
const filePath = searchParams.get('file');
|
|
64
|
+
const workspacePath = (0, utils_1.getWorkspacePath)();
|
|
65
|
+
// Check if git is initialized
|
|
66
|
+
if (!fs.existsSync(path.join(workspacePath, '.git'))) {
|
|
67
|
+
return server_1.NextResponse.json({
|
|
68
|
+
files: [],
|
|
69
|
+
log: [],
|
|
70
|
+
error: 'Git repository not found in workspace.'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const ref = getRefBranch(workspacePath);
|
|
74
|
+
if (action === 'diff' && filePath) {
|
|
75
|
+
const absPath = path.resolve(workspacePath, filePath);
|
|
76
|
+
// Safety check: ensure file is inside workspace
|
|
77
|
+
const normWorkspace = path.normalize(workspacePath).replace(/\\/g, '/').toLowerCase();
|
|
78
|
+
const normFile = path.normalize(absPath).replace(/\\/g, '/').toLowerCase();
|
|
79
|
+
if (!normFile.startsWith(normWorkspace)) {
|
|
80
|
+
return server_1.NextResponse.json({ error: 'Access denied' }, { status: 403 });
|
|
81
|
+
}
|
|
82
|
+
// Check if file is untracked
|
|
83
|
+
const statusOutput = runGit(`git status --porcelain "${filePath}"`, workspacePath);
|
|
84
|
+
const isUntracked = statusOutput.startsWith('??');
|
|
85
|
+
if (isUntracked && fs.existsSync(absPath)) {
|
|
86
|
+
// Return file contents prefixed with + to simulate unified diff for new file
|
|
87
|
+
const content = fs.readFileSync(absPath, 'utf-8');
|
|
88
|
+
const diffLines = content.split('\n').map(line => `+${line}`).join('\n');
|
|
89
|
+
return server_1.NextResponse.json({ diff: diffLines });
|
|
90
|
+
}
|
|
91
|
+
const diffOutput = runGit(`git diff ${ref} -- "${filePath}"`, workspacePath);
|
|
92
|
+
return server_1.NextResponse.json({ diff: diffOutput || 'No modifications found.' });
|
|
93
|
+
}
|
|
94
|
+
if (action === 'status') {
|
|
95
|
+
// 1. Get modified files with stats
|
|
96
|
+
const numstatOutput = runGit(`git diff ${ref} --numstat`, workspacePath);
|
|
97
|
+
const filesMap = {};
|
|
98
|
+
if (numstatOutput) {
|
|
99
|
+
numstatOutput.split('\n').forEach(line => {
|
|
100
|
+
const parts = line.split('\t');
|
|
101
|
+
if (parts.length >= 3) {
|
|
102
|
+
const additions = parseInt(parts[0], 10) || 0;
|
|
103
|
+
const deletions = parseInt(parts[1], 10) || 0;
|
|
104
|
+
const relPath = parts[2].trim();
|
|
105
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
106
|
+
const isTest = relPath.endsWith('.test.ts') || relPath.endsWith('.test.js') || relPath.endsWith('.spec.ts') || relPath.endsWith('.spec.js');
|
|
107
|
+
const type = isTest ? 'test'
|
|
108
|
+
: (ext === '.ts' || ext === '.js' || ext === '.tsx' || ext === '.jsx' ? 'code' : 'doc');
|
|
109
|
+
filesMap[relPath] = { path: relPath, additions, deletions, type };
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// 2. Get untracked files and append them
|
|
114
|
+
const statusOutput = runGit('git status --porcelain', workspacePath);
|
|
115
|
+
if (statusOutput) {
|
|
116
|
+
statusOutput.split('\n').forEach(line => {
|
|
117
|
+
if (line.startsWith('??')) {
|
|
118
|
+
const relPath = line.substring(3).trim();
|
|
119
|
+
if (!filesMap[relPath]) {
|
|
120
|
+
// Estimate lines as additions
|
|
121
|
+
let additions = 0;
|
|
122
|
+
const absPath = path.join(workspacePath, relPath);
|
|
123
|
+
try {
|
|
124
|
+
if (fs.existsSync(absPath) && fs.statSync(absPath).isFile()) {
|
|
125
|
+
additions = fs.readFileSync(absPath, 'utf-8').split('\n').length;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// ignore
|
|
130
|
+
}
|
|
131
|
+
const ext = path.extname(relPath).toLowerCase();
|
|
132
|
+
const isTest = relPath.endsWith('.test.ts') || relPath.endsWith('.test.js') || relPath.endsWith('.spec.ts') || relPath.endsWith('.spec.js');
|
|
133
|
+
const type = isTest ? 'test'
|
|
134
|
+
: (ext === '.ts' || ext === '.js' || ext === '.tsx' || ext === '.jsx' ? 'code' : 'doc');
|
|
135
|
+
filesMap[relPath] = { path: relPath, additions, deletions: 0, type };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const files = Object.values(filesMap);
|
|
141
|
+
// 3. Get Git Log
|
|
142
|
+
const logOutput = runGit('git log -n 10 --pretty=format:"%h|%s|%an|%ar"', workspacePath);
|
|
143
|
+
const log = logOutput ? logOutput.split('\n').map(line => {
|
|
144
|
+
const [hash, message, author, date] = line.split('|');
|
|
145
|
+
return { hash, message, author, date };
|
|
146
|
+
}) : [];
|
|
147
|
+
// 4. Get active branch name
|
|
148
|
+
const branch = runGit('git rev-parse --abbrev-ref HEAD', workspacePath) || 'HEAD';
|
|
149
|
+
return server_1.NextResponse.json({ files, log, branch });
|
|
150
|
+
}
|
|
151
|
+
return server_1.NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
return server_1.NextResponse.json({ error: err.message }, { status: 500 });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GET = GET;
|
|
37
|
+
exports.POST = POST;
|
|
38
|
+
const server_1 = require("next/server");
|
|
39
|
+
const utils_1 = require("../../../adapters/primary/api/utils");
|
|
40
|
+
const personas_1 = require("../../../domain/models/personas");
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
async function GET() {
|
|
44
|
+
try {
|
|
45
|
+
const workspacePath = (0, utils_1.getWorkspacePath)();
|
|
46
|
+
const configPath = path.join(workspacePath, '.specify', 'personas-config.json');
|
|
47
|
+
if (!fs.existsSync(configPath)) {
|
|
48
|
+
return server_1.NextResponse.json(personas_1.DEFAULT_PERSONAS);
|
|
49
|
+
}
|
|
50
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
51
|
+
const parsed = JSON.parse(content);
|
|
52
|
+
return server_1.NextResponse.json(parsed);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return server_1.NextResponse.json({ error: err.message }, { status: 500 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function POST(req) {
|
|
59
|
+
try {
|
|
60
|
+
const workspacePath = (0, utils_1.getWorkspacePath)();
|
|
61
|
+
const body = await req.json();
|
|
62
|
+
if (!Array.isArray(body)) {
|
|
63
|
+
return server_1.NextResponse.json({ error: 'Body must be an array of persona configs' }, { status: 400 });
|
|
64
|
+
}
|
|
65
|
+
const configDir = path.join(workspacePath, '.specify');
|
|
66
|
+
if (!fs.existsSync(configDir)) {
|
|
67
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
const configPath = path.join(configDir, 'personas-config.json');
|
|
70
|
+
fs.writeFileSync(configPath, JSON.stringify(body, null, 2), 'utf-8');
|
|
71
|
+
return server_1.NextResponse.json({ success: true, personas: body });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return server_1.NextResponse.json({ error: err.message }, { status: 500 });
|
|
75
|
+
}
|
|
76
|
+
}
|