tspl-cli 0.0.0-stage → 0.1.1
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/README.md +127 -2
- package/bin/tspl.js +10 -0
- package/package.json +41 -4
- package/src/active-org.js +49 -0
- package/src/analysis/analyzer.js +61 -0
- package/src/analysis/extract.js +37 -0
- package/src/analysis/file-filter.js +69 -0
- package/src/analysis/git-reader.js +108 -0
- package/src/analysis/html-parser.js +46 -0
- package/src/analysis/js-parser.js +473 -0
- package/src/analysis/py-parser.js +66 -0
- package/src/analysis/repo-index.js +107 -0
- package/src/api-client.js +108 -0
- package/src/browser-tests.js +209 -0
- package/src/commands/analyze.command.js +81 -0
- package/src/commands/auth.command.js +250 -0
- package/src/commands/build.command.js +267 -0
- package/src/commands/connect.command.js +270 -0
- package/src/commands/engines.command.js +136 -0
- package/src/commands/find.command.js +107 -0
- package/src/commands/init.command.js +79 -0
- package/src/commands/link.command.js +65 -0
- package/src/commands/mcp.command.js +12 -0
- package/src/commands/member.command.js +82 -0
- package/src/commands/plan.command.js +161 -0
- package/src/commands/project.command.js +191 -0
- package/src/commands/setup.command.js +63 -0
- package/src/commands/sync.command.js +59 -0
- package/src/commands/task.command.js +475 -0
- package/src/commands/usage.command.js +105 -0
- package/src/config-store.js +255 -0
- package/src/connect/claude-skill.md +30 -0
- package/src/engines.js +218 -0
- package/src/find/graph.js +272 -0
- package/src/find/search.js +473 -0
- package/src/find/text.js +86 -0
- package/src/git-helper.js +102 -0
- package/src/index.js +91 -0
- package/src/mcp/server.js +708 -0
- package/src/project-config.js +197 -0
- package/src/project-init.js +447 -0
- package/src/providers/github.js +249 -0
- package/src/providers/index.js +83 -0
- package/src/runtime-check.js +18 -0
- package/src/setup.js +180 -0
- package/src/shared/approaches.js +44 -0
- package/src/shared/constants.js +89 -0
- package/src/shared/index.js +8 -0
- package/src/shared/workflow-progress.js +67 -0
- package/src/shared/workflow.js +381 -0
- package/src/stack-detector.js +47 -0
- package/src/task/approaches.js +53 -0
- package/src/task/branch-choice.js +69 -0
- package/src/task/brief.js +115 -0
- package/src/task/build.js +549 -0
- package/src/task/check.js +415 -0
- package/src/task/conventions.js +113 -0
- package/src/task/gate.js +88 -0
- package/src/task/hooks.js +82 -0
- package/src/task/plan-draft.js +43 -0
- package/src/task/plan-page.js +123 -0
- package/src/task/plan.js +293 -0
- package/src/task/planning-context.js +165 -0
- package/src/task/refine.js +73 -0
- package/src/workflow/agents.js +336 -0
- package/src/workflow/checkout-lock.js +92 -0
- package/src/workflow/client.js +82 -0
- package/src/workflow/controller.js +585 -0
- package/src/workflow/evidence.js +276 -0
- package/src/workflow/pr-body.js +52 -0
- package/src/workflow/process.js +82 -0
- package/src/workflow/review.js +128 -0
- package/src/workflow/runtime.js +148 -0
- package/src/workflow/wait.js +58 -0
- package/src/workspace.js +250 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { input } = require('@inquirer/prompts')
|
|
4
|
+
|
|
5
|
+
const { request } = require('../api-client')
|
|
6
|
+
const { readCredentials, readLinks, writeLinks } = require('../config-store')
|
|
7
|
+
|
|
8
|
+
const localLinks = project =>
|
|
9
|
+
readLinks()[`${readCredentials().organization}/${project.slug}`] || { repos: {} }
|
|
10
|
+
|
|
11
|
+
const printProject = project => {
|
|
12
|
+
const links = localLinks(project)
|
|
13
|
+
console.log(`${project.name} (${project.slug})`)
|
|
14
|
+
if (project.description) {
|
|
15
|
+
console.log(` ${project.description}`)
|
|
16
|
+
}
|
|
17
|
+
if (!project.repos.length) {
|
|
18
|
+
console.log(' No repos yet. Run tspl inside the project folder to add them.')
|
|
19
|
+
}
|
|
20
|
+
project.repos.forEach(repo => {
|
|
21
|
+
const local = links.repos[repo._id]
|
|
22
|
+
console.log(
|
|
23
|
+
` ${repo.name.padEnd(24)} [${repo.defaultBaseBranch}] ${local ? local.path : '(not on this machine)'}`,
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const list = async () => {
|
|
29
|
+
const projects = await request('GET', '/api/v1/projects')
|
|
30
|
+
if (!projects.length) {
|
|
31
|
+
console.log('No projects yet. Run tspl inside a project folder to create one.')
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
projects.forEach(project => {
|
|
35
|
+
console.log(`${project.slug} ${project.name} (${project.repos.length} repos)`)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const show = async projectRef => {
|
|
40
|
+
const project = await request(
|
|
41
|
+
'GET',
|
|
42
|
+
`/api/v1/projects/${encodeURIComponent(projectRef)}`,
|
|
43
|
+
)
|
|
44
|
+
printProject(project)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const removeRepo = async (projectRef, repoName) => {
|
|
48
|
+
const project = await request(
|
|
49
|
+
'GET',
|
|
50
|
+
`/api/v1/projects/${encodeURIComponent(projectRef)}`,
|
|
51
|
+
)
|
|
52
|
+
const repo = project.repos.find(r => r.name === repoName)
|
|
53
|
+
if (!repo) {
|
|
54
|
+
throw new Error(`"${repoName}" is not a repo of ${project.name}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const updated = await request(
|
|
58
|
+
'DELETE',
|
|
59
|
+
`/api/v1/projects/${project._id}/repos/${repo._id}`,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const links = readLinks()
|
|
63
|
+
const key = `${readCredentials().organization}/${project.slug}`
|
|
64
|
+
if (links[key]) {
|
|
65
|
+
delete links[key].repos[repo._id]
|
|
66
|
+
links[key].ignored = [
|
|
67
|
+
...new Set([...(links[key].ignored || []), repo.fingerprint || repo.remoteHash]),
|
|
68
|
+
]
|
|
69
|
+
writeLinks(links)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
printProject(updated)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const memberId = async email => {
|
|
76
|
+
const members = await request('GET', '/api/v1/members')
|
|
77
|
+
const member = members.find(item => item.email === email.toLowerCase())
|
|
78
|
+
if (!member) {
|
|
79
|
+
throw new Error(`${email} is not in your organization; add them first: tspl member add ${email} --name "…" --role developer`)
|
|
80
|
+
}
|
|
81
|
+
return member.userId
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const addMember = async (projectRef, email) => {
|
|
85
|
+
const project = await request('POST', `/api/v1/projects/${encodeURIComponent(projectRef)}/members`, { email })
|
|
86
|
+
console.log(`${email} can now work on ${project.name} (${project.members.length} people).`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const removeMember = async (projectRef, email) => {
|
|
90
|
+
const project = await request('DELETE', `/api/v1/projects/${encodeURIComponent(projectRef)}/members/${await memberId(email)}`)
|
|
91
|
+
console.log(`${email} is off ${project.name}.`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const setApproval = async (projectRef, value) => {
|
|
95
|
+
const on = { on: true, yes: true, true: true, off: false, no: false, false: false }[String(value).toLowerCase()]
|
|
96
|
+
if (on === undefined) {
|
|
97
|
+
throw new Error('Use on or off, e.g. tspl project set-approval my-project on')
|
|
98
|
+
}
|
|
99
|
+
const project = await request('PUT', `/api/v1/projects/${encodeURIComponent(projectRef)}/settings`, { requirePmApproval: on })
|
|
100
|
+
console.log(on
|
|
101
|
+
? `${project.name}: plans now need PM approval after the developer approves.`
|
|
102
|
+
: `${project.name}: the developer's approval is enough; no PM approval needed.`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const setBranch = async (projectRef, repoName, branch) => {
|
|
106
|
+
const project = await request(
|
|
107
|
+
'GET',
|
|
108
|
+
`/api/v1/projects/${encodeURIComponent(projectRef)}`,
|
|
109
|
+
)
|
|
110
|
+
const repo = project.repos.find(r => r.name === repoName)
|
|
111
|
+
if (!repo) {
|
|
112
|
+
throw new Error(`"${repoName}" is not a repo of ${project.name}`)
|
|
113
|
+
}
|
|
114
|
+
const updated = await request(
|
|
115
|
+
'PATCH',
|
|
116
|
+
`/api/v1/projects/${project._id}/repos/${repo._id}`,
|
|
117
|
+
{ defaultBaseBranch: branch },
|
|
118
|
+
)
|
|
119
|
+
printProject(updated)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const remove = async (projectRef, options) => {
|
|
123
|
+
const project = await request(
|
|
124
|
+
'GET',
|
|
125
|
+
`/api/v1/projects/${encodeURIComponent(projectRef)}`,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if (!options.yes) {
|
|
129
|
+
const typed = await input({
|
|
130
|
+
message: `This deletes project "${project.name}" for everyone. Type ${project.slug} to confirm:`,
|
|
131
|
+
})
|
|
132
|
+
const answer = typed.trim().toLowerCase()
|
|
133
|
+
if (answer !== project.slug && answer !== project.name.toLowerCase()) {
|
|
134
|
+
console.log('Not deleted: the name did not match.')
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await request('DELETE', `/api/v1/projects/${project._id}`)
|
|
140
|
+
|
|
141
|
+
const links = readLinks()
|
|
142
|
+
delete links[`${readCredentials().organization}/${project.slug}`]
|
|
143
|
+
writeLinks(links)
|
|
144
|
+
|
|
145
|
+
console.log(`Deleted project "${project.name}". Your local repos are untouched.`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
register(program) {
|
|
150
|
+
const project = program.command('project').description('View projects and their repos')
|
|
151
|
+
|
|
152
|
+
project.command('list').description('Projects you can see').action(list)
|
|
153
|
+
|
|
154
|
+
project
|
|
155
|
+
.command('show <project>')
|
|
156
|
+
.description('One project, its repos, and where they are on this machine')
|
|
157
|
+
.action(show)
|
|
158
|
+
|
|
159
|
+
project
|
|
160
|
+
.command('remove-repo <project> <repo>')
|
|
161
|
+
.description('Remove a repo from a project (it is archived, not deleted)')
|
|
162
|
+
.action(removeRepo)
|
|
163
|
+
|
|
164
|
+
project
|
|
165
|
+
.command('set-branch <project> <repo> <branch>')
|
|
166
|
+
.description('Change which branch is read and built on for a repo')
|
|
167
|
+
.action(setBranch)
|
|
168
|
+
|
|
169
|
+
project
|
|
170
|
+
.command('add-member <project> <email>')
|
|
171
|
+
.description('Give a person in your organization access to a project (owner, admin or PM)')
|
|
172
|
+
.action(addMember)
|
|
173
|
+
|
|
174
|
+
project
|
|
175
|
+
.command('remove-member <project> <email>')
|
|
176
|
+
.description('Take a person off a project (owner, admin or PM)')
|
|
177
|
+
.action(removeMember)
|
|
178
|
+
|
|
179
|
+
project
|
|
180
|
+
.command('set-approval <project> <on|off>')
|
|
181
|
+
.description('Whether plans need PM approval before building (owner, admin or PM)')
|
|
182
|
+
.action(setApproval)
|
|
183
|
+
|
|
184
|
+
project
|
|
185
|
+
.command('delete <project>')
|
|
186
|
+
.alias('rm')
|
|
187
|
+
.description('Delete a project for everyone (owner or admin)')
|
|
188
|
+
.option('-y, --yes', 'skip the confirmation')
|
|
189
|
+
.action(remove)
|
|
190
|
+
},
|
|
191
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Prompts = require('@inquirer/prompts')
|
|
4
|
+
const { execFileSync } = require('child_process')
|
|
5
|
+
|
|
6
|
+
const { configureMachine } = require('../setup')
|
|
7
|
+
const { request, originOf } = require('../api-client')
|
|
8
|
+
const config = require('../config-store')
|
|
9
|
+
const engines = require('../engines')
|
|
10
|
+
const { login } = require('./auth.command')
|
|
11
|
+
const { TOOLS, connectTool } = require('./connect.command')
|
|
12
|
+
|
|
13
|
+
const ICON = { ok: '✓', failed: '✗', missing: '✗', manual: '!', skipped: '–' }
|
|
14
|
+
|
|
15
|
+
const gitVersion = () => {
|
|
16
|
+
try {
|
|
17
|
+
return execFileSync('git', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultDeps = () => ({
|
|
24
|
+
prompts: Prompts,
|
|
25
|
+
config: { read: config.readConfig, write: config.writeConfig },
|
|
26
|
+
credentials: { read: config.readCredentials, clear: config.clearCredentials },
|
|
27
|
+
engines: {
|
|
28
|
+
detectAll: engines.detectAll,
|
|
29
|
+
defaultAssignments: engines.defaultAssignments,
|
|
30
|
+
read: config.readEngines,
|
|
31
|
+
write: config.writeEngines,
|
|
32
|
+
run: engines.run,
|
|
33
|
+
},
|
|
34
|
+
request,
|
|
35
|
+
originOf,
|
|
36
|
+
login,
|
|
37
|
+
connect: connectTool,
|
|
38
|
+
tools: TOOLS,
|
|
39
|
+
gitVersion,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const setup = async options => {
|
|
43
|
+
const result = await configureMachine(options, defaultDeps())
|
|
44
|
+
console.log('')
|
|
45
|
+
result.steps.forEach(item => console.log(`${ICON[item.status] || '?'} ${item.label}: ${item.detail}`))
|
|
46
|
+
console.log(`\n${result.ready ? 'This machine is ready.' : 'This machine is not ready yet.'}`)
|
|
47
|
+
result.next.forEach(line => console.log(` → ${line}`))
|
|
48
|
+
if (!result.ready) process.exitCode = 1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
register(program) {
|
|
53
|
+
program
|
|
54
|
+
.command('setup')
|
|
55
|
+
.description('Configure this machine step by step: API login, AI engines, AI tool integrations, optional engine test (no project needed)')
|
|
56
|
+
.option('-y, --yes', 'keep existing choices and ask nothing')
|
|
57
|
+
.option('--api <url>', 'TSPL API address')
|
|
58
|
+
.option('--email <email>', 'your email')
|
|
59
|
+
.option('--org <slug>', 'organization, if you belong to several')
|
|
60
|
+
.option('--connect <tools...>', 'AI tools to connect (claude, codex, antigravity, gemini, cursor, vscode)')
|
|
61
|
+
.action(setup)
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Path = require('path')
|
|
4
|
+
const { spawn } = require('child_process')
|
|
5
|
+
|
|
6
|
+
const { findProject } = require('../workspace')
|
|
7
|
+
const { refreshIndex } = require('../analysis/analyzer')
|
|
8
|
+
const { fetchBranch } = require('../git-helper')
|
|
9
|
+
const { lockSync, unlockSync, readSyncState, writeSyncState } = require('../config-store')
|
|
10
|
+
|
|
11
|
+
const BIN = Path.join(__dirname, '..', '..', 'bin', 'tspl.js')
|
|
12
|
+
const FETCH_EVERY = 30 * 60 * 1000
|
|
13
|
+
|
|
14
|
+
const fetchDue = projectId => {
|
|
15
|
+
const fetchedAt = (readSyncState()[projectId] || {}).fetchedAt
|
|
16
|
+
return !fetchedAt || Date.now() - new Date(fetchedAt).getTime() > FETCH_EVERY
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const fetchBases = (project, repos) => {
|
|
20
|
+
const state = readSyncState()
|
|
21
|
+
state[project._id] = { ...(state[project._id] || {}), fetchedAt: new Date().toISOString() }
|
|
22
|
+
writeSyncState(state)
|
|
23
|
+
repos.forEach(repo => fetchBranch(repo.path, repo.defaultBaseBranch))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sync = async () => {
|
|
27
|
+
if (!lockSync()) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const { project, repos, offline } = await findProject(process.cwd())
|
|
32
|
+
if (offline) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
if (fetchDue(project._id)) {
|
|
36
|
+
fetchBases(project, repos)
|
|
37
|
+
}
|
|
38
|
+
await refreshIndex(project, repos)
|
|
39
|
+
} finally {
|
|
40
|
+
unlockSync()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const syncInBackground = cwd => {
|
|
45
|
+
const child = spawn(process.execPath, [BIN, 'sync'], { cwd, detached: true, stdio: 'ignore' })
|
|
46
|
+
child.on('error', () => {})
|
|
47
|
+
child.unref()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
fetchDue,
|
|
52
|
+
syncInBackground,
|
|
53
|
+
register(program) {
|
|
54
|
+
program
|
|
55
|
+
.command('sync', { hidden: true })
|
|
56
|
+
.description('Fetch the newest base branches and refresh the local code map (runs in the background)')
|
|
57
|
+
.action(sync)
|
|
58
|
+
},
|
|
59
|
+
}
|