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,473 @@
1
+ 'use strict'
2
+
3
+ const { parse } = require('@babel/parser')
4
+ const { isSecretValue } = require('./file-filter')
5
+
6
+ const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'request']
7
+ const HTTP_VERB = /^(get|post|put|patch|delete|head|options)$/i
8
+ const HTTP_CLIENTS = /^\$?(http|httpclient|axios|api|apiservice|apiclient|client|request|instance)$/i
9
+ const URL_LIKE = /^(\/|\{\}\/|https?:\/\/)/
10
+ const EXPRESS_ROUTERS = /^(router|app|route|api)$/i
11
+ const UI_ROUTE_KEYS = ['component', 'loadChildren', 'loadComponent', 'redirectTo', 'children']
12
+ const SKIP_KEYS = new Set(['loc', 'start', 'end', 'extra', 'leadingComments', 'trailingComments', 'innerComments', 'comments', 'tokens'])
13
+ const LABEL_ATTRIBUTES = /^(title|placeholder|label|text|header|headerTitle|tabBarLabel|accessibilityLabel|alt)$/
14
+ const LIMIT = 500
15
+ const LABEL_LIMIT = 150
16
+ const VALUE_LIMIT = 200
17
+ const TEXT_LIMIT = 100
18
+ const COMMENT_LIMIT = 50
19
+ const CONSTANT_NAME = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$|^[A-Z]{3,}$/
20
+ const CODE_LIKE = /[;{}=]|=>|\(\)|^\s*(const|let|var|if|return|import|await)\b/
21
+
22
+ const pluginsFor = ext => {
23
+ if (ext === '.ts') return ['typescript', 'decorators-legacy']
24
+ if (ext === '.tsx') return ['typescript', 'jsx', 'decorators-legacy']
25
+ return ['jsx', 'decorators-legacy']
26
+ }
27
+
28
+ const walk = (root, visit) => {
29
+ const stack = [root]
30
+ while (stack.length) {
31
+ const node = stack.pop()
32
+ visit(node)
33
+ for (const key of Object.keys(node)) {
34
+ if (SKIP_KEYS.has(key)) continue
35
+ const value = node[key]
36
+ if (Array.isArray(value)) {
37
+ for (let i = value.length - 1; i >= 0; i--) {
38
+ if (value[i] && typeof value[i].type === 'string') stack.push(value[i])
39
+ }
40
+ } else if (value && typeof value.type === 'string') {
41
+ stack.push(value)
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ const lineOf = node => (node.loc ? node.loc.start.line : null)
48
+
49
+ const keyName = prop => {
50
+ if (!prop || !prop.key) return null
51
+ if (prop.key.type === 'Identifier') return prop.key.name
52
+ if (prop.key.type === 'StringLiteral') return prop.key.value
53
+ return null
54
+ }
55
+
56
+ const property = (object, name) =>
57
+ object.properties.find(
58
+ prop => (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') && keyName(prop) === name,
59
+ )
60
+
61
+ const text = node => {
62
+ if (!node) return null
63
+ if (node.type === 'StringLiteral') return node.value
64
+ if (node.type === 'TemplateLiteral') {
65
+ return node.quasis.map(q => q.value.cooked).join('{}')
66
+ }
67
+ if (node.type === 'BinaryExpression' && node.operator === '+') {
68
+ const left = text(node.left)
69
+ const right = text(node.right)
70
+ if (left === null && right === null) return null
71
+ return `${left === null ? '{}' : left}${right === null ? '{}' : right}`
72
+ }
73
+ return null
74
+ }
75
+
76
+ const lastName = node => {
77
+ if (!node) return null
78
+ if (node.type === 'Identifier') return node.name
79
+ if (node.type === 'ThisExpression') return 'this'
80
+ if (node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') {
81
+ return node.property.type === 'Identifier' ? node.property.name : null
82
+ }
83
+ return null
84
+ }
85
+
86
+ const memberNames = node => {
87
+ if (!node) return []
88
+ if (node.type === 'Identifier') return [node.name]
89
+ if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
90
+ return [...memberNames(node.object), node.property.name]
91
+ }
92
+ return []
93
+ }
94
+
95
+ const handlerName = route => {
96
+ const holder = ['config', 'options']
97
+ .map(key => property(route, key))
98
+ .find(prop => prop && prop.value && prop.value.type === 'ObjectExpression')
99
+ const handler = property(route, 'handler') || (holder && property(holder.value, 'handler'))
100
+ if (!handler || !handler.value) return null
101
+ const names = memberNames(handler.value).filter(name => name !== 'handler')
102
+ return names.length > 1 ? names[names.length - 1] : null
103
+ }
104
+
105
+ const labelText = value => {
106
+ const label = value.replace(/\s+/g, ' ').trim()
107
+ return label.length >= 2 && label.length <= 80 && /[a-z]/i.test(label) ? label : null
108
+ }
109
+
110
+ const literalValue = node => {
111
+ if (!node) return null
112
+ if (node.type === 'StringLiteral') return node.value
113
+ if (node.type === 'NumericLiteral' || node.type === 'BooleanLiteral') return String(node.value)
114
+ if (node.type === 'TemplateLiteral' && !node.expressions.length) return node.quasis.map(q => q.value.cooked).join('')
115
+ return null
116
+ }
117
+
118
+ const valueOf = (name, node) => {
119
+ const value = literalValue(node)
120
+ if (value === null || !value.trim() || value.length > 80 || isSecretValue(name, value)) return null
121
+ return value
122
+ }
123
+
124
+ const commentLines = comments =>
125
+ comments
126
+ .flatMap(comment => comment.value.split('\n'))
127
+ .map(line => line.replace(/^[\s*/#-]+/, '').replace(/\s+/g, ' ').trim())
128
+ .filter(line => line.length <= 120 && line.split(' ').length >= 3 && /[a-z]{3}/i.test(line) && !CODE_LIKE.test(line))
129
+
130
+ const isModuleExports = node =>
131
+ node.type === 'MemberExpression' &&
132
+ node.object.type === 'Identifier' &&
133
+ node.object.name === 'module' &&
134
+ node.property.type === 'Identifier' &&
135
+ node.property.name === 'exports'
136
+
137
+ const classInfo = (node, name) => ({
138
+ kind: 'class',
139
+ name: name || (node.id && node.id.name) || 'default',
140
+ line: lineOf(node),
141
+ methods: node.body.body
142
+ .filter(m => (m.type === 'ClassMethod' || m.type === 'ClassProperty') && keyName(m) && keyName(m) !== 'constructor')
143
+ .filter(m => m.type === 'ClassMethod' || (m.value && /Function/.test(m.value.type)))
144
+ .map(keyName)
145
+ .slice(0, 100),
146
+ })
147
+
148
+ const decoratorsOf = node =>
149
+ (node.decorators || [])
150
+ .map(d => d.expression)
151
+ .filter(e => e && e.type === 'CallExpression' && e.callee.type === 'Identifier')
152
+
153
+ const topLevelSymbols = (program, strings) => {
154
+ const symbols = []
155
+ const addDeclaration = decl => {
156
+ if (!decl) return
157
+ if (decl.type === 'FunctionDeclaration' && decl.id) {
158
+ symbols.push({ kind: 'function', name: decl.id.name, line: lineOf(decl) })
159
+ } else if (decl.type === 'ClassDeclaration') {
160
+ symbols.push(classInfo(decl))
161
+ } else if (decl.type === 'VariableDeclaration') {
162
+ decl.declarations.forEach(d => {
163
+ if (d.id.type !== 'Identifier' || !d.init) return
164
+ if (d.init.type === 'StringLiteral') strings.set(d.id.name, d.init.value)
165
+ if (/Function/.test(d.init.type)) {
166
+ symbols.push({ kind: 'function', name: d.id.name, line: lineOf(d) })
167
+ } else if (d.init.type === 'ClassExpression') {
168
+ symbols.push(classInfo(d.init, d.id.name))
169
+ }
170
+ })
171
+ }
172
+ }
173
+
174
+ program.body.forEach(statement => {
175
+ if (statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration') {
176
+ addDeclaration(statement.declaration)
177
+ } else if (
178
+ statement.type === 'ExpressionStatement' &&
179
+ statement.expression.type === 'AssignmentExpression' &&
180
+ isModuleExports(statement.expression.left) &&
181
+ statement.expression.right.type === 'ClassExpression'
182
+ ) {
183
+ symbols.push(classInfo(statement.expression.right))
184
+ } else {
185
+ addDeclaration(statement)
186
+ }
187
+ })
188
+ return symbols
189
+ }
190
+
191
+ const extractJs = (code, ext) => {
192
+ let ast
193
+ try {
194
+ ast = parse(code, {
195
+ sourceType: 'unambiguous',
196
+ errorRecovery: true,
197
+ allowReturnOutsideFunction: true,
198
+ plugins: pluginsFor(ext),
199
+ })
200
+ } catch (err) {
201
+ return { parseError: true }
202
+ }
203
+
204
+ const strings = new Map()
205
+ const symbols = topLevelSymbols(ast.program, strings)
206
+ const imports = new Set()
207
+ const exports = new Set()
208
+ const routes = []
209
+ const uiRoutes = []
210
+ const schemas = []
211
+ const modelNames = []
212
+ const modelSchemas = new Map()
213
+ const schemaVariables = new Map()
214
+ const httpCalls = []
215
+ const components = []
216
+ const screens = []
217
+ const refs = new Set()
218
+ const labels = new Set()
219
+ const values = []
220
+ const texts = new Set()
221
+ const topLevel = new Set()
222
+ ast.program.body.forEach(statement => {
223
+ const declaration = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement
224
+ if (declaration && declaration.type === 'VariableDeclaration') declaration.declarations.forEach(d => topLevel.add(d))
225
+ })
226
+ const addValue = (name, node) => {
227
+ const value = valueOf(name, node)
228
+ if (value !== null && values.length < VALUE_LIMIT) values.push({ name, value, line: lineOf(node) })
229
+ }
230
+
231
+ walk(ast.program, node => {
232
+ switch (node.type) {
233
+ case 'VariableDeclarator':
234
+ if (node.id.type === 'Identifier' && node.init && /(New|Call)Expression/.test(node.init.type) && lastName(node.init.callee) === 'Schema') {
235
+ schemaVariables.set(node.init, node.id.name)
236
+ }
237
+ if (node.id.type === 'Identifier' && (topLevel.has(node) || CONSTANT_NAME.test(node.id.name))) {
238
+ addValue(node.id.name, node.init)
239
+ }
240
+ break
241
+ case 'ObjectProperty': {
242
+ const name = keyName(node)
243
+ if (name && CONSTANT_NAME.test(name)) addValue(name, node.value)
244
+ break
245
+ }
246
+ case 'StringLiteral':
247
+ case 'TemplateLiteral': {
248
+ const value = node.type === 'StringLiteral' ? node.value : text(node)
249
+ if (value && value.length >= 8 && value.length <= 120 && / [a-z]/i.test(value) && !URL_LIKE.test(value) && !isSecretValue(null, value) && texts.size < TEXT_LIMIT) {
250
+ texts.add(value.replace(/\s+/g, ' ').trim())
251
+ }
252
+ break
253
+ }
254
+ case 'Identifier':
255
+ if (node.name.length >= 4 && /[A-Z]/.test(node.name)) refs.add(node.name)
256
+ break
257
+ case 'JSXText': {
258
+ const label = labelText(node.value)
259
+ if (label) labels.add(label)
260
+ break
261
+ }
262
+ case 'JSXAttribute': {
263
+ const label = node.name.type === 'JSXIdentifier' && LABEL_ATTRIBUTES.test(node.name.name) &&
264
+ node.value && node.value.type === 'StringLiteral' && labelText(node.value.value)
265
+ if (label) labels.add(label)
266
+ break
267
+ }
268
+ case 'ImportDeclaration':
269
+ imports.add(node.source.value)
270
+ break
271
+ case 'ExportAllDeclaration':
272
+ imports.add(node.source.value)
273
+ exports.add('*')
274
+ break
275
+ case 'ExportNamedDeclaration':
276
+ if (node.source) imports.add(node.source.value)
277
+ if (node.declaration) {
278
+ const d = node.declaration
279
+ if (d.id) exports.add(d.id.name)
280
+ if (d.declarations) d.declarations.forEach(v => v.id.name && exports.add(v.id.name))
281
+ }
282
+ node.specifiers.forEach(s => exports.add(s.exported.name || s.exported.value))
283
+ break
284
+ case 'ExportDefaultDeclaration':
285
+ exports.add('default')
286
+ break
287
+ case 'AssignmentExpression': {
288
+ const left = node.left
289
+ if (isModuleExports(left)) {
290
+ if (node.right.type === 'ObjectExpression') {
291
+ node.right.properties.forEach(p => keyName(p) && exports.add(keyName(p)))
292
+ } else {
293
+ exports.add('default')
294
+ }
295
+ } else if (
296
+ left.type === 'MemberExpression' &&
297
+ left.property.type === 'Identifier' &&
298
+ ((left.object.type === 'Identifier' && left.object.name === 'exports') || isModuleExports(left.object))
299
+ ) {
300
+ exports.add(left.property.name)
301
+ }
302
+ break
303
+ }
304
+ case 'CallExpression':
305
+ case 'OptionalCallExpression':
306
+ case 'NewExpression': {
307
+ const callee = node.callee
308
+ const first = node.arguments[0]
309
+ if (callee.type === 'Identifier' && callee.name === 'require' && first && first.type === 'StringLiteral') {
310
+ imports.add(first.value)
311
+ }
312
+ if (callee.type === 'Import' && first && first.type === 'StringLiteral') {
313
+ imports.add(first.value)
314
+ }
315
+ if (lastName(callee) === 'Schema' && first && first.type === 'ObjectExpression') {
316
+ const options = node.arguments[1]
317
+ const collection = options && options.type === 'ObjectExpression' && property(options, 'collection')
318
+ schemas.push({
319
+ variable: schemaVariables.get(node) || null,
320
+ fields: first.properties.map(keyName).filter(Boolean).slice(0, 200),
321
+ collection: collection ? text(collection.value) || strings.get(collection.value.name) || null : null,
322
+ line: lineOf(node),
323
+ })
324
+ }
325
+ if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
326
+ const method = callee.property.name
327
+ const owner = lastName(callee.object)
328
+ if (method === 'model' && first) {
329
+ const name = first.type === 'StringLiteral' ? first.value : strings.get(first.name)
330
+ if (name) modelNames.push(name)
331
+ const schema = node.arguments[1]
332
+ if (name && schema && schema.type === 'Identifier') modelSchemas.set(schema.name, name)
333
+ }
334
+ if (HTTP_METHODS.includes(method) && owner && HTTP_CLIENTS.test(owner)) {
335
+ const url = text(first)
336
+ if (url !== null) httpCalls.push({ method: method.toUpperCase(), url, line: lineOf(node) })
337
+ }
338
+ if (HTTP_METHODS.includes(method) && owner && EXPRESS_ROUTERS.test(owner) && first && first.type === 'StringLiteral' && first.value.startsWith('/')) {
339
+ routes.push({ method: method.toUpperCase(), path: first.value, line: lineOf(node) })
340
+ }
341
+ }
342
+ const second = node.arguments[1]
343
+ if (first && first.type === 'StringLiteral' && HTTP_VERB.test(first.value) && second) {
344
+ const url = text(second)
345
+ if (url !== null && URL_LIKE.test(url)) {
346
+ httpCalls.push({ method: first.value.toUpperCase(), url, line: lineOf(node) })
347
+ }
348
+ }
349
+ if (callee.type === 'Identifier' && callee.name === 'fetch' && first) {
350
+ const url = text(first)
351
+ const options = node.arguments[1]
352
+ const method = options && options.type === 'ObjectExpression' && property(options, 'method')
353
+ if (url !== null) {
354
+ httpCalls.push({ method: method ? (text(method.value) || 'GET').toUpperCase() : 'GET', url, line: lineOf(node) })
355
+ }
356
+ }
357
+ break
358
+ }
359
+ case 'ObjectExpression': {
360
+ const methodProp = property(node, 'method')
361
+ const pathProp = property(node, 'path')
362
+ const urlProp = property(node, 'url')
363
+ if (urlProp && urlProp.value && !pathProp) {
364
+ const url = text(urlProp.value)
365
+ const verb = methodProp && methodProp.value ? text(methodProp.value) : 'GET'
366
+ if (url !== null && URL_LIKE.test(url) && verb && HTTP_VERB.test(verb)) {
367
+ httpCalls.push({ method: verb.toUpperCase(), url, line: lineOf(node) })
368
+ }
369
+ } else if (methodProp && pathProp && pathProp.value) {
370
+ const path = text(pathProp.value)
371
+ const value = methodProp.value
372
+ const methods = value.type === 'ArrayExpression'
373
+ ? value.elements.map(text).filter(Boolean)
374
+ : [text(value)].filter(Boolean)
375
+ if (path !== null && methods.length) {
376
+ routes.push({ method: methods.join(',').toUpperCase(), path, handler: handlerName(node), line: lineOf(node) })
377
+ }
378
+ } else if (pathProp && pathProp.value && UI_ROUTE_KEYS.some(key => property(node, key))) {
379
+ const path = text(pathProp.value)
380
+ const component = property(node, 'component')
381
+ const lazy = property(node, 'loadChildren') || property(node, 'loadComponent')
382
+ let target = null
383
+ if (lazy) {
384
+ walk(lazy.value, inner => {
385
+ if (!target && inner.type === 'CallExpression' && inner.callee.type === 'Import' && inner.arguments[0]) {
386
+ target = text(inner.arguments[0])
387
+ }
388
+ })
389
+ }
390
+ if (path !== null) {
391
+ uiRoutes.push({
392
+ path,
393
+ component: component && component.value.type === 'Identifier' ? component.value.name : null,
394
+ lazy: target,
395
+ line: lineOf(node),
396
+ })
397
+ }
398
+ }
399
+ break
400
+ }
401
+ case 'ClassDeclaration': {
402
+ decoratorsOf(node).forEach(decorator => {
403
+ const kind = decorator.callee.name
404
+ if (!['Component', 'Injectable', 'NgModule', 'Directive', 'Pipe'].includes(kind)) return
405
+ const config = decorator.arguments[0]
406
+ const selector = config && config.type === 'ObjectExpression' && property(config, 'selector')
407
+ const template = config && config.type === 'ObjectExpression' && property(config, 'templateUrl')
408
+ components.push({
409
+ kind: kind === 'Injectable' ? 'service' : kind.toLowerCase(),
410
+ name: node.id ? node.id.name : 'default',
411
+ selector: selector ? text(selector.value) : null,
412
+ template: template ? text(template.value) : null,
413
+ line: lineOf(node),
414
+ })
415
+ })
416
+ break
417
+ }
418
+ case 'JSXOpeningElement': {
419
+ const name = node.name
420
+ if (name.type === 'JSXMemberExpression' && name.property.name === 'Screen') {
421
+ const attr = attrName => node.attributes.find(a => a.type === 'JSXAttribute' && a.name.name === attrName)
422
+ const screenName = attr('name')
423
+ const component = attr('component')
424
+ if (screenName && screenName.value && screenName.value.type === 'StringLiteral') {
425
+ screens.push({
426
+ name: screenName.value.value,
427
+ component: component && component.value && component.value.expression && component.value.expression.type === 'Identifier'
428
+ ? component.value.expression.name
429
+ : null,
430
+ line: lineOf(node),
431
+ })
432
+ }
433
+ }
434
+ break
435
+ }
436
+ default:
437
+ break
438
+ }
439
+ })
440
+
441
+ const byPosition = !modelSchemas.size && schemas.length === modelNames.length
442
+ const models = schemas.map(({ variable, ...schema }, i) => ({
443
+ name: modelSchemas.get(variable) || (schemas.length === 1 || byPosition ? modelNames[i] || null : null),
444
+ ...(variable ? { variable } : {}),
445
+ ...schema,
446
+ }))
447
+ modelNames
448
+ .filter(name => !models.some(m => m.name === name))
449
+ .forEach(name => models.push({ name, fields: [], collection: null, line: null }))
450
+
451
+ const defined = new Set(symbols.map(symbol => symbol.name))
452
+
453
+ return {
454
+ imports: [...imports].slice(0, LIMIT),
455
+ exports: [...exports].slice(0, LIMIT),
456
+ symbols: symbols.slice(0, LIMIT),
457
+ refs: [...refs].filter(name => !defined.has(name)).slice(0, LIMIT),
458
+ labels: [...labels].slice(0, LABEL_LIMIT),
459
+ values,
460
+ texts: [...texts].filter(value => !labels.has(value)),
461
+ comments: [...new Set(commentLines(ast.comments || []))].slice(0, COMMENT_LIMIT),
462
+ routes: routes.slice(0, LIMIT),
463
+ uiRoutes: uiRoutes.slice(0, LIMIT),
464
+ models: models.slice(0, LIMIT),
465
+ httpCalls: httpCalls.slice(0, LIMIT),
466
+ components: components.slice(0, LIMIT),
467
+ screens: screens.slice(0, LIMIT),
468
+ }
469
+ }
470
+
471
+ module.exports = {
472
+ extractJs,
473
+ }
@@ -0,0 +1,66 @@
1
+ 'use strict'
2
+
3
+ const { isSecretValue } = require('./file-filter')
4
+
5
+ const LIMIT = 500
6
+
7
+ const extractPy = code => {
8
+ const imports = new Set()
9
+ const symbols = []
10
+ const routes = []
11
+ const values = []
12
+ const comments = new Set()
13
+ let currentClass = null
14
+
15
+ code.split('\n').forEach((raw, index) => {
16
+ const line = index + 1
17
+ const fromImport = raw.match(/^\s*from\s+([\w.]+)\s+import\s/)
18
+ const plainImport = raw.match(/^\s*import\s+([\w.]+)/)
19
+ const topDef = raw.match(/^(?:async\s+)?def\s+(\w+)/)
20
+ const topClass = raw.match(/^class\s+(\w+)/)
21
+ const method = raw.match(/^\s+(?:async\s+)?def\s+(\w+)/)
22
+ const route = raw.match(/^\s*@\w+\.(route|get|post|put|patch|delete)\(\s*['"]([^'"]+)['"]/)
23
+
24
+ const constant = raw.match(/^([A-Za-z_]\w*)\s*=\s*(?:(['"])(.*?)\2|(-?\d+(?:\.\d+)?|True|False))\s*(?:#.*)?$/)
25
+ const comment = raw.match(/^\s*#\s*(.+)$/)
26
+ if (constant) {
27
+ const value = constant[3] !== undefined ? constant[3] : constant[4]
28
+ if (value.trim() && value.length <= 80 && !isSecretValue(constant[1], value)) values.push({ name: constant[1], value, line })
29
+ }
30
+ if (comment && comment[1].split(' ').length >= 3 && comment[1].length <= 120) comments.add(comment[1].trim())
31
+
32
+ if (fromImport) imports.add(fromImport[1])
33
+ else if (plainImport) imports.add(plainImport[1])
34
+
35
+ if (topClass) {
36
+ currentClass = { kind: 'class', name: topClass[1], line, methods: [] }
37
+ symbols.push(currentClass)
38
+ } else if (topDef) {
39
+ currentClass = null
40
+ symbols.push({ kind: 'function', name: topDef[1], line })
41
+ } else if (method && currentClass && method[1] !== '__init__') {
42
+ currentClass.methods.push(method[1])
43
+ }
44
+
45
+ if (route) {
46
+ routes.push({
47
+ method: route[1] === 'route' ? 'GET' : route[1].toUpperCase(),
48
+ path: route[2],
49
+ line,
50
+ })
51
+ }
52
+ })
53
+
54
+ return {
55
+ imports: [...imports].slice(0, LIMIT),
56
+ exports: [],
57
+ symbols: symbols.slice(0, LIMIT),
58
+ routes: routes.slice(0, LIMIT),
59
+ values: values.slice(0, 200),
60
+ comments: [...comments].slice(0, 50),
61
+ }
62
+ }
63
+
64
+ module.exports = {
65
+ extractPy,
66
+ }
@@ -0,0 +1,107 @@
1
+ 'use strict'
2
+
3
+ const Path = require('path')
4
+ const { resolveRef, listFiles, fileHistory, readBlobs } = require('./git-reader')
5
+ const { classify } = require('./file-filter')
6
+ const { PARSER_VERSION, extractFacts } = require('./extract')
7
+
8
+ const BATCH = 500
9
+ const CONFIG_FILES = ['package.json', 'tsconfig.json', 'jsconfig.json']
10
+
11
+ const parseJson = code => {
12
+ try {
13
+ return JSON.parse(code)
14
+ } catch (err) {
15
+ try {
16
+ return JSON.parse(
17
+ code
18
+ .replace(/("(?:\\.|[^"\\])*")|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (match, string) => string || '')
19
+ .replace(/,(\s*[}\]])/g, '$1'),
20
+ )
21
+ } catch (inner) {
22
+ return null
23
+ }
24
+ }
25
+ }
26
+
27
+ const moduleAliases = pkg =>
28
+ Object.entries((pkg && pkg._moduleAliases) || {})
29
+ .filter(([, to]) => typeof to === 'string')
30
+ .map(([from, to]) => ({ from, to: Path.posix.normalize(to), wildcard: false }))
31
+
32
+ const pathAliases = tsconfig => {
33
+ const options = (tsconfig && tsconfig.compilerOptions) || {}
34
+ const base = options.baseUrl || '.'
35
+ return Object.entries(options.paths || {})
36
+ .filter(([, targets]) => Array.isArray(targets) && typeof targets[0] === 'string')
37
+ .map(([from, targets]) => ({
38
+ from: from.replace(/\*$/, ''),
39
+ to: Path.posix.normalize(Path.posix.join(base, targets[0].replace(/\*$/, ''))).replace(/^\.$/, ''),
40
+ wildcard: from.endsWith('*'),
41
+ }))
42
+ }
43
+
44
+ const readConfig = async (dir, files) => {
45
+ const byPath = new Map(files.map(file => [file.path, file.blobHash]))
46
+ const wanted = CONFIG_FILES.filter(name => byPath.has(name))
47
+ const blobs = wanted.length ? await readBlobs(dir, wanted.map(name => byPath.get(name))) : new Map()
48
+ const json = name => (byPath.has(name) ? parseJson(blobs.get(byPath.get(name)) || '') : null)
49
+ const pkg = json('package.json')
50
+ return {
51
+ packageName: (pkg && pkg.name) || null,
52
+ packageMain: (pkg && typeof pkg.main === 'string' && pkg.main) || null,
53
+ aliases: [...moduleAliases(pkg), ...pathAliases(json('tsconfig.json') || json('jsconfig.json'))],
54
+ }
55
+ }
56
+
57
+ const buildRepoIndex = async (repo, previous) => {
58
+ const branch = repo.defaultBaseBranch
59
+ const { ref, commitSha, date } = resolveRef(repo.path, branch)
60
+ const reusable = previous && previous.parserVersion === PARSER_VERSION
61
+ if (reusable && previous.commitSha === commitSha) {
62
+ return { ...previous, name: repo.name, newlyParsed: 0 }
63
+ }
64
+
65
+ const files = listFiles(repo.path, ref).map(file => ({
66
+ ...file,
67
+ status: classify(file.path, file.size),
68
+ }))
69
+ const known = reusable ? previous.facts : {}
70
+ const facts = {}
71
+ const toParse = []
72
+ files
73
+ .filter(file => file.status === 'parsed')
74
+ .forEach(file => {
75
+ if (known[file.blobHash]) {
76
+ facts[file.blobHash] = known[file.blobHash]
77
+ } else if (!facts[file.blobHash]) {
78
+ facts[file.blobHash] = null
79
+ toParse.push(file)
80
+ }
81
+ })
82
+
83
+ for (let i = 0; i < toParse.length; i += BATCH) {
84
+ const batch = toParse.slice(i, i + BATCH)
85
+ const blobs = await readBlobs(repo.path, batch.map(file => file.blobHash))
86
+ batch.forEach(file => {
87
+ facts[file.blobHash] = extractFacts(file.path, blobs.get(file.blobHash) || '')
88
+ })
89
+ }
90
+
91
+ return {
92
+ name: repo.name,
93
+ branch,
94
+ commitSha,
95
+ date,
96
+ parserVersion: PARSER_VERSION,
97
+ ...(await readConfig(repo.path, files)),
98
+ files,
99
+ facts,
100
+ history: fileHistory(repo.path, ref, new Set(files.filter(file => file.status === 'parsed').map(file => file.path))),
101
+ newlyParsed: toParse.length,
102
+ }
103
+ }
104
+
105
+ module.exports = {
106
+ buildRepoIndex,
107
+ }