@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 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
  }