@nick848/fet 1.1.1 → 1.1.2
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/dist/cli/index.js +239 -1
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2004,6 +2004,10 @@ var phaseByCommand = {
|
|
|
2004
2004
|
onboard: "explore"
|
|
2005
2005
|
};
|
|
2006
2006
|
async function proxyCommand(ctx, command, args) {
|
|
2007
|
+
if (command === "propose" || command === "continue" || command === "ff") {
|
|
2008
|
+
await artifactWorkflowCommand(ctx, command, args);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2007
2011
|
const openSpecArgs = stripFetOptions(args);
|
|
2008
2012
|
const runState = {};
|
|
2009
2013
|
await withProjectLock(ctx.projectRoot, { command, cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
@@ -2078,6 +2082,225 @@ async function proxyCommand(ctx, command, args) {
|
|
|
2078
2082
|
data: graphContext ? { graphContext } : void 0
|
|
2079
2083
|
});
|
|
2080
2084
|
}
|
|
2085
|
+
async function artifactWorkflowCommand(ctx, command, args) {
|
|
2086
|
+
const openSpecArgs = stripFetOptions(args);
|
|
2087
|
+
const runState = {};
|
|
2088
|
+
await withProjectLock(ctx.projectRoot, { command, cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
2089
|
+
await assertOpenSpecCommandSupported(ctx, "status", command);
|
|
2090
|
+
await assertOpenSpecCommandSupported(ctx, "instructions", command);
|
|
2091
|
+
if (command === "propose") {
|
|
2092
|
+
await assertOpenSpecCommandSupported(ctx, "new", command);
|
|
2093
|
+
}
|
|
2094
|
+
const changeId = command === "propose" ? await ensureProposedChange(ctx, openSpecArgs) : await requireChangeId(ctx);
|
|
2095
|
+
const artifactId = command === "continue" ? await resolveArtifactId(ctx, changeId, openSpecArgs[0]) : await resolveArtifactId(ctx, changeId);
|
|
2096
|
+
runState.graphContext = await buildWorkflowGraphContext(ctx, {
|
|
2097
|
+
command,
|
|
2098
|
+
args: artifactId ? [artifactId, "--change", changeId] : ["--change", changeId],
|
|
2099
|
+
changeId
|
|
2100
|
+
});
|
|
2101
|
+
const instructions = await runInstructions(ctx, changeId, artifactId);
|
|
2102
|
+
await syncWorkflowState(ctx, command, changeId, {
|
|
2103
|
+
openSpecCommand: "instructions",
|
|
2104
|
+
openSpecArgs: [artifactId, "--change", changeId],
|
|
2105
|
+
exitCode: instructions.exitCode
|
|
2106
|
+
});
|
|
2107
|
+
const status = await readOpenSpecStatus(ctx, changeId);
|
|
2108
|
+
ctx.output.result({
|
|
2109
|
+
ok: true,
|
|
2110
|
+
command,
|
|
2111
|
+
summary: `fet ${command} prepared OpenSpec artifact "${artifactId}" for change "${changeId}".`,
|
|
2112
|
+
warnings: runState.graphContext?.warnings,
|
|
2113
|
+
nextSteps: [
|
|
2114
|
+
`Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
|
|
2115
|
+
`Run fet passthrough status --change ${changeId}`,
|
|
2116
|
+
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId}`
|
|
2117
|
+
],
|
|
2118
|
+
data: {
|
|
2119
|
+
changeId,
|
|
2120
|
+
artifactId,
|
|
2121
|
+
instructions: instructions.data,
|
|
2122
|
+
status,
|
|
2123
|
+
graphContext: runState.graphContext
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
async function ensureProposedChange(ctx, args) {
|
|
2129
|
+
if (ctx.changeId) {
|
|
2130
|
+
return ctx.changeId;
|
|
2131
|
+
}
|
|
2132
|
+
const existing = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
2133
|
+
const explicit = args[0];
|
|
2134
|
+
if (!explicit) {
|
|
2135
|
+
throw new FetError({
|
|
2136
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
2137
|
+
message: "A change id or short description is required for fet propose.",
|
|
2138
|
+
suggestedCommand: "fet propose <change-id-or-description>"
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
const input = args.join(" ").trim();
|
|
2142
|
+
const changeId = isKebabId(input) ? input : toKebabId(input);
|
|
2143
|
+
if (!changeId) {
|
|
2144
|
+
throw new FetError({
|
|
2145
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
2146
|
+
message: "Unable to derive a valid OpenSpec change id from the proposal input.",
|
|
2147
|
+
details: { input },
|
|
2148
|
+
suggestedCommand: "fet propose <kebab-case-change-id>"
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
if (existing.changes.includes(changeId)) {
|
|
2152
|
+
return changeId;
|
|
2153
|
+
}
|
|
2154
|
+
const createArgs = ["change", changeId];
|
|
2155
|
+
if (input && input !== changeId) {
|
|
2156
|
+
createArgs.push("--description", input);
|
|
2157
|
+
}
|
|
2158
|
+
const result = await ctx.openSpec.run("new", createArgs, { cwd: ctx.projectRoot, stdio: "pipe" });
|
|
2159
|
+
if (result.exitCode !== 0) {
|
|
2160
|
+
throw new FetError({
|
|
2161
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
2162
|
+
message: "OpenSpec new change failed.",
|
|
2163
|
+
details: result,
|
|
2164
|
+
recoverable: true
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
return changeId;
|
|
2168
|
+
}
|
|
2169
|
+
async function resolveArtifactId(ctx, changeId, requestedArtifactId) {
|
|
2170
|
+
const status = await readOpenSpecStatus(ctx, changeId);
|
|
2171
|
+
const artifacts = status.artifacts ?? [];
|
|
2172
|
+
if (requestedArtifactId) {
|
|
2173
|
+
const requested = artifacts.find((artifact) => artifact.id === requestedArtifactId);
|
|
2174
|
+
if (!requested) {
|
|
2175
|
+
throw new FetError({
|
|
2176
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
2177
|
+
message: `OpenSpec artifact "${requestedArtifactId}" was not found for change "${changeId}".`,
|
|
2178
|
+
details: { changeId, requestedArtifactId, artifacts: artifacts.map((artifact) => artifact.id) },
|
|
2179
|
+
suggestedCommand: `fet passthrough status --change ${changeId} --json`
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
if (requested.status !== "ready" && requested.status !== "complete") {
|
|
2183
|
+
throw new FetError({
|
|
2184
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
2185
|
+
message: `OpenSpec artifact "${requestedArtifactId}" is ${requested.status}, not ready.`,
|
|
2186
|
+
details: { changeId, requestedArtifactId, missingDeps: requested.missingDeps },
|
|
2187
|
+
suggestedCommand: `fet continue --change ${changeId}`
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
return requestedArtifactId;
|
|
2191
|
+
}
|
|
2192
|
+
const next = artifacts.find((artifact) => artifact.status === "ready");
|
|
2193
|
+
if (!next) {
|
|
2194
|
+
throw new FetError({
|
|
2195
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
2196
|
+
message: `No ready OpenSpec artifact was found for change "${changeId}".`,
|
|
2197
|
+
details: { changeId, artifacts },
|
|
2198
|
+
suggestedCommand: status.isComplete ? `fet apply --change ${changeId}` : `fet passthrough status --change ${changeId} --json`
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
return next.id;
|
|
2202
|
+
}
|
|
2203
|
+
async function readOpenSpecStatus(ctx, changeId) {
|
|
2204
|
+
const result = await ctx.openSpec.run("status", ["--change", changeId, "--json"], { cwd: ctx.projectRoot, stdio: "pipe" });
|
|
2205
|
+
if (result.exitCode !== 0) {
|
|
2206
|
+
throw new FetError({
|
|
2207
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
2208
|
+
message: "OpenSpec status failed.",
|
|
2209
|
+
details: result,
|
|
2210
|
+
recoverable: true
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
try {
|
|
2214
|
+
return parseOpenSpecJson(result.stdout ?? "{}");
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
throw new FetError({
|
|
2217
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
2218
|
+
message: "Unable to parse OpenSpec status JSON.",
|
|
2219
|
+
details: { stdout: result.stdout, stderr: result.stderr },
|
|
2220
|
+
cause: error
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
async function runInstructions(ctx, changeId, artifactId) {
|
|
2225
|
+
const args = [artifactId, "--change", changeId, ...ctx.json ? ["--json"] : []];
|
|
2226
|
+
const result = await ctx.openSpec.run("instructions", args, { cwd: ctx.projectRoot, stdio: "pipe" });
|
|
2227
|
+
if (result.stdout && !ctx.json) {
|
|
2228
|
+
process.stdout.write(result.stdout.endsWith("\n") ? result.stdout : `${result.stdout}
|
|
2229
|
+
`);
|
|
2230
|
+
}
|
|
2231
|
+
if (result.stderr && !ctx.json) {
|
|
2232
|
+
process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}
|
|
2233
|
+
`);
|
|
2234
|
+
}
|
|
2235
|
+
if (result.exitCode !== 0) {
|
|
2236
|
+
throw new FetError({
|
|
2237
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
2238
|
+
message: "OpenSpec instructions failed.",
|
|
2239
|
+
details: result,
|
|
2240
|
+
recoverable: true
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
if (!ctx.json) {
|
|
2244
|
+
return { exitCode: result.exitCode, data: null };
|
|
2245
|
+
}
|
|
2246
|
+
try {
|
|
2247
|
+
return { exitCode: result.exitCode, data: parseOpenSpecJson(result.stdout ?? "{}") };
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
throw new FetError({
|
|
2250
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
2251
|
+
message: "Unable to parse OpenSpec instructions JSON.",
|
|
2252
|
+
details: { stdout: result.stdout, stderr: result.stderr },
|
|
2253
|
+
cause: error
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
async function syncWorkflowState(ctx, command, changeId, lastOpenSpecCommand) {
|
|
2258
|
+
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
2259
|
+
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
2260
|
+
state.openChangeIds = inspection.changes;
|
|
2261
|
+
if (inspection.changes.includes(changeId)) {
|
|
2262
|
+
state.activeChangeId = changeId;
|
|
2263
|
+
}
|
|
2264
|
+
await ctx.stateStore.writeGlobal(state);
|
|
2265
|
+
if (!inspection.changes.includes(changeId)) {
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
2269
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
|
|
2270
|
+
changeState.currentPhase = "propose";
|
|
2271
|
+
changeState.phases.propose = {
|
|
2272
|
+
status: "in_progress",
|
|
2273
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2274
|
+
};
|
|
2275
|
+
changeState.lastOpenSpecCommand = {
|
|
2276
|
+
command: lastOpenSpecCommand.openSpecCommand,
|
|
2277
|
+
args: lastOpenSpecCommand.openSpecArgs,
|
|
2278
|
+
exitCode: lastOpenSpecCommand.exitCode,
|
|
2279
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2280
|
+
};
|
|
2281
|
+
changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
|
|
2282
|
+
changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2283
|
+
await ctx.stateStore.writeChange(changeState);
|
|
2284
|
+
}
|
|
2285
|
+
function resolveOutputPath(status, artifactId) {
|
|
2286
|
+
return status.artifacts?.find((artifact) => artifact.id === artifactId)?.outputPath ?? `${artifactId}.md`;
|
|
2287
|
+
}
|
|
2288
|
+
function isKebabId(value) {
|
|
2289
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
2290
|
+
}
|
|
2291
|
+
function toKebabId(value) {
|
|
2292
|
+
return value.trim().toLowerCase().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2293
|
+
}
|
|
2294
|
+
function parseOpenSpecJson(stdout) {
|
|
2295
|
+
const trimmed = stdout.trim();
|
|
2296
|
+
const objectStart = trimmed.indexOf("{");
|
|
2297
|
+
const arrayStart = trimmed.indexOf("[");
|
|
2298
|
+
const starts = [objectStart, arrayStart].filter((index) => index >= 0);
|
|
2299
|
+
if (!starts.length) {
|
|
2300
|
+
return JSON.parse(trimmed || "{}");
|
|
2301
|
+
}
|
|
2302
|
+
return JSON.parse(trimmed.slice(Math.min(...starts)));
|
|
2303
|
+
}
|
|
2081
2304
|
async function assertOpenSpecCommandSupported(ctx, openSpecCommand, fetCommand) {
|
|
2082
2305
|
const capabilities = await ctx.openSpec.getCapabilities();
|
|
2083
2306
|
if (capabilities.commands.includes(openSpecCommand)) {
|
|
@@ -4214,6 +4437,10 @@ var DefaultOpenSpecAdapter = class {
|
|
|
4214
4437
|
function parseCommands(help) {
|
|
4215
4438
|
const known = [
|
|
4216
4439
|
"init",
|
|
4440
|
+
"update",
|
|
4441
|
+
"list",
|
|
4442
|
+
"view",
|
|
4443
|
+
"change",
|
|
4217
4444
|
"explore",
|
|
4218
4445
|
"propose",
|
|
4219
4446
|
"new",
|
|
@@ -4224,7 +4451,18 @@ function parseCommands(help) {
|
|
|
4224
4451
|
"sync",
|
|
4225
4452
|
"archive",
|
|
4226
4453
|
"bulk-archive",
|
|
4227
|
-
"onboard"
|
|
4454
|
+
"onboard",
|
|
4455
|
+
"spec",
|
|
4456
|
+
"config",
|
|
4457
|
+
"schema",
|
|
4458
|
+
"validate",
|
|
4459
|
+
"show",
|
|
4460
|
+
"feedback",
|
|
4461
|
+
"completion",
|
|
4462
|
+
"status",
|
|
4463
|
+
"instructions",
|
|
4464
|
+
"templates",
|
|
4465
|
+
"schemas"
|
|
4228
4466
|
];
|
|
4229
4467
|
return known.filter((command) => new RegExp(`\\b${escapeRegExp(command)}\\b`).test(help));
|
|
4230
4468
|
}
|