@shanvit7/poiesis 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.
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/index.ts +534 -0
- package/package.json +59 -0
- package/prompts/implement.md +21 -0
- package/prompts/plan.md +10 -0
- package/prompts/prereq.md +15 -0
- package/prompts/theory.md +32 -0
- package/prompts/write-tests.md +8 -0
- package/skills/agent-browser/SKILL.md +143 -0
- package/src/chapter.ts +624 -0
- package/src/config.ts +15 -0
- package/src/onboarding.ts +57 -0
- package/src/progress.ts +94 -0
- package/src/project.ts +311 -0
- package/src/steps.ts +66 -0
- package/src/types.ts +67 -0
- package/src/utils.ts +164 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
import type { ChapterState, ChapterStep } from "./types.ts"
|
|
5
|
+
|
|
6
|
+
export const slugify = (s: string): string =>
|
|
7
|
+
s
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
10
|
+
.replace(/^-+|-+$/g, "")
|
|
11
|
+
.slice(0, 40)
|
|
12
|
+
|
|
13
|
+
export const expandHome = (p: string): string => p.replace(/^~/, process.env.HOME ?? "")
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scan $HOME for directories that actually exist — used to build ask_user_question options
|
|
17
|
+
* for the project location picker. Returns at most 4 (the tool's max per question).
|
|
18
|
+
*/
|
|
19
|
+
export const scanHomeDirs = (): { label: string; path: string }[] => {
|
|
20
|
+
const home = process.env.HOME ?? ""
|
|
21
|
+
const candidates = ["Desktop", "projects", "dev", "code", "Documents", "workspace", "src"]
|
|
22
|
+
return candidates
|
|
23
|
+
.filter((name) => existsSync(`${home}/${name}`))
|
|
24
|
+
.slice(0, 4)
|
|
25
|
+
.map((name) => ({ label: name, path: `${home}/${name}` }))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const readJson = <T>(path: string): T => {
|
|
29
|
+
const full = expandHome(path)
|
|
30
|
+
return JSON.parse(readFileSync(full, "utf8")) as T
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const writeJson = (path: string, data: unknown): void => {
|
|
34
|
+
const full = expandHome(path)
|
|
35
|
+
mkdirSync(dirname(full), { recursive: true })
|
|
36
|
+
writeFileSync(full, JSON.stringify(data, null, 2))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const exists = (path: string): boolean => existsSync(expandHome(path))
|
|
40
|
+
|
|
41
|
+
export const run = (cmd: string, cwd?: string): string => {
|
|
42
|
+
try {
|
|
43
|
+
return execSync(cmd, {
|
|
44
|
+
cwd: cwd ? expandHome(cwd) : undefined,
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
47
|
+
}).trim()
|
|
48
|
+
} catch (e: unknown) {
|
|
49
|
+
const err = e as { stderr?: Buffer; message?: string }
|
|
50
|
+
throw new Error(`Command failed: ${cmd}\n${err.stderr?.toString() ?? err.message ?? ""}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const ensureDir = (path: string): void => {
|
|
55
|
+
mkdirSync(expandHome(path), { recursive: true })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Chapter step state ──────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export const chapterStatePath = (projectDir: string, n: number): string =>
|
|
61
|
+
join(expandHome(projectDir), ".poiesis", "chapters", `chapter-${n}.state.json`)
|
|
62
|
+
|
|
63
|
+
export const readChapterState = (projectDir: string, n: number): ChapterState | null => {
|
|
64
|
+
const p = chapterStatePath(projectDir, n)
|
|
65
|
+
if (!existsSync(p)) return null
|
|
66
|
+
return readJson<ChapterState>(p)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Deep-merges patch into existing state, or creates from defaults. */
|
|
70
|
+
export const writeChapterState = (
|
|
71
|
+
projectDir: string,
|
|
72
|
+
n: number,
|
|
73
|
+
patch: Partial<ChapterState>
|
|
74
|
+
): void => {
|
|
75
|
+
const existing = readChapterState(projectDir, n) ?? {
|
|
76
|
+
step: "prereq" as ChapterStep,
|
|
77
|
+
prereqResult: null,
|
|
78
|
+
testsFile: null,
|
|
79
|
+
testsPlan: [],
|
|
80
|
+
testsPass: false,
|
|
81
|
+
startedAt: new Date().toISOString(),
|
|
82
|
+
}
|
|
83
|
+
writeJson(chapterStatePath(projectDir, n), { ...existing, ...patch })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find a poiesis project directory that has an active .progress.json.
|
|
88
|
+
*
|
|
89
|
+
* Two-pass:
|
|
90
|
+
* 1. cwd itself is the project root (user is already inside it)
|
|
91
|
+
* 2. scan immediate children of cwd (user is in the parent)
|
|
92
|
+
*/
|
|
93
|
+
export const findActiveProject = (cwd: string): string | null => {
|
|
94
|
+
const base = expandHome(cwd)
|
|
95
|
+
|
|
96
|
+
// Pass 1: cwd is the project itself
|
|
97
|
+
if (existsSync(join(base, ".poiesis", "chapters", ".progress.json"))) return base
|
|
98
|
+
|
|
99
|
+
// Pass 2: scan one level down
|
|
100
|
+
let entries: import("node:fs").Dirent[]
|
|
101
|
+
try {
|
|
102
|
+
entries = readdirSync(base, { withFileTypes: true })
|
|
103
|
+
} catch {
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
if (!e.isDirectory()) continue
|
|
108
|
+
if (existsSync(join(base, e.name, ".poiesis", "chapters", ".progress.json")))
|
|
109
|
+
return join(base, e.name)
|
|
110
|
+
}
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ponytail: self-check
|
|
115
|
+
if (process.argv[1]?.endsWith("utils.ts")) {
|
|
116
|
+
import("node:os").then(({ tmpdir }) => {
|
|
117
|
+
import("node:fs").then(({ mkdirSync, writeFileSync, rmSync }) => {
|
|
118
|
+
const s = slugify("Build a Rust HTTP Server from Scratch!!")
|
|
119
|
+
console.assert(s === "build-a-rust-http-server-from-scratch", `slugify failed: ${s}`)
|
|
120
|
+
|
|
121
|
+
// Pass 2: cwd is the parent, project is a child dir
|
|
122
|
+
const tmp = join(tmpdir(), `poiesis-utils-${Date.now()}`)
|
|
123
|
+
const projDir = join(tmp, "my-project", ".poiesis", "chapters")
|
|
124
|
+
mkdirSync(projDir, { recursive: true })
|
|
125
|
+
writeFileSync(join(projDir, ".progress.json"), "{}")
|
|
126
|
+
const found = findActiveProject(tmp)
|
|
127
|
+
console.assert(found === join(tmp, "my-project"), `pass-2 failed: ${found}`)
|
|
128
|
+
|
|
129
|
+
// Pass 1: cwd IS the project (user ran /poiesis from inside it)
|
|
130
|
+
const insideFound = findActiveProject(join(tmp, "my-project"))
|
|
131
|
+
console.assert(insideFound === join(tmp, "my-project"), `pass-1 failed: ${insideFound}`)
|
|
132
|
+
|
|
133
|
+
// null when nothing found
|
|
134
|
+
const empty = join(tmpdir(), `poiesis-empty-${Date.now()}`)
|
|
135
|
+
mkdirSync(empty)
|
|
136
|
+
console.assert(findActiveProject(empty) === null, "should return null for empty dir")
|
|
137
|
+
|
|
138
|
+
// chapterState round-trip
|
|
139
|
+
writeChapterState(join(tmp, "my-project"), 1, { step: "theory", prereqResult: "primed" })
|
|
140
|
+
const st = readChapterState(join(tmp, "my-project"), 1)
|
|
141
|
+
console.assert(st?.step === "theory", `state step not persisted: ${st?.step}`)
|
|
142
|
+
console.assert(
|
|
143
|
+
st?.prereqResult === "primed",
|
|
144
|
+
`state prereqResult not persisted: ${st?.prereqResult}`
|
|
145
|
+
)
|
|
146
|
+
console.assert(
|
|
147
|
+
st?.testsFile === null,
|
|
148
|
+
`state testsFile should default null: ${st?.testsFile}`
|
|
149
|
+
)
|
|
150
|
+
// patch merges, does not clobber existing fields
|
|
151
|
+
writeChapterState(join(tmp, "my-project"), 1, { step: "plan" })
|
|
152
|
+
const st2 = readChapterState(join(tmp, "my-project"), 1)
|
|
153
|
+
console.assert(st2?.step === "plan", `patched step failed: ${st2?.step}`)
|
|
154
|
+
console.assert(
|
|
155
|
+
st2?.prereqResult === "primed",
|
|
156
|
+
`prereqResult should survive patch: ${st2?.prereqResult}`
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
rmSync(tmp, { recursive: true })
|
|
160
|
+
rmSync(empty, { recursive: true })
|
|
161
|
+
console.log("utils.ts: ok")
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
}
|