@fabrika/builder 0.2.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/BUILD-CONTRACT.md +41 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/cli/src/bin.ts +66 -0
- package/cli/src/build-status.ts +109 -0
- package/cli/src/catalog.ts +42 -0
- package/cli/src/commands/advance.ts +73 -0
- package/cli/src/commands/build.ts +215 -0
- package/cli/src/commands/ensure-daemon.ts +25 -0
- package/cli/src/commands/gate.ts +165 -0
- package/cli/src/commands/induce.ts +61 -0
- package/cli/src/commands/init.ts +91 -0
- package/cli/src/commands/next.ts +31 -0
- package/cli/src/config.ts +47 -0
- package/cli/src/context.ts +28 -0
- package/cli/src/daemon-client.ts +280 -0
- package/cli/src/daemon-lifecycle.ts +171 -0
- package/cli/src/derive.ts +109 -0
- package/cli/src/index.ts +38 -0
- package/cli/src/induce.ts +151 -0
- package/cli/src/inject-skills.ts +313 -0
- package/cli/src/issues.ts +75 -0
- package/cli/src/layout.ts +124 -0
- package/cli/src/packet.ts +108 -0
- package/cli/src/phase-state.ts +19 -0
- package/cli/src/phases.ts +131 -0
- package/cli/src/skeletons.ts +104 -0
- package/daemon/src/bin.ts +70 -0
- package/daemon/src/build-stream.ts +38 -0
- package/daemon/src/handlers.ts +472 -0
- package/daemon/src/index.ts +123 -0
- package/daemon/src/paths.ts +30 -0
- package/daemon/src/phases.ts +221 -0
- package/daemon/src/static.ts +72 -0
- package/decisions/README.md +133 -0
- package/decisions/patterns/app-deploy.md +55 -0
- package/decisions/patterns/auth-password-kv.md +45 -0
- package/decisions/patterns/count-store.md +44 -0
- package/decisions/patterns/di-container.md +47 -0
- package/decisions/patterns/feature-architecture.md +48 -0
- package/decisions/patterns/lib-publish.md +46 -0
- package/decisions/patterns/monorepo.md +47 -0
- package/decisions/patterns/named-seeds.md +45 -0
- package/decisions/patterns/observability.md +45 -0
- package/decisions/patterns/single-worker-spa.md +74 -0
- package/decisions/patterns/structured-logging.md +49 -0
- package/decisions/patterns/typed-worker-rpc.md +87 -0
- package/decisions/patterns/vozka-app-deploy.md +74 -0
- package/decisions/patterns/write-split.md +47 -0
- package/decisions/technologies/analytics-engine.md +44 -0
- package/decisions/technologies/bindx.md +55 -0
- package/decisions/technologies/bun.md +51 -0
- package/decisions/technologies/buzola.md +80 -0
- package/decisions/technologies/cloudflare-access.md +50 -0
- package/decisions/technologies/cloudflare-vite-plugin.md +43 -0
- package/decisions/technologies/cloudflare.md +50 -0
- package/decisions/technologies/contember.md +65 -0
- package/decisions/technologies/cron-triggers.md +46 -0
- package/decisions/technologies/d1.md +51 -0
- package/decisions/technologies/durable-objects.md +44 -0
- package/decisions/technologies/kv.md +42 -0
- package/decisions/technologies/lopata.md +55 -0
- package/decisions/technologies/nuasite.md +50 -0
- package/decisions/technologies/oblaka.md +48 -0
- package/decisions/technologies/opice.md +40 -0
- package/decisions/technologies/pletivo.md +53 -0
- package/decisions/technologies/postgres.md +47 -0
- package/decisions/technologies/preact.md +44 -0
- package/decisions/technologies/propustka.md +137 -0
- package/decisions/technologies/queue.md +55 -0
- package/decisions/technologies/r2.md +41 -0
- package/decisions/technologies/react-router.md +47 -0
- package/decisions/technologies/react.md +37 -0
- package/decisions/technologies/roj.md +51 -0
- package/decisions/technologies/sentry.md +49 -0
- package/decisions/technologies/tailwind.md +45 -0
- package/decisions/technologies/vite.md +47 -0
- package/decisions/technologies/vozka.md +122 -0
- package/decisions/technologies/workers-ai.md +59 -0
- package/decisions/technologies/wrangler.md +41 -0
- package/decisions/tooling/deptective.md +45 -0
- package/decisions/tooling/edvabe.md +45 -0
- package/index.ts +2 -0
- package/package.json +51 -0
- package/skill/SKILL.md +76 -0
- package/skill/agents/build-orchestrator.md +170 -0
- package/skill/agents/flows-author.md +65 -0
- package/skill/agents/implementer.md +37 -0
- package/skill/agents/openai.yaml +4 -0
- package/skill/agents/opice-skeleton-author.md +68 -0
- package/skill/gates/brief.jsx +20 -0
- package/skill/gates/build.jsx +20 -0
- package/skill/gates/design.jsx +22 -0
- package/skill/gates/flows.jsx +14 -0
- package/skill/gates/review-grid.jsx +32 -0
- package/skill/gates/tech.jsx +17 -0
- package/skill/gates/vision.jsx +27 -0
- package/stacks/README.md +44 -0
- package/stacks/cf-fat.md +64 -0
- package/stacks/cf-internal-app.md +125 -0
- package/stacks/cf-lightweight.md +54 -0
- package/stacks/contember-app.md +53 -0
- package/stacks/content-site.md +45 -0
- package/ui/dist/assets/annotations-runtime.js +1 -0
- package/ui/dist/assets/client.css +2 -0
- package/ui/dist/assets/client.fhjppkvr.js +214 -0
- package/ui/dist/assets/components.js +63 -0
- package/ui/dist/assets/preact-compat.js +1 -0
- package/ui/dist/assets/react-jsx-dev-runtime.js +3 -0
- package/ui/dist/assets/react-jsx-runtime.js +3 -0
- package/ui/dist/assets/runtime.js +1 -0
- package/ui/dist/index.html +36 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BASE_URL } from '../config'
|
|
2
|
+
import { health } from '../daemon-client'
|
|
3
|
+
import { ensureDaemon } from '../daemon-lifecycle'
|
|
4
|
+
|
|
5
|
+
export async function cmdEnsureDaemon(_argv: string[]): Promise<number> {
|
|
6
|
+
const up = await ensureDaemon()
|
|
7
|
+
if (!up) {
|
|
8
|
+
console.log(JSON.stringify({ ok: false, running: false, baseUrl: BASE_URL }))
|
|
9
|
+
return 1
|
|
10
|
+
}
|
|
11
|
+
const info = await health()
|
|
12
|
+
console.log(
|
|
13
|
+
JSON.stringify({
|
|
14
|
+
ok: true,
|
|
15
|
+
running: true,
|
|
16
|
+
baseUrl: BASE_URL,
|
|
17
|
+
version: info?.version ?? null,
|
|
18
|
+
pid: info?.pid ?? null,
|
|
19
|
+
dataDir: info?.dataDir ?? null,
|
|
20
|
+
sessionsDir: info?.sessionsDir ?? null,
|
|
21
|
+
sessions: info?.sessions ?? null,
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
return 0
|
|
25
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'fs'
|
|
2
|
+
import { extname, resolve } from 'path'
|
|
3
|
+
import { parseArgs } from 'util'
|
|
4
|
+
import { BASE_URL, TIMEOUT_MS } from '../config'
|
|
5
|
+
import { resolveContext } from '../context'
|
|
6
|
+
import { isDaemonRunning, peekFeedback, pushPlan, waitForFeedback } from '../daemon-client'
|
|
7
|
+
import { ensureDaemon, openBrowser } from '../daemon-lifecycle'
|
|
8
|
+
|
|
9
|
+
export async function cmdGate(argv: string[]): Promise<number> {
|
|
10
|
+
const sub = argv[0]
|
|
11
|
+
const rest = argv.slice(1)
|
|
12
|
+
if (sub === 'open') return gateOpen(rest)
|
|
13
|
+
if (sub === 'wait') return gateWait(rest)
|
|
14
|
+
console.error(`[fabrika] unknown gate subcommand: ${sub ?? '(none)'}. Use \`gate open\` or \`gate wait\`.`)
|
|
15
|
+
return 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function gateOpen(argv: string[]): Promise<number> {
|
|
19
|
+
const { values, positionals } = parseArgs({
|
|
20
|
+
args: argv,
|
|
21
|
+
options: {
|
|
22
|
+
view: { type: 'string' },
|
|
23
|
+
dir: { type: 'string' },
|
|
24
|
+
label: { type: 'string' },
|
|
25
|
+
session: { type: 'string' },
|
|
26
|
+
root: { type: 'string' },
|
|
27
|
+
},
|
|
28
|
+
allowPositionals: true,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const name = positionals[0]
|
|
32
|
+
if (!name) {
|
|
33
|
+
console.error('[fabrika] gate open requires a <name>. Usage: fabrika builder gate open <name> --view <file.jsx>')
|
|
34
|
+
return 1
|
|
35
|
+
}
|
|
36
|
+
if (!values.view && !values.dir) {
|
|
37
|
+
console.error('[fabrika] gate open requires exactly one of --view <file.jsx> or --dir <jsxDir>.')
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
if (values.view && values.dir) {
|
|
41
|
+
console.error('[fabrika] gate open accepts either --view or --dir, not both.')
|
|
42
|
+
return 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ctx = resolveContext({ dir: values.root, session: values.session })
|
|
46
|
+
|
|
47
|
+
let source: { view: string } | { directory: string }
|
|
48
|
+
if (values.dir) {
|
|
49
|
+
const directory = resolve(values.dir)
|
|
50
|
+
if (!existsSync(directory) || !statSync(directory).isDirectory()) {
|
|
51
|
+
console.error(`[fabrika] --dir not a directory: ${directory}`)
|
|
52
|
+
return 1
|
|
53
|
+
}
|
|
54
|
+
source = { directory }
|
|
55
|
+
} else {
|
|
56
|
+
const viewPath = resolve(values.view!)
|
|
57
|
+
if (!existsSync(viewPath)) {
|
|
58
|
+
console.error(`[fabrika] --view file not found: ${viewPath}`)
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
if (!statSync(viewPath).isFile()) {
|
|
62
|
+
console.error(`[fabrika] --view must be a file; use --dir for a directory: ${viewPath}`)
|
|
63
|
+
return 1
|
|
64
|
+
}
|
|
65
|
+
if (extname(viewPath).toLowerCase() !== '.jsx') {
|
|
66
|
+
console.error(`[fabrika] --view must point to a .jsx file: ${viewPath}`)
|
|
67
|
+
return 1
|
|
68
|
+
}
|
|
69
|
+
source = { view: viewPath }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const label = values.label ?? name
|
|
73
|
+
const browserUrl = `${BASE_URL}/s/${ctx.sessionId}`
|
|
74
|
+
|
|
75
|
+
const up = await ensureDaemon({ quiet: true })
|
|
76
|
+
if (!up) {
|
|
77
|
+
console.error(`[fabrika] daemon unavailable; cannot push gate "${name}".`)
|
|
78
|
+
console.log(
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
ok: false,
|
|
81
|
+
reachable: false,
|
|
82
|
+
gate: name,
|
|
83
|
+
...source,
|
|
84
|
+
browserUrl,
|
|
85
|
+
hint: 'Start the daemon (fabrika builder ensure-daemon), then re-run gate open.',
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
return 0 // soft-fail so the loop can proceed offline / be tested
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result = await pushPlan(ctx.sessionId, {
|
|
92
|
+
...source,
|
|
93
|
+
projectRoot: ctx.repoRoot,
|
|
94
|
+
label,
|
|
95
|
+
gate: name,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (!result.reachable || !result.ok) {
|
|
99
|
+
console.error(`[fabrika] gate push failed: ${result.error ?? 'compile error'}`)
|
|
100
|
+
if (result.errors) {
|
|
101
|
+
for (const [file, err] of Object.entries(result.errors)) {
|
|
102
|
+
console.error(` ${file}: ${err}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (result.isNew && result.browserUrl) {
|
|
109
|
+
openBrowser(result.browserUrl)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
ok: true,
|
|
115
|
+
browserUrl: result.browserUrl ?? browserUrl,
|
|
116
|
+
revision: result.revision,
|
|
117
|
+
gate: name,
|
|
118
|
+
sessionId: ctx.sessionId,
|
|
119
|
+
canvasFiles: result.canvasFiles,
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
return 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function gateWait(argv: string[]): Promise<number> {
|
|
126
|
+
const { values } = parseArgs({
|
|
127
|
+
args: argv,
|
|
128
|
+
options: {
|
|
129
|
+
gate: { type: 'string' },
|
|
130
|
+
timeout: { type: 'string' },
|
|
131
|
+
session: { type: 'string' },
|
|
132
|
+
root: { type: 'string' },
|
|
133
|
+
},
|
|
134
|
+
allowPositionals: true,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const ctx = resolveContext({ dir: values.root, session: values.session })
|
|
138
|
+
const timeoutMs = values.timeout ? parseInt(values.timeout, 10) : TIMEOUT_MS
|
|
139
|
+
|
|
140
|
+
const up = await ensureDaemon({ quiet: true })
|
|
141
|
+
if (!up) {
|
|
142
|
+
console.error('[fabrika] daemon unavailable; cannot wait for gate feedback.')
|
|
143
|
+
return 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const pending = await peekFeedback(ctx.sessionId)
|
|
147
|
+
if (pending !== null) {
|
|
148
|
+
process.stdout.write(pending)
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const feedback = await waitForFeedback(ctx.sessionId, timeoutMs)
|
|
154
|
+
process.stdout.write(feedback)
|
|
155
|
+
return 0
|
|
156
|
+
} catch (error: unknown) {
|
|
157
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
158
|
+
if (reason === 'timeout') {
|
|
159
|
+
console.error('[fabrika] timeout waiting for gate feedback.')
|
|
160
|
+
} else {
|
|
161
|
+
console.error(`[fabrika] gate wait failed: ${reason}`)
|
|
162
|
+
}
|
|
163
|
+
return 1
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { parseArgs } from 'util'
|
|
4
|
+
import { resolveStackPointer } from '../catalog'
|
|
5
|
+
import { resolveContext } from '../context'
|
|
6
|
+
import { induceIssues, renderIssue } from '../induce'
|
|
7
|
+
import { readSkeletons } from '../skeletons'
|
|
8
|
+
|
|
9
|
+
export async function cmdInduce(argv: string[]): Promise<number> {
|
|
10
|
+
const { values } = parseArgs({
|
|
11
|
+
args: argv,
|
|
12
|
+
options: {
|
|
13
|
+
dir: { type: 'string' },
|
|
14
|
+
session: { type: 'string' },
|
|
15
|
+
force: { type: 'boolean', default: false },
|
|
16
|
+
},
|
|
17
|
+
allowPositionals: true,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const ctx = resolveContext({ dir: values.dir, session: values.session })
|
|
21
|
+
if (!ctx.initialized) {
|
|
22
|
+
console.error(`[fabrika] no .fabrika/ found at or above ${ctx.repoRoot}. Run \`fabrika builder init\` first.`)
|
|
23
|
+
return 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const testsBrowserDir = join(ctx.repoRoot, 'tests', 'browser')
|
|
27
|
+
const skeletons = readSkeletons(testsBrowserDir)
|
|
28
|
+
if (skeletons.length === 0) {
|
|
29
|
+
console.error(
|
|
30
|
+
`[fabrika] no opice skeletons in ${testsBrowserDir}. `
|
|
31
|
+
+ `Author them in phase 6 (skeletons) first.`,
|
|
32
|
+
)
|
|
33
|
+
return 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const issues = induceIssues(skeletons, { stack: resolveStackPointer(ctx.profile) ?? undefined })
|
|
37
|
+
const issuesDir = join(ctx.fabrikaDir, 'issues')
|
|
38
|
+
mkdirSync(issuesDir, { recursive: true })
|
|
39
|
+
|
|
40
|
+
let written = 0
|
|
41
|
+
let skipped = 0
|
|
42
|
+
for (const issue of issues) {
|
|
43
|
+
const path = join(issuesDir, `${issue.id}.md`)
|
|
44
|
+
if (existsSync(path) && !values.force) {
|
|
45
|
+
skipped++
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
writeFileSync(path, renderIssue(issue))
|
|
49
|
+
written++
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const foundation = issues.filter((i) => i.kind === 'foundation').length
|
|
53
|
+
const feature = issues.filter((i) => i.kind === 'feature').length
|
|
54
|
+
console.log(
|
|
55
|
+
`[fabrika] induced ${issues.length} issue(s) from ${skeletons.length} skeleton(s): `
|
|
56
|
+
+ `${foundation} foundation + ${feature} feature.`,
|
|
57
|
+
)
|
|
58
|
+
console.log(`[fabrika] wrote ${written}, skipped ${skipped} existing (use --force to overwrite).`)
|
|
59
|
+
console.log(`[fabrika] → .fabrika/issues/ · next: fabrika builder advance && fabrika builder next (build)`)
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { rmSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { parseArgs } from 'util'
|
|
4
|
+
import { BASE_URL, SESSIONS_DIR } from '../config'
|
|
5
|
+
import { ensureDaemon, openBrowser } from '../daemon-lifecycle'
|
|
6
|
+
import { type AgentHost, injectAgentSupport, parseAgentHosts } from '../inject-skills'
|
|
7
|
+
import { scaffold } from '../layout'
|
|
8
|
+
|
|
9
|
+
async function resetStaleSession(sessionId: string): Promise<void> {
|
|
10
|
+
try {
|
|
11
|
+
await fetch(`${BASE_URL}/api/session/${encodeURIComponent(sessionId)}/reset`, { method: 'POST' })
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
rmSync(join(SESSIONS_DIR, sessionId), { recursive: true, force: true })
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function cmdInit(argv: string[]): Promise<number> {
|
|
21
|
+
const { values, positionals } = parseArgs({
|
|
22
|
+
args: argv,
|
|
23
|
+
options: {
|
|
24
|
+
profile: { type: 'string', default: 'cf-lightweight' },
|
|
25
|
+
session: { type: 'string' },
|
|
26
|
+
agent: { type: 'string', default: 'all' },
|
|
27
|
+
'no-daemon': { type: 'boolean', default: false },
|
|
28
|
+
'no-open': { type: 'boolean', default: false },
|
|
29
|
+
},
|
|
30
|
+
allowPositionals: true,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
let hosts: AgentHost[]
|
|
34
|
+
try {
|
|
35
|
+
hosts = parseAgentHosts(values.agent)
|
|
36
|
+
} catch (error: unknown) {
|
|
37
|
+
console.error(`[fabrika] ${error instanceof Error ? error.message : String(error)}`)
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
const dir = positionals[0] ?? process.cwd()
|
|
41
|
+
const result = scaffold(dir, { profile: values.profile, sessionId: values.session })
|
|
42
|
+
const support = injectAgentSupport(result.repoRoot, { hosts })
|
|
43
|
+
|
|
44
|
+
if (result.alreadyInitialized) {
|
|
45
|
+
console.error(`[fabrika] .fabrika/ already initialized at ${result.fabrikaDir} (idempotent re-run)`)
|
|
46
|
+
} else {
|
|
47
|
+
console.error(`[fabrika] scaffolded .fabrika/ at ${result.fabrikaDir}`)
|
|
48
|
+
}
|
|
49
|
+
if (result.createdDirs.length) {
|
|
50
|
+
console.error(`[fabrika] created ${result.createdDirs.length} dir(s)`)
|
|
51
|
+
}
|
|
52
|
+
if (support.sourceDir) {
|
|
53
|
+
console.error(
|
|
54
|
+
`[fabrika] agent support (${support.hosts.join('+')}): `
|
|
55
|
+
+ `${support.written.length} installed, ${support.updated.length} updated, `
|
|
56
|
+
+ `${support.skipped.length} current, ${support.preserved.length} user-edited preserved`,
|
|
57
|
+
)
|
|
58
|
+
} else {
|
|
59
|
+
console.error(`[fabrika] agent support: source dir not found, skipped installation`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const browserUrl = `${BASE_URL}/s/${result.config.sessionId}`
|
|
63
|
+
if (!values['no-daemon']) {
|
|
64
|
+
const up = await ensureDaemon({ quiet: true })
|
|
65
|
+
if (!result.alreadyInitialized) {
|
|
66
|
+
await resetStaleSession(result.config.sessionId)
|
|
67
|
+
console.error(`[fabrika] cleared any stale session for ${result.config.sessionId}`)
|
|
68
|
+
}
|
|
69
|
+
if (up && !values['no-open']) {
|
|
70
|
+
openBrowser(browserUrl)
|
|
71
|
+
} else if (!up) {
|
|
72
|
+
console.error(`[fabrika] daemon not started (offline). Dashboard: ${browserUrl}`)
|
|
73
|
+
}
|
|
74
|
+
} else if (!result.alreadyInitialized) {
|
|
75
|
+
await resetStaleSession(result.config.sessionId)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
ok: true,
|
|
81
|
+
repoRoot: result.repoRoot,
|
|
82
|
+
fabrikaDir: result.fabrikaDir,
|
|
83
|
+
sessionId: result.config.sessionId,
|
|
84
|
+
profile: result.config.profile,
|
|
85
|
+
agents: support.hosts,
|
|
86
|
+
browserUrl,
|
|
87
|
+
createdConfig: result.createdConfig,
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
return 0
|
|
91
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parseArgs } from 'util'
|
|
2
|
+
import { resolveContext } from '../context'
|
|
3
|
+
import { packetJson, renderPacket } from '../packet'
|
|
4
|
+
import { resolvePhaseState } from '../phase-state'
|
|
5
|
+
|
|
6
|
+
export async function cmdNext(argv: string[]): Promise<number> {
|
|
7
|
+
const { values } = parseArgs({
|
|
8
|
+
args: argv,
|
|
9
|
+
options: {
|
|
10
|
+
json: { type: 'boolean', default: false },
|
|
11
|
+
dir: { type: 'string' },
|
|
12
|
+
session: { type: 'string' },
|
|
13
|
+
},
|
|
14
|
+
allowPositionals: true,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const ctx = resolveContext({ dir: values.dir, session: values.session })
|
|
18
|
+
if (!ctx.initialized) {
|
|
19
|
+
console.error(`[fabrika] no .fabrika/ found at or above ${ctx.repoRoot}. Run \`fabrika builder init\` first.`)
|
|
20
|
+
return 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { state, source } = await resolvePhaseState(ctx.repoRoot, ctx.fabrikaDir, ctx.sessionId)
|
|
24
|
+
|
|
25
|
+
if (values.json) {
|
|
26
|
+
console.log(JSON.stringify({ ...packetJson(state, { profile: ctx.profile }), source }))
|
|
27
|
+
} else {
|
|
28
|
+
process.stdout.write(renderPacket(state, { profile: ctx.profile }))
|
|
29
|
+
}
|
|
30
|
+
return 0
|
|
31
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from 'crypto'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import { homedir, tmpdir } from 'os'
|
|
4
|
+
import { join, resolve } from 'path'
|
|
5
|
+
|
|
6
|
+
export const DAEMON_PORT = parseInt(process.env['FABRIKA_PORT'] || '19410', 10)
|
|
7
|
+
export const BASE_URL = `http://localhost:${DAEMON_PORT}`
|
|
8
|
+
export const WS_URL = `ws://localhost:${DAEMON_PORT}`
|
|
9
|
+
|
|
10
|
+
export const TIMEOUT_MS = parseInt(process.env['FABRIKA_TIMEOUT'] || String(60 * 60 * 1000), 10)
|
|
11
|
+
|
|
12
|
+
export const LEGACY_DATA_DIR = join(homedir(), '.claude', 'fabrika')
|
|
13
|
+
export const NEUTRAL_DATA_DIR = join(homedir(), '.fabrika')
|
|
14
|
+
export const DATA_DIR = resolveDataDir()
|
|
15
|
+
export const SESSIONS_DIR = join(DATA_DIR, 'sessions')
|
|
16
|
+
export const TEMP_DIR = join(tmpdir(), 'fabrika')
|
|
17
|
+
export const PID_FILE = join(TEMP_DIR, 'daemon.pid')
|
|
18
|
+
|
|
19
|
+
export const FABRIKA_DIRNAME = '.fabrika'
|
|
20
|
+
|
|
21
|
+
export function resolveDataDir(opts: {
|
|
22
|
+
home?: string
|
|
23
|
+
explicit?: string
|
|
24
|
+
exists?: (path: string) => boolean
|
|
25
|
+
} = {}): string {
|
|
26
|
+
const home = opts.home ?? homedir()
|
|
27
|
+
const explicit = opts.explicit ?? process.env['FABRIKA_DATA_DIR']
|
|
28
|
+
if (explicit) return resolve(explicit)
|
|
29
|
+
const neutral = join(home, '.fabrika')
|
|
30
|
+
const legacy = join(home, '.claude', 'fabrika')
|
|
31
|
+
const exists = opts.exists ?? existsSync
|
|
32
|
+
if (exists(neutral)) return neutral
|
|
33
|
+
if (exists(legacy)) return legacy
|
|
34
|
+
return neutral
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getSessionId(explicit?: string, recorded?: string, repoRoot?: string): string {
|
|
38
|
+
if (explicit) return explicit
|
|
39
|
+
if (process.env['FABRIKA_SESSION_ID']) return process.env['FABRIKA_SESSION_ID']
|
|
40
|
+
if (recorded) return recorded
|
|
41
|
+
return hashRepoPath(repoRoot ?? process.cwd())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function hashRepoPath(repoRoot: string): string {
|
|
45
|
+
const abs = resolve(repoRoot)
|
|
46
|
+
return 's_' + createHash('sha256').update(abs).digest('hex').slice(0, 16)
|
|
47
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolve } from 'path'
|
|
2
|
+
import { getSessionId } from './config'
|
|
3
|
+
import { fabrikaDir, findRepoRoot, readConfig } from './layout'
|
|
4
|
+
|
|
5
|
+
export interface CommandContext {
|
|
6
|
+
repoRoot: string
|
|
7
|
+
fabrikaDir: string
|
|
8
|
+
sessionId: string
|
|
9
|
+
profile: string
|
|
10
|
+
initialized: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveContext(opts: { dir?: string; session?: string } = {}): CommandContext {
|
|
14
|
+
const repoRoot = opts.dir
|
|
15
|
+
? resolve(opts.dir)
|
|
16
|
+
: findRepoRoot(process.cwd()) ?? process.cwd()
|
|
17
|
+
|
|
18
|
+
const config = readConfig(repoRoot)
|
|
19
|
+
const sessionId = getSessionId(opts.session, config?.sessionId, repoRoot)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
repoRoot,
|
|
23
|
+
fabrikaDir: fabrikaDir(repoRoot),
|
|
24
|
+
sessionId,
|
|
25
|
+
profile: config?.profile ?? 'cf-lightweight',
|
|
26
|
+
initialized: config !== null,
|
|
27
|
+
}
|
|
28
|
+
}
|