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.
Files changed (75) hide show
  1. package/README.md +127 -2
  2. package/bin/tspl.js +10 -0
  3. package/package.json +41 -4
  4. package/src/active-org.js +49 -0
  5. package/src/analysis/analyzer.js +61 -0
  6. package/src/analysis/extract.js +37 -0
  7. package/src/analysis/file-filter.js +69 -0
  8. package/src/analysis/git-reader.js +108 -0
  9. package/src/analysis/html-parser.js +46 -0
  10. package/src/analysis/js-parser.js +473 -0
  11. package/src/analysis/py-parser.js +66 -0
  12. package/src/analysis/repo-index.js +107 -0
  13. package/src/api-client.js +108 -0
  14. package/src/browser-tests.js +209 -0
  15. package/src/commands/analyze.command.js +81 -0
  16. package/src/commands/auth.command.js +250 -0
  17. package/src/commands/build.command.js +267 -0
  18. package/src/commands/connect.command.js +270 -0
  19. package/src/commands/engines.command.js +136 -0
  20. package/src/commands/find.command.js +107 -0
  21. package/src/commands/init.command.js +79 -0
  22. package/src/commands/link.command.js +65 -0
  23. package/src/commands/mcp.command.js +12 -0
  24. package/src/commands/member.command.js +82 -0
  25. package/src/commands/plan.command.js +161 -0
  26. package/src/commands/project.command.js +191 -0
  27. package/src/commands/setup.command.js +63 -0
  28. package/src/commands/sync.command.js +59 -0
  29. package/src/commands/task.command.js +475 -0
  30. package/src/commands/usage.command.js +105 -0
  31. package/src/config-store.js +255 -0
  32. package/src/connect/claude-skill.md +30 -0
  33. package/src/engines.js +218 -0
  34. package/src/find/graph.js +272 -0
  35. package/src/find/search.js +473 -0
  36. package/src/find/text.js +86 -0
  37. package/src/git-helper.js +102 -0
  38. package/src/index.js +91 -0
  39. package/src/mcp/server.js +708 -0
  40. package/src/project-config.js +197 -0
  41. package/src/project-init.js +447 -0
  42. package/src/providers/github.js +249 -0
  43. package/src/providers/index.js +83 -0
  44. package/src/runtime-check.js +18 -0
  45. package/src/setup.js +180 -0
  46. package/src/shared/approaches.js +44 -0
  47. package/src/shared/constants.js +89 -0
  48. package/src/shared/index.js +8 -0
  49. package/src/shared/workflow-progress.js +67 -0
  50. package/src/shared/workflow.js +381 -0
  51. package/src/stack-detector.js +47 -0
  52. package/src/task/approaches.js +53 -0
  53. package/src/task/branch-choice.js +69 -0
  54. package/src/task/brief.js +115 -0
  55. package/src/task/build.js +549 -0
  56. package/src/task/check.js +415 -0
  57. package/src/task/conventions.js +113 -0
  58. package/src/task/gate.js +88 -0
  59. package/src/task/hooks.js +82 -0
  60. package/src/task/plan-draft.js +43 -0
  61. package/src/task/plan-page.js +123 -0
  62. package/src/task/plan.js +293 -0
  63. package/src/task/planning-context.js +165 -0
  64. package/src/task/refine.js +73 -0
  65. package/src/workflow/agents.js +336 -0
  66. package/src/workflow/checkout-lock.js +92 -0
  67. package/src/workflow/client.js +82 -0
  68. package/src/workflow/controller.js +585 -0
  69. package/src/workflow/evidence.js +276 -0
  70. package/src/workflow/pr-body.js +52 -0
  71. package/src/workflow/process.js +82 -0
  72. package/src/workflow/review.js +128 -0
  73. package/src/workflow/runtime.js +148 -0
  74. package/src/workflow/wait.js +58 -0
  75. 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
+ }