@stacksjs/defaults 0.75.61 → 0.75.63
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/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/ide/vscode/build.ts +0 -52
- package/ide/vscode/scripts/package.ts +0 -71
- package/ide/vscode/scripts/stx-contributes.ts +0 -78
- package/ide/vscode/scripts/ts-plugin.ts +0 -72
- package/ide/vscode/src/env-file.ts +0 -103
- package/ide/vscode/src/env.ts +0 -241
- package/ide/vscode/src/extension.ts +0 -163
- package/ide/vscode/src/pickier-protocol.ts +0 -51
- package/ide/vscode/src/pickier-worker.ts +0 -92
- package/ide/vscode/src/pickier.ts +0 -433
- package/ide/vscode/src/project.ts +0 -450
- package/ide/vscode/src/stacks.ts +0 -482
- package/ide/vscode/tests/build.test.ts +0 -82
- package/ide/vscode/tests/env.test.ts +0 -166
- package/ide/vscode/tests/extension.test.ts +0 -428
- package/ide/vscode/tests/fake-vscode.ts +0 -228
- package/ide/vscode/tests/manifest.test.ts +0 -141
- package/ide/vscode/tests/package.test.ts +0 -47
- package/ide/vscode/tests/pickier.test.ts +0 -202
- package/ide/vscode/tests/project.test.ts +0 -268
- package/ide/vscode/tests/ts-plugin-harness.ts +0 -39
- package/ide/vscode/tsconfig.json +0 -19
|
@@ -1,450 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The pure half of the extension: everything that can be decided from text.
|
|
3
|
-
*
|
|
4
|
-
* Nothing here imports `vscode` or touches the filesystem, so it is unit-tested
|
|
5
|
-
* directly. Where Stacks itself makes the same decision (the dev server's URL
|
|
6
|
-
* and port in `buddy dev`, the command list in `buddy list`), the rule is
|
|
7
|
-
* mirrored from that code and the source is named, so the two can be compared
|
|
8
|
-
* when either one changes.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The env files a development process reads, least to most specific - the
|
|
13
|
-
* order `autoLoadEnv()` in `@stacksjs/env` (core/env/src/plugin.ts) loads them
|
|
14
|
-
* in when no environment is set, which is what `./buddy dev` runs under.
|
|
15
|
-
*/
|
|
16
|
-
export const DEV_ENV_FILES = ['.env', '.env.local', '.env.development', '.env.development.local'] as const
|
|
17
|
-
|
|
18
|
-
/** The dev server's frontend port when `PORT` is unset (`buddy dev`). */
|
|
19
|
-
export const DEFAULT_FRONTEND_PORT = 3000
|
|
20
|
-
|
|
21
|
-
/** `buddy dev` falls back to this when `APP_URL` is unset. */
|
|
22
|
-
export const DEFAULT_APP_URL = 'stacks.localhost'
|
|
23
|
-
|
|
24
|
-
export type Env = Record<string, string | undefined>
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Parse a dotenv file into key/value pairs.
|
|
28
|
-
*
|
|
29
|
-
* Covers what Stacks' own `.env` files use: `KEY=value`, an optional `export`
|
|
30
|
-
* prefix, single/double/backtick quotes, and `#` comments (whole-line, or
|
|
31
|
-
* trailing after whitespace on an unquoted value). Multi-line values are not
|
|
32
|
-
* needed for anything this extension reads, so they are not supported.
|
|
33
|
-
*/
|
|
34
|
-
export function parseEnvFile(source: string): Record<string, string> {
|
|
35
|
-
const result: Record<string, string> = {}
|
|
36
|
-
|
|
37
|
-
for (const rawLine of source.split(/\r?\n/)) {
|
|
38
|
-
const line = rawLine.trim()
|
|
39
|
-
if (!line || line.startsWith('#'))
|
|
40
|
-
continue
|
|
41
|
-
|
|
42
|
-
const match = line.match(/^(?:export\s+)?([A-Z_a-z][\w.-]*)\s*=\s*(.*)$/)
|
|
43
|
-
if (!match)
|
|
44
|
-
continue
|
|
45
|
-
|
|
46
|
-
const key = match[1] as string
|
|
47
|
-
let value = (match[2] as string).trim()
|
|
48
|
-
const quote = value[0]
|
|
49
|
-
|
|
50
|
-
if (quote === '"' || quote === '\'' || quote === '`') {
|
|
51
|
-
const end = value.indexOf(quote, 1)
|
|
52
|
-
value = end === -1 ? value.slice(1) : value.slice(1, end)
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
const comment = value.search(/\s#/)
|
|
56
|
-
if (comment !== -1)
|
|
57
|
-
value = value.slice(0, comment).trim()
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
result[key] = value
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return result
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Merge the development env files of a project, later files winning.
|
|
68
|
-
*
|
|
69
|
-
* `read` returns a file's contents, or `undefined` when it does not exist.
|
|
70
|
-
* Ciphertext (`encrypted:...`) goes through `decrypt`, which the extension
|
|
71
|
-
* builds from `@stacksjs/env`'s own decryption and the project's keys
|
|
72
|
-
* (./env.ts), so an encrypted `APP_URL` or `PORT` previews the same URL
|
|
73
|
-
* `buddy dev` serves. Without `decrypt`, or when it cannot decrypt a value
|
|
74
|
-
* (no key), the value is skipped: a ciphertext `PORT` would only produce a
|
|
75
|
-
* nonsense URL. Decrypted values stay in this object, in memory.
|
|
76
|
-
*/
|
|
77
|
-
export function loadProjectEnv(
|
|
78
|
-
read: (file: string) => string | undefined,
|
|
79
|
-
files: readonly string[] = DEV_ENV_FILES,
|
|
80
|
-
decrypt?: (file: string, value: string) => string | undefined,
|
|
81
|
-
): Record<string, string> {
|
|
82
|
-
const env: Record<string, string> = {}
|
|
83
|
-
|
|
84
|
-
for (const file of files) {
|
|
85
|
-
const source = read(file)
|
|
86
|
-
if (source === undefined)
|
|
87
|
-
continue
|
|
88
|
-
|
|
89
|
-
for (const [key, value] of Object.entries(parseEnvFile(source))) {
|
|
90
|
-
if (!value.startsWith('encrypted:')) {
|
|
91
|
-
env[key] = value
|
|
92
|
-
continue
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const plaintext = decrypt?.(file, value)
|
|
96
|
-
if (plaintext !== undefined)
|
|
97
|
-
env[key] = plaintext
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return env
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* The pretty domain `buddy dev` serves the app on, or `null` for plain
|
|
106
|
-
* localhost. Mirrors `resolvePrettyDevDomain()` in core/buddy/src/commands/dev.ts.
|
|
107
|
-
*/
|
|
108
|
-
export function resolvePrettyDomain(appUrl: string | undefined): string | null {
|
|
109
|
-
if (!appUrl)
|
|
110
|
-
return null
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const url = new URL(/^https?:\/\//i.test(appUrl) ? appUrl : `https://${appUrl}`)
|
|
114
|
-
const hostname = url.hostname.toLowerCase()
|
|
115
|
-
if (isLoopbackHost(hostname))
|
|
116
|
-
return null
|
|
117
|
-
|
|
118
|
-
return hostname
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return null
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function isLoopbackHost(hostname: string): boolean {
|
|
126
|
-
const host = hostname.toLowerCase().replace(/^\[|\]$/g, '')
|
|
127
|
-
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0'
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Whether a URL points at this machine, so it may need port forwarding when remote. */
|
|
131
|
-
export function isLoopbackUrl(url: string): boolean {
|
|
132
|
-
try {
|
|
133
|
-
return isLoopbackHost(new URL(url).hostname)
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return false
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Normalize an entry path to `/path?query#hash`. Mirrors
|
|
142
|
-
* `normalizeDevelopmentEntryPath()` in core/buddy/src/commands/dev.ts.
|
|
143
|
-
*/
|
|
144
|
-
export function normalizeEntryPath(path: string | undefined): string {
|
|
145
|
-
const value = path?.trim()
|
|
146
|
-
if (!value || value === '/')
|
|
147
|
-
return '/'
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const parsed = new URL(value, 'http://stacks.localhost')
|
|
151
|
-
return `${parsed.pathname}${parsed.search}${parsed.hash}`
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
return '/'
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* The application path a project configured explicitly: `APP_PATH`, else
|
|
160
|
-
* `appPath` in `config/app.ts` - the same two places, read the same way, as
|
|
161
|
-
* `configuredApplicationPath()` in core/buddy/src/commands/dev.ts.
|
|
162
|
-
*
|
|
163
|
-
* `buddy dev` additionally guesses an entry page from the views it finds. The
|
|
164
|
-
* extension does not repeat that guess: when it started the dev server itself,
|
|
165
|
-
* it reads the exact URL from the banner instead (`parseDevBannerUrl`).
|
|
166
|
-
*/
|
|
167
|
-
export function configuredAppPath(env: Env, appConfigSource?: string): string {
|
|
168
|
-
if (env.APP_PATH)
|
|
169
|
-
return normalizeEntryPath(env.APP_PATH)
|
|
170
|
-
|
|
171
|
-
const fromConfig = appConfigSource?.match(/^[ \t]*appPath\s*:\s*(['"`])([^'"`]+)\1/m)?.[2]
|
|
172
|
-
return normalizeEntryPath(fromConfig)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Join a base URL and an entry path. Mirrors `developmentUrl()` in dev.ts. */
|
|
176
|
-
export function developmentUrl(baseUrl: string, entryPath: string): string {
|
|
177
|
-
const normalized = normalizeEntryPath(entryPath)
|
|
178
|
-
if (normalized === '/')
|
|
179
|
-
return baseUrl.replace(/\/$/, '')
|
|
180
|
-
|
|
181
|
-
return new URL(normalized, `${baseUrl.replace(/\/$/, '')}/`).toString()
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export interface Endpoint {
|
|
185
|
-
host: string
|
|
186
|
-
port: number
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export interface PreviewCandidate extends Endpoint {
|
|
190
|
-
url: string
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export interface PreviewPlan {
|
|
194
|
-
/** The frontend dev server itself. If this is not listening, nothing is running. */
|
|
195
|
-
server: Endpoint
|
|
196
|
-
/** URLs to open, best first. The last one is always the plain localhost URL. */
|
|
197
|
-
candidates: PreviewCandidate[]
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Work out where the running dev server can be reached, the way `buddy dev`
|
|
202
|
-
* decides it (core/buddy/src/commands/dev.ts, `startDevelopmentServer`):
|
|
203
|
-
*
|
|
204
|
-
* - the frontend binds `PORT`, else 3000;
|
|
205
|
-
* - a non-loopback `APP_URL` (default `stacks.localhost`) is served as
|
|
206
|
-
* `https://<domain>` through the rpx proxy on :443, unless
|
|
207
|
-
* `STACKS_DEV_LOCALHOST=1` opts out;
|
|
208
|
-
* - otherwise the app is at `http://localhost:<port>`.
|
|
209
|
-
*
|
|
210
|
-
* Whether the proxy actually came up is only known at runtime, so the pretty
|
|
211
|
-
* URL is a candidate to probe, with localhost behind it.
|
|
212
|
-
*/
|
|
213
|
-
export function planPreview(input: { env: Env, appConfigSource?: string, preferLocalhost?: boolean }): PreviewPlan {
|
|
214
|
-
const { env } = input
|
|
215
|
-
const port = Number(env.PORT) || DEFAULT_FRONTEND_PORT
|
|
216
|
-
const entryPath = configuredAppPath(env, input.appConfigSource)
|
|
217
|
-
const local: PreviewCandidate = {
|
|
218
|
-
url: developmentUrl(`http://localhost:${port}`, entryPath),
|
|
219
|
-
host: 'localhost',
|
|
220
|
-
port,
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const localhostOnly = input.preferLocalhost === true || env.STACKS_DEV_LOCALHOST === '1'
|
|
224
|
-
const domain = localhostOnly ? null : resolvePrettyDomain(env.APP_URL ?? DEFAULT_APP_URL)
|
|
225
|
-
const candidates = domain
|
|
226
|
-
? [{ url: developmentUrl(`https://${domain}`, entryPath), host: domain, port: 443 }, local]
|
|
227
|
-
: [local]
|
|
228
|
-
|
|
229
|
-
return { server: { host: local.host, port }, candidates }
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Resolves whether something accepts TCP connections on `host:port`. */
|
|
233
|
-
export type Probe = (host: string, port: number) => Promise<boolean>
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* The URL to preview, or `undefined` when the dev server is not listening.
|
|
237
|
-
*
|
|
238
|
-
* The frontend port is the readiness signal, not the proxy: the rpx daemon on
|
|
239
|
-
* :443 is shared between projects, so it can answer while this project's dev
|
|
240
|
-
* server is down. Once the frontend answers, the first candidate whose
|
|
241
|
-
* endpoint accepts connections wins.
|
|
242
|
-
*/
|
|
243
|
-
export async function choosePreviewUrl(plan: PreviewPlan, probe: Probe): Promise<string | undefined> {
|
|
244
|
-
if (!await probe(plan.server.host, plan.server.port))
|
|
245
|
-
return undefined
|
|
246
|
-
|
|
247
|
-
for (const candidate of plan.candidates) {
|
|
248
|
-
const isServer = candidate.host === plan.server.host && candidate.port === plan.server.port
|
|
249
|
-
if (isServer || await probe(candidate.host, candidate.port))
|
|
250
|
-
return candidate.url
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return undefined
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// eslint-disable-next-line no-control-regex
|
|
257
|
-
const ANSI_PATTERN = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007\u001B]*(?:\u0007|\u001B\\)|[@-Z\\-_])/g
|
|
258
|
-
|
|
259
|
-
export function stripAnsi(text: string): string {
|
|
260
|
-
return text.replace(ANSI_PATTERN, '')
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* The app URL from `buddy dev`'s ready banner, e.g. ` ➜ App : https://stacks.localhost/dashboard`.
|
|
265
|
-
*
|
|
266
|
-
* This is the exact URL the dev server settled on - after port shifts, the
|
|
267
|
-
* proxy check and the entry-page guess - so it beats anything `planPreview`
|
|
268
|
-
* can infer from configuration.
|
|
269
|
-
*/
|
|
270
|
-
export function parseDevBannerUrl(output: string): string | undefined {
|
|
271
|
-
for (const line of stripAnsi(output).split(/\r?\n/)) {
|
|
272
|
-
const match = line.match(/➜\s+(?:App|Site)\s*:\s*(https?:\/\/\S+)/)
|
|
273
|
-
if (match)
|
|
274
|
-
return match[1]
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return undefined
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export interface BuddyCommandArgument {
|
|
281
|
-
name: string
|
|
282
|
-
required: boolean
|
|
283
|
-
variadic: boolean
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export interface BuddyCommand {
|
|
287
|
-
name: string
|
|
288
|
-
description: string
|
|
289
|
-
usage?: string
|
|
290
|
-
arguments: BuddyCommandArgument[]
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** `make:model`, `dev`, `env:get`, `test:types` - the shape of a buddy command name. */
|
|
294
|
-
const COMMAND_NAME = /^[a-z][\w-]*(?::[\w-]+)*$/i
|
|
295
|
-
|
|
296
|
-
export function isBuddyCommandName(name: string): boolean {
|
|
297
|
-
return COMMAND_NAME.test(name)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Parse `./buddy list --json`. Returns `undefined` when the output is not the
|
|
302
|
-
* JSON inventory (an older buddy without `--json`, or a crash), so the caller
|
|
303
|
-
* can fall back to the text listing.
|
|
304
|
-
*
|
|
305
|
-
* Anything printed before the JSON (the env loader's status line, say) is skipped.
|
|
306
|
-
*/
|
|
307
|
-
export function parseBuddyListJson(output: string): BuddyCommand[] | undefined {
|
|
308
|
-
const start = output.indexOf('{')
|
|
309
|
-
const end = output.lastIndexOf('}')
|
|
310
|
-
if (start === -1 || end <= start)
|
|
311
|
-
return undefined
|
|
312
|
-
|
|
313
|
-
let data: unknown
|
|
314
|
-
try {
|
|
315
|
-
data = JSON.parse(output.slice(start, end + 1))
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
return undefined
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const commands = (data as { commands?: unknown })?.commands
|
|
322
|
-
if (!Array.isArray(commands))
|
|
323
|
-
return undefined
|
|
324
|
-
|
|
325
|
-
const result: BuddyCommand[] = []
|
|
326
|
-
for (const entry of commands as Array<Record<string, unknown>>) {
|
|
327
|
-
const name = entry?.name
|
|
328
|
-
if (typeof name !== 'string' || !isBuddyCommandName(name))
|
|
329
|
-
continue
|
|
330
|
-
|
|
331
|
-
const args = Array.isArray(entry.arguments) ? entry.arguments as Array<Record<string, unknown>> : []
|
|
332
|
-
result.push({
|
|
333
|
-
name,
|
|
334
|
-
description: typeof entry.description === 'string' ? entry.description : '',
|
|
335
|
-
usage: typeof entry.usage === 'string' ? entry.usage.replace(/^\$\s*/, '') : undefined,
|
|
336
|
-
arguments: args
|
|
337
|
-
.filter(argument => typeof argument?.name === 'string')
|
|
338
|
-
.map(argument => ({
|
|
339
|
-
name: argument.name as string,
|
|
340
|
-
required: argument.required === true,
|
|
341
|
-
variadic: argument.variadic === true,
|
|
342
|
-
})),
|
|
343
|
-
})
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return sortCommands(result)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Parse the text `./buddy list` prints:
|
|
351
|
-
*
|
|
352
|
-
* ```text
|
|
353
|
-
* Available Commands:
|
|
354
|
-
*
|
|
355
|
-
* Development:
|
|
356
|
-
* dev Start development server
|
|
357
|
-
*
|
|
358
|
-
* make:
|
|
359
|
-
* make:model Create a new Model
|
|
360
|
-
*
|
|
361
|
-
* Total: 338 commands
|
|
362
|
-
* ```
|
|
363
|
-
*
|
|
364
|
-
* Commands are the indented lines; group headings and the total are not.
|
|
365
|
-
* The text form carries no argument information.
|
|
366
|
-
*/
|
|
367
|
-
export function parseBuddyListText(output: string): BuddyCommand[] {
|
|
368
|
-
const seen = new Set<string>()
|
|
369
|
-
const result: BuddyCommand[] = []
|
|
370
|
-
|
|
371
|
-
for (const line of stripAnsi(output).split(/\r?\n/)) {
|
|
372
|
-
const match = line.match(/^ {2,4}(\S+)(?:\s+(\S.*))?$/)
|
|
373
|
-
const name = match?.[1]
|
|
374
|
-
if (!name || !isBuddyCommandName(name) || seen.has(name))
|
|
375
|
-
continue
|
|
376
|
-
|
|
377
|
-
seen.add(name)
|
|
378
|
-
result.push({ name, description: match[2]?.trim() ?? '', arguments: [] })
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return sortCommands(result)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/** JSON inventory when the output is one, the text listing otherwise. */
|
|
385
|
-
export function parseBuddyList(output: string): BuddyCommand[] {
|
|
386
|
-
return parseBuddyListJson(output) ?? parseBuddyListText(output)
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function sortCommands(commands: BuddyCommand[]): BuddyCommand[] {
|
|
390
|
-
return commands.sort((a, b) => a.name.localeCompare(b.name))
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* The project's own CLI, relative to the workspace root. Never `buddy` off the
|
|
395
|
-
* PATH or from `node_modules/.bin`: there, `buddy` may well be the dependency
|
|
396
|
-
* bot (`@buddysh/buddy`), a different program that happens to share the name.
|
|
397
|
-
*/
|
|
398
|
-
export const BUDDY = './buddy'
|
|
399
|
-
|
|
400
|
-
/** `./buddy <name> <args>`. Throws on a name that is not a buddy command. */
|
|
401
|
-
export function buddyCommandLine(name: string, args: string | readonly string[] = ''): string {
|
|
402
|
-
if (!isBuddyCommandName(name))
|
|
403
|
-
throw new Error(`Not a buddy command name: ${JSON.stringify(name)}`)
|
|
404
|
-
|
|
405
|
-
const rest = (typeof args === 'string' ? args : args.join(' ')).trim()
|
|
406
|
-
return rest ? `${BUDDY} ${name} ${rest}` : `${BUDDY} ${name}`
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* A command line from free text typed into the picker, e.g. `migrate --diff`.
|
|
411
|
-
* A leading `buddy`, `./buddy`, `bud` or `stacks` is dropped so pasting a
|
|
412
|
-
* command from the docs still runs the project's own `./buddy`.
|
|
413
|
-
*/
|
|
414
|
-
export function freeTextCommandLine(text: string): string | undefined {
|
|
415
|
-
const trimmed = text.trim()
|
|
416
|
-
if (!trimmed)
|
|
417
|
-
return undefined
|
|
418
|
-
|
|
419
|
-
const rest = trimmed.replace(/^(?:\.\/)?(?:buddy|bud|stacks)(?=\s|$)/, '').trim()
|
|
420
|
-
return rest ? `${BUDDY} ${rest}` : BUDDY
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/** Placeholder text for a command's positional arguments, e.g. `<name> [type]`. */
|
|
424
|
-
export function argumentHint(command: BuddyCommand): string {
|
|
425
|
-
return command.arguments
|
|
426
|
-
.map((argument) => {
|
|
427
|
-
const name = argument.variadic ? `${argument.name}...` : argument.name
|
|
428
|
-
return argument.required ? `<${name}>` : `[${name}]`
|
|
429
|
-
})
|
|
430
|
-
.join(' ')
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
export function requiresArguments(command: BuddyCommand): boolean {
|
|
434
|
-
return command.arguments.some(argument => argument.required)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Which workspace folder is the Stacks project: the preferred one (the active
|
|
439
|
-
* editor's) when it qualifies, else the first that does.
|
|
440
|
-
*/
|
|
441
|
-
export function pickProjectRoot(
|
|
442
|
-
folders: readonly string[],
|
|
443
|
-
preferred: string | undefined,
|
|
444
|
-
isProject: (folder: string) => boolean,
|
|
445
|
-
): string | undefined {
|
|
446
|
-
if (preferred && folders.includes(preferred) && isProject(preferred))
|
|
447
|
-
return preferred
|
|
448
|
-
|
|
449
|
-
return folders.find(folder => isProject(folder))
|
|
450
|
-
}
|