@nick848/fet 0.1.0 → 1.0.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 (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +101 -44
  3. package/dist/chunk-FZOVNHE7.js +104 -0
  4. package/dist/chunk-FZOVNHE7.js.map +1 -0
  5. package/dist/cli/index.js +1795 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/index.js +12 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +43 -49
  10. package/dist/apply.d.ts +0 -1
  11. package/dist/apply.js +0 -172
  12. package/dist/approval.d.ts +0 -2
  13. package/dist/approval.js +0 -26
  14. package/dist/atomic-write.d.ts +0 -5
  15. package/dist/atomic-write.js +0 -41
  16. package/dist/cli.d.ts +0 -2
  17. package/dist/cli.js +0 -178
  18. package/dist/doctor.d.ts +0 -1
  19. package/dist/doctor.js +0 -93
  20. package/dist/fingerprint.d.ts +0 -6
  21. package/dist/fingerprint.js +0 -77
  22. package/dist/hooks.d.ts +0 -12
  23. package/dist/hooks.js +0 -47
  24. package/dist/init.d.ts +0 -4
  25. package/dist/init.js +0 -47
  26. package/dist/opencode-skills.d.ts +0 -3
  27. package/dist/opencode-skills.js +0 -236
  28. package/dist/openspec.d.ts +0 -16
  29. package/dist/openspec.js +0 -73
  30. package/dist/paths.d.ts +0 -9
  31. package/dist/paths.js +0 -20
  32. package/dist/prompt.d.ts +0 -4
  33. package/dist/prompt.js +0 -30
  34. package/dist/scanner.d.ts +0 -23
  35. package/dist/scanner.js +0 -352
  36. package/dist/skills.d.ts +0 -3
  37. package/dist/skills.js +0 -142
  38. package/dist/state.d.ts +0 -17
  39. package/dist/state.js +0 -126
  40. package/dist/tasks.d.ts +0 -13
  41. package/dist/tasks.js +0 -69
  42. package/dist/types.d.ts +0 -38
  43. package/dist/types.js +0 -1
  44. package/dist/validate.d.ts +0 -1
  45. package/dist/validate.js +0 -150
  46. package/dist/verify.d.ts +0 -6
  47. package/dist/verify.js +0 -193
  48. package/dist/watch-paths.d.ts +0 -2
  49. package/dist/watch-paths.js +0 -70
  50. package/dist/workflow-hints.d.ts +0 -2
  51. package/dist/workflow-hints.js +0 -9
@@ -1,236 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import JSON5 from "json5";
4
- import { writeFileAtomicSync } from "./atomic-write.js";
5
- import { promptSkillConflict } from "./prompt.js";
6
- const OPENCODE_SKILLS = [
7
- {
8
- id: "fet-explore",
9
- description: "FET: run fet explore (openspec proxy)",
10
- body: "Run in terminal: `fet explore`",
11
- },
12
- {
13
- id: "fet-propose",
14
- description: "FET: run fet propose (openspec proxy)",
15
- body: "Run in terminal: `fet propose`",
16
- },
17
- {
18
- id: "fet-new",
19
- description: "FET: create change skeleton via fet new <id>",
20
- body: "Run in terminal: `fet new <change-id>`",
21
- },
22
- {
23
- id: "fet-continue",
24
- description: "FET: run fet continue",
25
- body: "Run in terminal: `fet continue`",
26
- },
27
- {
28
- id: "fet-ff",
29
- description: "FET: run fet ff",
30
- body: "Run in terminal: `fet ff`",
31
- },
32
- {
33
- id: "fet-apply",
34
- description: "FET: refresh apply-instructions and implement current task",
35
- body: "Run `fet apply` in the project terminal to refresh `openspec/changes/<change-id>/apply-instructions.md`, then follow that file.",
36
- },
37
- {
38
- id: "fet-validate",
39
- description: "FET: validate current apply task",
40
- body: "Run in terminal: `fet validate`",
41
- },
42
- {
43
- id: "fet-verify",
44
- description: "FET: final verify (manual or --auto)",
45
- body: "Run `fet verify` or `fet verify --auto` per team policy.",
46
- },
47
- {
48
- id: "fet-sync",
49
- description: "FET: sync delta specs (needs interactive TTY)",
50
- body: "Run `fet sync` in an interactive terminal after verify.",
51
- },
52
- {
53
- id: "fet-archive",
54
- description: "FET: archive a change after verify",
55
- body: "Run `fet archive <change-id>` after verify.",
56
- },
57
- {
58
- id: "fet-bulk-archive",
59
- description: "FET: bulk archive multiple changes",
60
- body: "Run `fet bulk-archive <id> [<id> ...]`",
61
- },
62
- {
63
- id: "fet-onboard",
64
- description: "FET: run fet onboard",
65
- body: "Run in terminal: `fet onboard`",
66
- },
67
- {
68
- id: "fet-update-context",
69
- description: "FET: refresh AGENTS.md and openspec config fet block",
70
- body: "Run in terminal: `fet update-context`",
71
- },
72
- {
73
- id: "fet-doctor",
74
- description: "FET: diagnose fet-state, openspec path, and tooling",
75
- body: "Run in terminal: `fet doctor`",
76
- },
77
- ];
78
- /** Paths merged into `instructions` (OpenCode config docs); includes Cursor rule for parity. */
79
- const FET_INSTRUCTION_ENTRIES = [
80
- "AGENTS.md",
81
- "openspec/config.yaml",
82
- ".cursor/rules/fet-context.mdc",
83
- ".opencode/instructions/fet-context.md",
84
- ];
85
- function skillMd(s) {
86
- return [
87
- "---",
88
- `name: ${s.id}`,
89
- `description: ${s.description}`,
90
- "metadata:",
91
- " fet: \"true\"",
92
- "---",
93
- "",
94
- s.body,
95
- "",
96
- ].join("\n");
97
- }
98
- function writeFetInstructionContext(cwd) {
99
- const dir = join(cwd, ".opencode", "instructions");
100
- mkdirSync(dir, { recursive: true });
101
- const p = join(dir, "fet-context.md");
102
- const body = [
103
- "# FET / OpenSpec — project instructions",
104
- "",
105
- "OpenCode loads this file via root `opencode.json` → `instructions` (see https://opencode.ai/docs/zh-cn/config/ ).",
106
- "",
107
- "## Before substantive edits",
108
- "",
109
- "1. Read `AGENTS.md` and `openspec/config.yaml`.",
110
- "2. If present, follow `.cursor/rules/fet-context.mdc` (same file as Cursor; listed in `opencode.json` → `instructions` for OpenCode).",
111
- "3. Use the **skill** tool to load FET workflow skills from `.opencode/skills/fet-*/SKILL.md` when needed (e.g. `fet-apply`, `fet-validate`).",
112
- "4. `fet` orchestrates around OpenSpec; terminal commands are `fet <subcommand>` unless you intentionally call `openspec` directly.",
113
- "",
114
- "## Token saving",
115
- "",
116
- "For trivial Q&A you may skip loading every instruction file; for code changes prefer full context.",
117
- "",
118
- ].join("\n");
119
- if (!existsSync(p)) {
120
- writeFileSync(p, body, "utf8");
121
- return;
122
- }
123
- const cur = readFileSync(p, "utf8");
124
- if (cur.includes("FET / OpenSpec"))
125
- return;
126
- writeFileSync(p, `${cur.trimEnd()}\n\n---\n\n${body}`, "utf8");
127
- }
128
- function resolveExistingOpenCodeConfigPath(cwd) {
129
- const json = join(cwd, "opencode.json");
130
- if (existsSync(json))
131
- return json;
132
- const jsonc = join(cwd, "opencode.jsonc");
133
- if (existsSync(jsonc))
134
- return jsonc;
135
- return null;
136
- }
137
- function defaultOpenCodeConfig() {
138
- return {
139
- $schema: "https://opencode.ai/config.json",
140
- permission: {
141
- skill: {
142
- "fet-*": "allow",
143
- },
144
- },
145
- instructions: [...FET_INSTRUCTION_ENTRIES],
146
- };
147
- }
148
- function mergeInstructions(cur) {
149
- const inst = cur.instructions;
150
- const arr = Array.isArray(inst) ? inst.map((x) => String(x)) : [];
151
- const set = new Set(arr);
152
- for (const p of FET_INSTRUCTION_ENTRIES) {
153
- if (!set.has(p)) {
154
- arr.push(p);
155
- set.add(p);
156
- }
157
- }
158
- return arr;
159
- }
160
- function mergeOpenCodeConfig(cwd) {
161
- writeFetInstructionContext(cwd);
162
- const existingPath = resolveExistingOpenCodeConfigPath(cwd);
163
- const targetPath = existingPath ?? join(cwd, "opencode.json");
164
- if (!existingPath) {
165
- writeFileAtomicSync(targetPath, `${JSON.stringify(defaultOpenCodeConfig(), null, 2)}\n`);
166
- return;
167
- }
168
- let raw;
169
- try {
170
- raw = readFileSync(existingPath, "utf8");
171
- }
172
- catch {
173
- console.warn(`[fet] could not read ${existingPath}`);
174
- return;
175
- }
176
- let cur;
177
- try {
178
- cur = JSON5.parse(raw);
179
- }
180
- catch {
181
- console.warn(`[fet] could not parse ${existingPath} — fix syntax or merge instructions / permission manually`);
182
- return;
183
- }
184
- if (existingPath.endsWith(".jsonc")) {
185
- console.warn(`[fet] Merging ${existingPath}: comments will be stripped on save (OpenCode supports JSONC; fet rewrites as formatted JSON).`);
186
- }
187
- cur.$schema = cur.$schema || "https://opencode.ai/config.json";
188
- const perm = typeof cur.permission === "object" && cur.permission !== null
189
- ? { ...cur.permission }
190
- : {};
191
- if (typeof perm.skill === "string") {
192
- console.warn(`[fet] permission.skill is a string ("${perm.skill}"); skipping fet-* merge — use an object map for skill patterns (see https://opencode.ai/docs/skills ).`);
193
- }
194
- else {
195
- const skillPerm = typeof perm.skill === "object" && perm.skill !== null
196
- ? { ...perm.skill }
197
- : {};
198
- skillPerm["fet-*"] = "allow";
199
- perm.skill = skillPerm;
200
- }
201
- cur.permission = perm;
202
- cur.instructions = mergeInstructions(cur);
203
- const out = `${JSON.stringify(cur, null, 2)}\n`;
204
- writeFileAtomicSync(targetPath, out);
205
- }
206
- export async function generateOpenCodeTooling(cwd, opts) {
207
- const root = join(cwd, ".opencode", "skills");
208
- mkdirSync(root, { recursive: true });
209
- for (const s of OPENCODE_SKILLS) {
210
- const dir = join(root, s.id);
211
- const filePath = join(dir, "SKILL.md");
212
- mkdirSync(dir, { recursive: true });
213
- if (!existsSync(filePath) || opts.force) {
214
- writeFileSync(filePath, skillMd(s), "utf8");
215
- continue;
216
- }
217
- const existing = readFileSync(filePath, "utf8");
218
- if (existing.includes("metadata:") && existing.includes("fet:")) {
219
- console.log(`[fet] skip OpenCode skill (already FET): ${filePath}`);
220
- continue;
221
- }
222
- const choice = await promptSkillConflict(filePath);
223
- if (choice === "skip") {
224
- console.log(`[fet] skipped OpenCode skill: ${filePath}`);
225
- continue;
226
- }
227
- if (choice === "merge") {
228
- const merged = `${existing.trimEnd()}\n\n---\n\n## FET (merged)\n\n${s.body}\n`;
229
- writeFileSync(filePath, merged, "utf8");
230
- continue;
231
- }
232
- writeFileSync(filePath, skillMd(s), "utf8");
233
- }
234
- mergeOpenCodeConfig(cwd);
235
- console.log(`[fet] OpenCode: skills → ${root}; config → opencode.json / opencode.jsonc + instructions (see opencode.ai/docs/zh-cn/config/)`);
236
- }
@@ -1,16 +0,0 @@
1
- /** Resolved absolute path to global `openspec` on PATH, or null (DESIGN 3.1, 14.2). */
2
- export declare function resolveOpenSpecExecutable(): string | null;
3
- /** How to spawn OpenSpec: direct binary or npx fallback (DESIGN 3.1). */
4
- export declare function getOpenSpecLaunch(): {
5
- command: string;
6
- prefixArgs: string[];
7
- /** For doctor / logging */
8
- displayPath: string;
9
- };
10
- export declare function spawnOpenSpec(args: string[], options?: {
11
- cwd?: string;
12
- inheritStdio?: boolean;
13
- }): Promise<number>;
14
- /** Best-effort version string for doctor (may use npx). */
15
- export declare function openSpecVersionLine(): string | null;
16
- export declare const FET_VERSION = "0.1.0";
package/dist/openspec.js DELETED
@@ -1,73 +0,0 @@
1
- import { execFileSync, spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { delimiter, join } from "node:path";
4
- function pathDirs() {
5
- const p = process.env.PATH ?? process.env.Path ?? "";
6
- return p.split(delimiter).filter(Boolean);
7
- }
8
- const WIN_EXTS = [".exe", ".cmd", ".bat", ".ps1", ""];
9
- function resolveOnPath(cmd) {
10
- const dirs = pathDirs();
11
- for (const d of dirs) {
12
- for (const ext of WIN_EXTS) {
13
- const candidate = join(d, cmd + ext);
14
- if (existsSync(candidate))
15
- return candidate;
16
- }
17
- }
18
- return null;
19
- }
20
- /** Resolved absolute path to global `openspec` on PATH, or null (DESIGN 3.1, 14.2). */
21
- export function resolveOpenSpecExecutable() {
22
- const direct = resolveOnPath(process.platform === "win32" ? "openspec.cmd" : "openspec");
23
- if (direct)
24
- return direct;
25
- return resolveOnPath("openspec");
26
- }
27
- function npxCmd() {
28
- return process.platform === "win32" ? "npx.cmd" : "npx";
29
- }
30
- /** How to spawn OpenSpec: direct binary or npx fallback (DESIGN 3.1). */
31
- export function getOpenSpecLaunch() {
32
- const direct = resolveOpenSpecExecutable();
33
- if (direct) {
34
- return { command: direct, prefixArgs: [], displayPath: direct };
35
- }
36
- return {
37
- command: npxCmd(),
38
- prefixArgs: ["--yes", "@fission-ai/openspec"],
39
- displayPath: `${npxCmd()} --yes @fission-ai/openspec`,
40
- };
41
- }
42
- export function spawnOpenSpec(args, options = {}) {
43
- const { command, prefixArgs } = getOpenSpecLaunch();
44
- const fullArgs = [...prefixArgs, ...args];
45
- const inheritStdio = options.inheritStdio ?? true;
46
- return new Promise((resolve, reject) => {
47
- const child = spawn(command, fullArgs, {
48
- cwd: options.cwd,
49
- stdio: inheritStdio ? "inherit" : "pipe",
50
- shell: false,
51
- env: process.env,
52
- });
53
- child.on("error", reject);
54
- child.on("close", (code) => resolve(code ?? 1));
55
- });
56
- }
57
- /** Best-effort version string for doctor (may use npx). */
58
- export function openSpecVersionLine() {
59
- try {
60
- const { command, prefixArgs } = getOpenSpecLaunch();
61
- const out = execFileSync(command, [...prefixArgs, "-V"], {
62
- encoding: "utf8",
63
- stdio: ["ignore", "pipe", "ignore"],
64
- timeout: 60_000,
65
- maxBuffer: 2_000_000,
66
- });
67
- return out.trim().split("\n")[0] ?? null;
68
- }
69
- catch {
70
- return null;
71
- }
72
- }
73
- export const FET_VERSION = "0.1.0";
package/dist/paths.d.ts DELETED
@@ -1,9 +0,0 @@
1
- export declare const OPENSPEC_DIR = "openspec";
2
- export declare const GLOBAL_STATE_FILE: string;
3
- export declare const CONFIG_FILE: string;
4
- export declare const CHANGES_DIR: string;
5
- export declare function changeDir(changeId: string): string;
6
- export declare function changeStatePath(changeId: string): string;
7
- export declare function applyInstructionsPath(changeId: string): string;
8
- export declare function verifyInstructionsPath(changeId: string): string;
9
- export declare function tasksPath(changeId: string): string;
package/dist/paths.js DELETED
@@ -1,20 +0,0 @@
1
- import { join } from "node:path";
2
- export const OPENSPEC_DIR = "openspec";
3
- export const GLOBAL_STATE_FILE = join(OPENSPEC_DIR, "fet-state.json");
4
- export const CONFIG_FILE = join(OPENSPEC_DIR, "config.yaml");
5
- export const CHANGES_DIR = join(OPENSPEC_DIR, "changes");
6
- export function changeDir(changeId) {
7
- return join(OPENSPEC_DIR, "changes", changeId);
8
- }
9
- export function changeStatePath(changeId) {
10
- return join(changeDir(changeId), "fet-state.json");
11
- }
12
- export function applyInstructionsPath(changeId) {
13
- return join(changeDir(changeId), "apply-instructions.md");
14
- }
15
- export function verifyInstructionsPath(changeId) {
16
- return join(changeDir(changeId), "VerifyInstructions.txt");
17
- }
18
- export function tasksPath(changeId) {
19
- return join(changeDir(changeId), "tasks.md");
20
- }
package/dist/prompt.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export declare function confirmAutoRun(message: string): Promise<boolean>;
2
- export type SkillConflictChoice = "overwrite" | "merge" | "skip";
3
- /** DESIGN 9.3: interactive conflict for Cursor skills */
4
- export declare function promptSkillConflict(path: string): Promise<SkillConflictChoice>;
package/dist/prompt.js DELETED
@@ -1,30 +0,0 @@
1
- import * as readline from "node:readline/promises";
2
- export async function confirmAutoRun(message) {
3
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4
- try {
5
- const ans = (await rl.question(`${message} (type yes to approve): `)).trim().toLowerCase();
6
- return ans === "yes";
7
- }
8
- finally {
9
- rl.close();
10
- }
11
- }
12
- /** DESIGN 9.3: interactive conflict for Cursor skills */
13
- export async function promptSkillConflict(path) {
14
- if (!process.stdin.isTTY)
15
- return "skip";
16
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
17
- try {
18
- const ans = (await rl.question(`[fet] ${path} exists. Overwrite / Merge / Skip? [o/m/S] `))
19
- .trim()
20
- .toLowerCase();
21
- if (ans === "o" || ans === "overwrite")
22
- return "overwrite";
23
- if (ans === "m" || ans === "merge")
24
- return "merge";
25
- return "skip";
26
- }
27
- finally {
28
- rl.close();
29
- }
30
- }
package/dist/scanner.d.ts DELETED
@@ -1,23 +0,0 @@
1
- export interface ScanResult {
2
- projectName: string;
3
- description: string;
4
- frameworkGuess: string;
5
- languageGuess: string;
6
- bundlerGuess: string;
7
- stateManagementGuess: string;
8
- uiLibraryGuess: string;
9
- readmeExcerpt: string;
10
- scriptsTable: string;
11
- testCommand: string;
12
- eslintHint: string;
13
- tsconfigHint: string;
14
- routeFiles: string;
15
- i18nHint: string;
16
- projectTree: string;
17
- warnings: string[];
18
- }
19
- export declare function scanProject(cwd: string): ScanResult;
20
- export declare function renderAgents(scan: ScanResult): string;
21
- export declare function writeAgentsMd(cwd: string, scan: ScanResult): void;
22
- export declare function mergeConfigYaml(cwd: string, scan: ScanResult): void;
23
- export declare function runUpdateContext(cwd: string): void;