@stacksjs/defaults 0.75.40 → 0.75.41
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/ai/skills/stacks-desktop/SKILL.md +12 -0
- package/app/Actions/Payment/CreatePaymentIntentAction.ts +1 -1
- package/app/Actions/Payment/CreateSetupIntentAction.ts +1 -1
- package/ide/vscode/.vscode/settings.json +1 -1
- package/ide/vscode/README.md +65 -20
- package/ide/vscode/package.json +102 -12
- package/ide/vscode/src/extension.ts +59 -0
- package/ide/vscode/src/project.ts +440 -0
- package/ide/vscode/src/stacks.ts +475 -0
- package/ide/vscode/src/vscode.d.ts +181 -0
- package/ide/vscode/tests/extension.test.ts +420 -0
- package/ide/vscode/tests/project.test.ts +268 -0
- package/ide/vscode/tsconfig.json +19 -0
- package/package.json +4 -4
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import type * as vscode from 'vscode'
|
|
2
|
+
import type { BuddyCommand, PreviewPlan, Probe } from './project'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
argumentHint,
|
|
6
|
+
buddyCommandLine,
|
|
7
|
+
choosePreviewUrl,
|
|
8
|
+
freeTextCommandLine,
|
|
9
|
+
isLoopbackUrl,
|
|
10
|
+
loadProjectEnv,
|
|
11
|
+
parseBuddyListJson,
|
|
12
|
+
parseBuddyListText,
|
|
13
|
+
parseDevBannerUrl,
|
|
14
|
+
pickProjectRoot,
|
|
15
|
+
planPreview,
|
|
16
|
+
requiresArguments,
|
|
17
|
+
} from './project'
|
|
18
|
+
|
|
19
|
+
type Vscode = typeof vscode
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What the extension needs from the machine, injected so the controller can
|
|
23
|
+
* be tested with a fake `vscode` and no real processes, sockets or files.
|
|
24
|
+
*/
|
|
25
|
+
export interface Host {
|
|
26
|
+
exists: (path: string) => boolean
|
|
27
|
+
/** File contents, or `undefined` when the file does not exist. */
|
|
28
|
+
read: (path: string) => string | undefined
|
|
29
|
+
/** Run `file` with `args` in `cwd` and resolve its stdout. */
|
|
30
|
+
exec: (file: string, args: string[], cwd: string) => Promise<string>
|
|
31
|
+
probe: Probe
|
|
32
|
+
sleep: (ms: number) => Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const COMMANDS = {
|
|
36
|
+
openPreview: 'stacks.openPreview',
|
|
37
|
+
startDevServer: 'stacks.startDevServer',
|
|
38
|
+
runBuddyCommand: 'stacks.runBuddyCommand',
|
|
39
|
+
} as const
|
|
40
|
+
|
|
41
|
+
export const DEV_TERMINAL = 'Stacks Dev'
|
|
42
|
+
export const BUDDY_TERMINAL = 'Stacks Buddy'
|
|
43
|
+
|
|
44
|
+
export interface Timing {
|
|
45
|
+
/** How long to wait for a new terminal's shell integration before typing blind. */
|
|
46
|
+
shellIntegrationMs: number
|
|
47
|
+
/** How long `Start Dev Server` waits for the server before giving up on the preview. */
|
|
48
|
+
devReadyMs: number
|
|
49
|
+
/** How long to trust the banner alone before also probing ports. */
|
|
50
|
+
bannerGraceMs: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULT_TIMING: Timing = {
|
|
54
|
+
shellIntegrationMs: 3000,
|
|
55
|
+
devReadyMs: 120_000,
|
|
56
|
+
bannerGraceMs: 15_000,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface DevRun {
|
|
60
|
+
terminal: vscode.Terminal
|
|
61
|
+
running: boolean
|
|
62
|
+
/** Whether output is being read through shell integration (so the banner URL will arrive). */
|
|
63
|
+
capturing: boolean
|
|
64
|
+
url?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface CommandItem extends vscode.QuickPickItem {
|
|
68
|
+
command?: BuddyCommand
|
|
69
|
+
freeText?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createStacksExtension(api: Vscode, host: Host, timingOverrides: Partial<Timing> = {}): {
|
|
73
|
+
activate: (context: vscode.ExtensionContext) => void
|
|
74
|
+
deactivate: () => void
|
|
75
|
+
} {
|
|
76
|
+
const timing: Timing = { ...DEFAULT_TIMING, ...timingOverrides }
|
|
77
|
+
let devRun: DevRun | undefined
|
|
78
|
+
let statusItem: vscode.StatusBarItem | undefined
|
|
79
|
+
const commandLists = new Map<string, Promise<BuddyCommand[]>>()
|
|
80
|
+
|
|
81
|
+
const settings = () => api.workspace.getConfiguration('stacks')
|
|
82
|
+
|
|
83
|
+
function isProject(folder: string): boolean {
|
|
84
|
+
return host.exists(join(folder, 'buddy')) || host.exists(join(folder, 'config', 'app.ts'))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function projectRoot(): string | undefined {
|
|
88
|
+
const folders = (api.workspace.workspaceFolders ?? []).map(folder => folder.uri.fsPath)
|
|
89
|
+
const activeUri = api.window.activeTextEditor?.document.uri
|
|
90
|
+
const preferred = activeUri ? api.workspace.getWorkspaceFolder(activeUri)?.uri.fsPath : undefined
|
|
91
|
+
const root = pickProjectRoot(folders, preferred, isProject)
|
|
92
|
+
|
|
93
|
+
if (!root)
|
|
94
|
+
void api.window.showErrorMessage('Stacks: open a Stacks project folder (one containing ./buddy) first.')
|
|
95
|
+
|
|
96
|
+
return root
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The root, if it has the `./buddy` the command is about to run. */
|
|
100
|
+
function buddyRoot(): string | undefined {
|
|
101
|
+
const root = projectRoot()
|
|
102
|
+
if (root && !host.exists(join(root, 'buddy'))) {
|
|
103
|
+
void api.window.showErrorMessage(`Stacks: there is no ./buddy in ${root}. Run the command from the project root, or run \`buddy install\` there first.`)
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return root
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function previewPlan(root: string): PreviewPlan {
|
|
111
|
+
return planPreview({
|
|
112
|
+
env: loadProjectEnv(file => host.read(join(root, file))),
|
|
113
|
+
appConfigSource: host.read(join(root, 'config', 'app.ts')),
|
|
114
|
+
preferLocalhost: settings().get<boolean>('preview.preferLocalhost', false),
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function openInSimpleBrowser(url: string): Promise<void> {
|
|
119
|
+
let target = api.Uri.parse(url)
|
|
120
|
+
// In a remote window (SSH, WSL, Codespaces) the dev server's localhost is
|
|
121
|
+
// the remote machine's; this forwards the port and returns the local URL.
|
|
122
|
+
if (isLoopbackUrl(url))
|
|
123
|
+
target = await api.env.asExternalUri(target)
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// The Simple Browser's extension-facing command, which takes a column,
|
|
127
|
+
// so the preview opens beside the editor instead of replacing it.
|
|
128
|
+
await api.commands.executeCommand('simpleBrowser.api.open', target, {
|
|
129
|
+
viewColumn: api.ViewColumn.Beside,
|
|
130
|
+
preserveFocus: true,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
await api.commands.executeCommand('simpleBrowser.show', target.toString(true))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function openPreview(): Promise<void> {
|
|
139
|
+
const override = settings().get<string>('preview.url', '').trim()
|
|
140
|
+
if (override)
|
|
141
|
+
return openInSimpleBrowser(override)
|
|
142
|
+
|
|
143
|
+
const root = projectRoot()
|
|
144
|
+
if (!root)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if (devRun?.running && devRun.url)
|
|
148
|
+
return openInSimpleBrowser(devRun.url)
|
|
149
|
+
|
|
150
|
+
const plan = previewPlan(root)
|
|
151
|
+
const url = await choosePreviewUrl(plan, host.probe)
|
|
152
|
+
if (url)
|
|
153
|
+
return openInSimpleBrowser(url)
|
|
154
|
+
|
|
155
|
+
const choice = await api.window.showWarningMessage(
|
|
156
|
+
`Stacks: nothing is listening on ${plan.server.host}:${plan.server.port}. Is the dev server running?`,
|
|
157
|
+
'Start Dev Server',
|
|
158
|
+
'Open Anyway',
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if (choice === 'Start Dev Server')
|
|
162
|
+
await startDevServer({ openPreview: true })
|
|
163
|
+
else if (choice === 'Open Anyway')
|
|
164
|
+
await openInSimpleBrowser(plan.candidates[0]!.url)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function findTerminal(name: string): vscode.Terminal | undefined {
|
|
168
|
+
return api.window.terminals.find(terminal => terminal.name === name && terminal.exitStatus === undefined)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function waitForShellIntegration(terminal: vscode.Terminal): Promise<vscode.TerminalShellIntegration | undefined> {
|
|
172
|
+
if (terminal.shellIntegration)
|
|
173
|
+
return Promise.resolve(terminal.shellIntegration)
|
|
174
|
+
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
subscription.dispose()
|
|
178
|
+
resolve(undefined)
|
|
179
|
+
}, timing.shellIntegrationMs)
|
|
180
|
+
|
|
181
|
+
const subscription = api.window.onDidChangeTerminalShellIntegration((event) => {
|
|
182
|
+
if (event.terminal !== terminal)
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
clearTimeout(timer)
|
|
186
|
+
subscription.dispose()
|
|
187
|
+
resolve(event.shellIntegration)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Run the dev server in `run.terminal`. With shell integration, the output is
|
|
194
|
+
* read to pick up the banner URL and the end of the command; without it the
|
|
195
|
+
* command is typed into the terminal and the ports are probed instead.
|
|
196
|
+
*/
|
|
197
|
+
async function launchDevServer(run: DevRun, commandLine: string): Promise<void> {
|
|
198
|
+
const integration = await waitForShellIntegration(run.terminal)
|
|
199
|
+
if (!integration) {
|
|
200
|
+
run.terminal.sendText(commandLine)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const execution = integration.executeCommand(commandLine)
|
|
205
|
+
run.capturing = true
|
|
206
|
+
|
|
207
|
+
const ended = api.window.onDidEndTerminalShellExecution((event) => {
|
|
208
|
+
if (event.execution !== execution)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
run.running = false
|
|
212
|
+
run.url = undefined
|
|
213
|
+
ended.dispose()
|
|
214
|
+
updateStatus()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
void (async () => {
|
|
218
|
+
let tail = ''
|
|
219
|
+
for await (const chunk of execution.read()) {
|
|
220
|
+
if (run.url)
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
tail = (tail + chunk).slice(-16_384)
|
|
224
|
+
run.url = parseDevBannerUrl(tail)
|
|
225
|
+
}
|
|
226
|
+
})().catch(() => {
|
|
227
|
+
run.capturing = false
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function openWhenReady(run: DevRun, root: string): Promise<void> {
|
|
232
|
+
const plan = previewPlan(root)
|
|
233
|
+
const startedAt = Date.now()
|
|
234
|
+
updateStatus('starting')
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
while (devRun === run && run.running && Date.now() - startedAt < timing.devReadyMs) {
|
|
238
|
+
if (run.url)
|
|
239
|
+
return await openInSimpleBrowser(run.url)
|
|
240
|
+
|
|
241
|
+
if (!run.capturing || Date.now() - startedAt > timing.bannerGraceMs) {
|
|
242
|
+
const url = await choosePreviewUrl(plan, host.probe)
|
|
243
|
+
if (url)
|
|
244
|
+
return await openInSimpleBrowser(url)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await host.sleep(1000)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
updateStatus()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function startDevServer(options: { openPreview?: boolean } = {}): Promise<void> {
|
|
256
|
+
const root = buddyRoot()
|
|
257
|
+
if (!root)
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
const existing = findTerminal(DEV_TERMINAL)
|
|
261
|
+
const knownFinished = existing !== undefined && devRun?.terminal === existing && !devRun.running
|
|
262
|
+
|
|
263
|
+
// A dev terminal this extension did not see finish may still be serving:
|
|
264
|
+
// it was started earlier in this window, or revived with the window.
|
|
265
|
+
// Typing `./buddy dev` into it would go to the running server's stdin.
|
|
266
|
+
if (existing && !knownFinished) {
|
|
267
|
+
existing.show(true)
|
|
268
|
+
const choice = await api.window.showInformationMessage(
|
|
269
|
+
'Stacks: the dev server terminal is already open.',
|
|
270
|
+
'Open Preview',
|
|
271
|
+
'Restart',
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if (choice === 'Open Preview')
|
|
275
|
+
return openPreview()
|
|
276
|
+
if (choice !== 'Restart')
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
existing.dispose()
|
|
280
|
+
devRun = undefined
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const shouldOpenPreview = options.openPreview ?? settings().get<boolean>('devServer.openPreview', true)
|
|
284
|
+
const terminal = knownFinished && existing
|
|
285
|
+
? existing
|
|
286
|
+
: api.window.createTerminal({
|
|
287
|
+
name: DEV_TERMINAL,
|
|
288
|
+
cwd: root,
|
|
289
|
+
// The preview opens inside VS Code, so `buddy dev` should not also
|
|
290
|
+
// open the system browser (core/buddy/src/commands/dev.ts).
|
|
291
|
+
env: shouldOpenPreview ? { STACKS_DEV_NO_OPEN: '1' } : undefined,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
terminal.show(true)
|
|
295
|
+
|
|
296
|
+
const run: DevRun = { terminal, running: true, capturing: false }
|
|
297
|
+
devRun = run
|
|
298
|
+
await launchDevServer(run, buddyCommandLine('dev', settings().get<string[]>('devServer.args', [])))
|
|
299
|
+
|
|
300
|
+
if (shouldOpenPreview)
|
|
301
|
+
void openWhenReady(run, root)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function listBuddyCommands(root: string): Promise<BuddyCommand[]> {
|
|
305
|
+
const cached = commandLists.get(root)
|
|
306
|
+
if (cached)
|
|
307
|
+
return cached
|
|
308
|
+
|
|
309
|
+
const buddy = join(root, 'buddy')
|
|
310
|
+
const listing = (async () => {
|
|
311
|
+
const json = await host.exec(buddy, ['list', '--json'], root).catch(() => '')
|
|
312
|
+
const fromJson = parseBuddyListJson(json)
|
|
313
|
+
if (fromJson?.length)
|
|
314
|
+
return fromJson
|
|
315
|
+
|
|
316
|
+
const fromText = parseBuddyListText(await host.exec(buddy, ['list'], root))
|
|
317
|
+
if (!fromText.length)
|
|
318
|
+
throw new Error('`./buddy list` printed no commands')
|
|
319
|
+
|
|
320
|
+
return fromText
|
|
321
|
+
})()
|
|
322
|
+
|
|
323
|
+
commandLists.set(root, listing)
|
|
324
|
+
listing.catch(() => commandLists.delete(root))
|
|
325
|
+
return listing
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function runInBuddyTerminal(root: string, commandLine: string): void {
|
|
329
|
+
const terminal = findTerminal(BUDDY_TERMINAL) ?? api.window.createTerminal({ name: BUDDY_TERMINAL, cwd: root })
|
|
330
|
+
terminal.show()
|
|
331
|
+
terminal.sendText(commandLine)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function runPickedCommand(root: string, item: CommandItem): Promise<void> {
|
|
335
|
+
if (item.freeText !== undefined) {
|
|
336
|
+
const commandLine = freeTextCommandLine(item.freeText)
|
|
337
|
+
if (commandLine)
|
|
338
|
+
runInBuddyTerminal(root, commandLine)
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const command = item.command
|
|
343
|
+
if (!command)
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
let args = ''
|
|
347
|
+
if (command.arguments.length) {
|
|
348
|
+
const required = requiresArguments(command)
|
|
349
|
+
const answer = await api.window.showInputBox({
|
|
350
|
+
title: buddyCommandLine(command.name),
|
|
351
|
+
prompt: command.usage ? `Usage: ${command.usage}` : command.description,
|
|
352
|
+
placeHolder: argumentHint(command),
|
|
353
|
+
ignoreFocusOut: true,
|
|
354
|
+
validateInput: value => required && !value.trim() ? `Arguments: ${argumentHint(command)}` : undefined,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
if (answer === undefined)
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
args = answer
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
runInBuddyTerminal(root, buddyCommandLine(command.name, args))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function runBuddyCommand(): Promise<void> {
|
|
367
|
+
const root = buddyRoot()
|
|
368
|
+
if (!root)
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
const picker = api.window.createQuickPick<CommandItem>()
|
|
372
|
+
picker.title = 'Run Buddy Command'
|
|
373
|
+
picker.placeholder = 'Loading ./buddy list ... or type any command, e.g. migrate --diff'
|
|
374
|
+
picker.matchOnDescription = true
|
|
375
|
+
picker.busy = true
|
|
376
|
+
|
|
377
|
+
let commands: BuddyCommand[] = []
|
|
378
|
+
const render = () => {
|
|
379
|
+
const value = picker.value.trim()
|
|
380
|
+
const items: CommandItem[] = commands.map(command => ({
|
|
381
|
+
label: command.name,
|
|
382
|
+
description: command.description,
|
|
383
|
+
detail: command.arguments.length ? `${command.name} ${argumentHint(command)}` : undefined,
|
|
384
|
+
command,
|
|
385
|
+
}))
|
|
386
|
+
|
|
387
|
+
// Typed text that is not exactly a command name runs as typed. It goes
|
|
388
|
+
// last so Enter on a partial name still picks the filtered command.
|
|
389
|
+
if (value && !commands.some(command => command.name === value))
|
|
390
|
+
items.push({ label: freeTextCommandLine(value) ?? value, description: 'Run as typed', alwaysShow: true, freeText: value })
|
|
391
|
+
|
|
392
|
+
picker.items = items
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
picker.onDidChangeValue(render)
|
|
396
|
+
picker.onDidHide(() => picker.dispose())
|
|
397
|
+
picker.onDidAccept(() => {
|
|
398
|
+
const typed = picker.value.trim()
|
|
399
|
+
const item = picker.selectedItems[0] ?? (typed ? { label: typed, freeText: typed } : undefined)
|
|
400
|
+
picker.hide()
|
|
401
|
+
if (item)
|
|
402
|
+
void runPickedCommand(root, item)
|
|
403
|
+
})
|
|
404
|
+
picker.show()
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
commands = await listBuddyCommands(root)
|
|
408
|
+
picker.placeholder = 'Pick a buddy command, or type one with arguments'
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
picker.placeholder = 'Could not read ./buddy list - type a command to run, e.g. migrate --diff'
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
picker.busy = false
|
|
415
|
+
render()
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function updateStatus(state: 'idle' | 'starting' = 'idle'): void {
|
|
420
|
+
if (!statusItem)
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
if (!settings().get<boolean>('statusBar.enabled', true)) {
|
|
424
|
+
statusItem.hide()
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
statusItem.text = state === 'starting' ? '$(loading~spin) Stacks' : '$(globe) Stacks'
|
|
429
|
+
statusItem.tooltip = state === 'starting'
|
|
430
|
+
? 'Waiting for the Stacks dev server...'
|
|
431
|
+
: 'Open the Stacks dev server in the Simple Browser'
|
|
432
|
+
statusItem.show()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function activate(context: vscode.ExtensionContext): void {
|
|
436
|
+
// Commands are hidden from the palette until a Stacks project activated us.
|
|
437
|
+
void api.commands.executeCommand('setContext', 'stacks.isProject', true)
|
|
438
|
+
|
|
439
|
+
statusItem = api.window.createStatusBarItem('stacks.preview', api.StatusBarAlignment.Left, 0)
|
|
440
|
+
statusItem.name = 'Stacks Preview'
|
|
441
|
+
statusItem.command = COMMANDS.openPreview
|
|
442
|
+
updateStatus()
|
|
443
|
+
|
|
444
|
+
// Custom commands live in app/Commands and need no registration, so a new
|
|
445
|
+
// file there is a new buddy command: forget the cached list.
|
|
446
|
+
const commandFiles = api.workspace.createFileSystemWatcher('**/app/Commands/**')
|
|
447
|
+
const forgetCommands = () => commandLists.clear()
|
|
448
|
+
|
|
449
|
+
context.subscriptions.push(
|
|
450
|
+
statusItem,
|
|
451
|
+
commandFiles,
|
|
452
|
+
commandFiles.onDidCreate(forgetCommands),
|
|
453
|
+
commandFiles.onDidChange(forgetCommands),
|
|
454
|
+
commandFiles.onDidDelete(forgetCommands),
|
|
455
|
+
api.commands.registerCommand(COMMANDS.openPreview, openPreview),
|
|
456
|
+
api.commands.registerCommand(COMMANDS.startDevServer, () => startDevServer()),
|
|
457
|
+
api.commands.registerCommand(COMMANDS.runBuddyCommand, runBuddyCommand),
|
|
458
|
+
api.window.onDidCloseTerminal((terminal) => {
|
|
459
|
+
if (devRun?.terminal === terminal)
|
|
460
|
+
devRun = undefined
|
|
461
|
+
}),
|
|
462
|
+
api.workspace.onDidChangeConfiguration((event) => {
|
|
463
|
+
if (event.affectsConfiguration('stacks.statusBar'))
|
|
464
|
+
updateStatus()
|
|
465
|
+
}),
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function deactivate(): void {
|
|
470
|
+
devRun = undefined
|
|
471
|
+
commandLists.clear()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return { activate, deactivate }
|
|
475
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of the VS Code extension API this extension uses.
|
|
3
|
+
*
|
|
4
|
+
* `@types/vscode` is the complete declaration, but it would be the only
|
|
5
|
+
* dependency of a package that otherwise has none, and the extension calls a
|
|
6
|
+
* couple of dozen members. The names and signatures below are copied from
|
|
7
|
+
* `@types/vscode` (engine ^1.93 for the terminal shell-integration API), so
|
|
8
|
+
* swapping this file for the real package later is a deletion, not a rewrite.
|
|
9
|
+
*/
|
|
10
|
+
declare module 'vscode' {
|
|
11
|
+
export interface Disposable {
|
|
12
|
+
dispose: () => unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Event<T> = (listener: (event: T) => unknown) => Disposable
|
|
16
|
+
|
|
17
|
+
export class Uri {
|
|
18
|
+
static parse(value: string): Uri
|
|
19
|
+
static file(path: string): Uri
|
|
20
|
+
readonly scheme: string
|
|
21
|
+
readonly authority: string
|
|
22
|
+
readonly path: string
|
|
23
|
+
readonly fsPath: string
|
|
24
|
+
toString(skipEncoding?: boolean): string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export enum ViewColumn {
|
|
28
|
+
Active = -1,
|
|
29
|
+
Beside = -2,
|
|
30
|
+
One = 1,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export enum StatusBarAlignment {
|
|
34
|
+
Left = 1,
|
|
35
|
+
Right = 2,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ExtensionContext {
|
|
39
|
+
readonly subscriptions: Disposable[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WorkspaceFolder {
|
|
43
|
+
readonly uri: Uri
|
|
44
|
+
readonly name: string
|
|
45
|
+
readonly index: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface WorkspaceConfiguration {
|
|
49
|
+
get: <T>(section: string, defaultValue: T) => T
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ConfigurationChangeEvent {
|
|
53
|
+
affectsConfiguration: (section: string) => boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FileSystemWatcher extends Disposable {
|
|
57
|
+
readonly onDidCreate: Event<Uri>
|
|
58
|
+
readonly onDidChange: Event<Uri>
|
|
59
|
+
readonly onDidDelete: Event<Uri>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TerminalOptions {
|
|
63
|
+
name?: string
|
|
64
|
+
cwd?: string | Uri
|
|
65
|
+
env?: Record<string, string | null | undefined>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TerminalExitStatus {
|
|
69
|
+
readonly code: number | undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface TerminalShellExecution {
|
|
73
|
+
read: () => AsyncIterable<string>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface TerminalShellIntegration {
|
|
77
|
+
executeCommand: (commandLine: string) => TerminalShellExecution
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface Terminal {
|
|
81
|
+
readonly name: string
|
|
82
|
+
readonly exitStatus: TerminalExitStatus | undefined
|
|
83
|
+
readonly shellIntegration: TerminalShellIntegration | undefined
|
|
84
|
+
show: (preserveFocus?: boolean) => void
|
|
85
|
+
sendText: (text: string, shouldExecute?: boolean) => void
|
|
86
|
+
dispose: () => void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface TerminalShellIntegrationChangeEvent {
|
|
90
|
+
readonly terminal: Terminal
|
|
91
|
+
readonly shellIntegration: TerminalShellIntegration
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface TerminalShellExecutionEndEvent {
|
|
95
|
+
readonly terminal: Terminal
|
|
96
|
+
readonly execution: TerminalShellExecution
|
|
97
|
+
readonly exitCode: number | undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface StatusBarItem {
|
|
101
|
+
text: string
|
|
102
|
+
tooltip: string | undefined
|
|
103
|
+
command: string | undefined
|
|
104
|
+
name: string | undefined
|
|
105
|
+
show: () => void
|
|
106
|
+
hide: () => void
|
|
107
|
+
dispose: () => void
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface QuickPickItem {
|
|
111
|
+
label: string
|
|
112
|
+
description?: string
|
|
113
|
+
detail?: string
|
|
114
|
+
alwaysShow?: boolean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface QuickPick<T extends QuickPickItem> extends Disposable {
|
|
118
|
+
value: string
|
|
119
|
+
title: string | undefined
|
|
120
|
+
placeholder: string | undefined
|
|
121
|
+
items: readonly T[]
|
|
122
|
+
readonly selectedItems: readonly T[]
|
|
123
|
+
busy: boolean
|
|
124
|
+
matchOnDescription: boolean
|
|
125
|
+
readonly onDidChangeValue: Event<string>
|
|
126
|
+
readonly onDidAccept: Event<void>
|
|
127
|
+
readonly onDidHide: Event<void>
|
|
128
|
+
show: () => void
|
|
129
|
+
hide: () => void
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface InputBoxOptions {
|
|
133
|
+
title?: string
|
|
134
|
+
prompt?: string
|
|
135
|
+
placeHolder?: string
|
|
136
|
+
value?: string
|
|
137
|
+
ignoreFocusOut?: boolean
|
|
138
|
+
validateInput?: (value: string) => string | undefined | null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface TextDocument {
|
|
142
|
+
readonly uri: Uri
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface TextEditor {
|
|
146
|
+
readonly document: TextDocument
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export namespace window {
|
|
150
|
+
export const activeTextEditor: TextEditor | undefined
|
|
151
|
+
export const terminals: readonly Terminal[]
|
|
152
|
+
export const onDidCloseTerminal: Event<Terminal>
|
|
153
|
+
export const onDidChangeTerminalShellIntegration: Event<TerminalShellIntegrationChangeEvent>
|
|
154
|
+
export const onDidEndTerminalShellExecution: Event<TerminalShellExecutionEndEvent>
|
|
155
|
+
export function createTerminal(options: TerminalOptions): Terminal
|
|
156
|
+
export function createStatusBarItem(id: string, alignment?: StatusBarAlignment, priority?: number): StatusBarItem
|
|
157
|
+
export function createQuickPick<T extends QuickPickItem>(): QuickPick<T>
|
|
158
|
+
export function showInputBox(options?: InputBoxOptions): PromiseLike<string | undefined>
|
|
159
|
+
export function showInformationMessage<T extends string>(message: string, ...items: T[]): PromiseLike<T | undefined>
|
|
160
|
+
export function showWarningMessage<T extends string>(message: string, ...items: T[]): PromiseLike<T | undefined>
|
|
161
|
+
export function showErrorMessage<T extends string>(message: string, ...items: T[]): PromiseLike<T | undefined>
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export namespace workspace {
|
|
165
|
+
export const workspaceFolders: readonly WorkspaceFolder[] | undefined
|
|
166
|
+
export const onDidChangeConfiguration: Event<ConfigurationChangeEvent>
|
|
167
|
+
export function getConfiguration(section?: string): WorkspaceConfiguration
|
|
168
|
+
export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined
|
|
169
|
+
export function createFileSystemWatcher(globPattern: string): FileSystemWatcher
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export namespace commands {
|
|
173
|
+
export function registerCommand(command: string, callback: (...args: any[]) => any): Disposable
|
|
174
|
+
export function executeCommand<T = unknown>(command: string, ...rest: any[]): PromiseLike<T>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export namespace env {
|
|
178
|
+
export function asExternalUri(target: Uri): PromiseLike<Uri>
|
|
179
|
+
export function openExternal(target: Uri): PromiseLike<boolean>
|
|
180
|
+
}
|
|
181
|
+
}
|