spl-lang 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 +147 -3
- package/dist/ast.d.ts +177 -0
- package/dist/ast.js +1 -0
- package/dist/auth.d.ts +19 -0
- package/dist/auth.js +110 -0
- package/dist/builtins.d.ts +8 -0
- package/dist/builtins.js +350 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +162 -0
- package/dist/copilot.d.ts +29 -0
- package/dist/copilot.js +192 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +52 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/interpreter.d.ts +25 -0
- package/dist/interpreter.js +546 -0
- package/dist/lexer.d.ts +43 -0
- package/dist/lexer.js +208 -0
- package/dist/llm.d.ts +46 -0
- package/dist/llm.js +54 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +27 -0
- package/dist/parser.d.ts +13 -0
- package/dist/parser.js +709 -0
- package/dist/restrict.d.ts +27 -0
- package/dist/restrict.js +158 -0
- package/dist/store.d.ts +20 -0
- package/dist/store.js +52 -0
- package/dist/tools.d.ts +38 -0
- package/dist/tools.js +266 -0
- package/dist/values.d.ts +49 -0
- package/dist/values.js +131 -0
- package/docs/syntax.ebnf +97 -0
- package/package.json +39 -6
package/README.md
CHANGED
|
@@ -1,3 +1,147 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# spl-lang: Semantic Prompt Language interpreter
|
|
2
|
+
|
|
3
|
+
A Python-like language where LLM calls are part of the syntax. Everything outside LLM calls is deterministic; LLM
|
|
4
|
+
calls go through the GitHub Copilot SDK. See [../LANG-PRD.md](../LANG-PRD.md) for the full spec,
|
|
5
|
+
[docs/syntax.ebnf](docs/syntax.ebnf) for the grammar and the [documentation site](../docs/index.html) (open it in a
|
|
6
|
+
browser, or publish `docs/` with GitHub Pages) for guides and reference.
|
|
7
|
+
|
|
8
|
+
## Run
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
npm install && npm run build
|
|
12
|
+
node dist/cli.js login # once: authenticate with GitHub Copilot
|
|
13
|
+
node dist/cli.js examples/live.spl
|
|
14
|
+
npx file:./lang examples/live.spl # npx from a checkout (or `npx spl-lang` once published)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node 20+ and an authenticated GitHub Copilot.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
spl <script.spl> [--log-level error|info|debug] [--silent] [--model <id>]
|
|
21
|
+
[--store <file.json> | --store-module <module>]
|
|
22
|
+
[--timeout <s>] [--cwd <dir>] [--check]
|
|
23
|
+
spl login # GitHub device flow: prints a code, you confirm it in the browser
|
|
24
|
+
spl logout # removes the credentials saved by `spl login`
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
* stdout: the value of `success` or a top-level `return` (strings raw, everything else as JSON) or the failure payload.
|
|
28
|
+
* stderr: logs, including model and reasoning effort of every LLM call.
|
|
29
|
+
* Exit code: `0` success, `1` uncaught failure, `2` syntax/usage error.
|
|
30
|
+
|
|
31
|
+
`spl login` does nothing if the runtime is already authenticated (an earlier login, a Copilot CLI / `gh` login or
|
|
32
|
+
`COPILOT_GITHUB_TOKEN`/`GH_TOKEN`/`GITHUB_TOKEN`). Otherwise it runs the device flow and saves the token to
|
|
33
|
+
`~/.spl/credentials.json` (override the folder with `SPL_HOME`); scripts use that token automatically. A script that
|
|
34
|
+
needs an LLM and finds no credentials fails with `llm_error` and tells you to run `spl login`.
|
|
35
|
+
|
|
36
|
+
## Language at a glance
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
@{model='gpt-5.5'}
|
|
40
|
+
|
|
41
|
+
def fib(n) {
|
|
42
|
+
if n < 2 { return n }
|
|
43
|
+
return fib(n - 1) + fib(n - 2)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let joke = ?{len='sentence', fmt='object'} 'A joke about programmers' # JSON validated and parsed
|
|
47
|
+
let why = think{level='high', show=True} 'Why is {joke.setup} funny?'
|
|
48
|
+
|
|
49
|
+
let color = None
|
|
50
|
+
enforce 'Answer in English' {
|
|
51
|
+
forbid 'profanity' { let color = ?{len='word'} 'Your favorite color?' } # results leave the block
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
{ # a bare block is a nested scope
|
|
55
|
+
let color = 'Red' # shadows the outer variable
|
|
56
|
+
log(color)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
restrict './out' { # also: restrict net 'host,..' / restrict exec 'git,npm'
|
|
60
|
+
let f = write_file{path='./out/a.txt', content='{fib(10)}'} 'Confirm'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try { assert fib(5) == 4 'wrong' } except err { log(err.reason) }
|
|
64
|
+
success {'joke': joke, 'why': why, 'color': color}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Decisions worth knowing (the PRD leaves them open):
|
|
68
|
+
|
|
69
|
+
* **Blocks use braces**, statements end at a newline.
|
|
70
|
+
* **Every modification of a variable starts with `let`**: `let x = 1`, `let x += 1`, `let d['k'] = 2`, `let d.k = 3`.
|
|
71
|
+
A bare `x = 1` is a syntax error.
|
|
72
|
+
* **Scoping.** `let NAME = ...` binds in the current scope. Bare `{ ... }` blocks and function bodies open a new scope,
|
|
73
|
+
so `let` there shadows an outer variable and the outer one is untouched. `if`/`elif`/`else`, `for`, `while`,
|
|
74
|
+
`try`/`except`, `enforce`, `forbid` and `restrict` bodies share the enclosing scope, so `let` inside them updates the
|
|
75
|
+
enclosing variable: this is how the answer of an LLM call leaves an `enforce`/`forbid`/`restrict` block. A function
|
|
76
|
+
cannot rebind its caller's variables (use a list or dict to share state); names are read through all enclosing scopes.
|
|
77
|
+
* **`return` exits immediately** from any scope: the innermost function, or the whole script when used at the top level
|
|
78
|
+
(like `success`). `break`/`continue` still belong to loops.
|
|
79
|
+
* **`?` is a prefix operator**: `?'a' + b` is `(?'a') + b`, `-?'n'` is `-(?'n')`.
|
|
80
|
+
* **LLM commands have defaults and braces are optional**: `?'q'`, `think 'q'` (level `medium`), and tools without
|
|
81
|
+
parameters, e.g. `read_file 'Read notes.txt and summarize it'`.
|
|
82
|
+
* **Every string interpolates `{expr}`**. Write `{{`/`}}` or `\{`/`\}` for literal braces, or use a raw string
|
|
83
|
+
`r'{"a": 1}'`. Values are substituted as is: strings raw, `True`/`False`/`None`, lists and dicts as JSON.
|
|
84
|
+
* Tool calls (`read_file`, `write_file`, `web_fetch`, `web_search`, `shell`): when the script supplies every parameter
|
|
85
|
+
the tool needs, the runtime runs it first (restriction-checked), and the output plus your instructions go to the LLM
|
|
86
|
+
whose answer is the value. When a parameter is missing (`read_file 'summarize notes.txt'`,
|
|
87
|
+
`write_file{path='a.txt'} 'write a haiku'`, `web_search 'latest Node.js LTS'`) the LLM calls the tool itself with the
|
|
88
|
+
parameters you gave kept fixed. `web_search` queries DuckDuckGo and counts as network access for `restrict net`.
|
|
89
|
+
* Builtins: `len str int float bool type list range abs round min max sum sorted reversed enumerate zip any all keys
|
|
90
|
+
values items json_parse json_stringify log`, plus Python-style string/list/dict methods. `log(...)` writes to stderr.
|
|
91
|
+
* Unknown modifiers/values, `break` outside loops, `try` without `except`, a modification without `let`, etc. are
|
|
92
|
+
syntax errors (exit code 2).
|
|
93
|
+
|
|
94
|
+
## Restrictions
|
|
95
|
+
|
|
96
|
+
Two layers: the LLM is told about active restrictions in the prompt, and the runtime enforces them. The LLM only gets
|
|
97
|
+
the five runtime tools (`read_file`, `write_file`, `web_fetch`, `web_search`, `shell`); every call is checked against
|
|
98
|
+
the active `restrict` scopes and any other Copilot permission request is rejected. A violation raises a
|
|
99
|
+
`restriction_violation` failure (catchable with `try/except`).
|
|
100
|
+
|
|
101
|
+
* Paths are canonicalized (symlinks resolved, `..` removed) relative to `--cwd` before the check.
|
|
102
|
+
* `net` matches exact hosts (and `*.example.com` wildcards); redirects are re-checked on every hop. `web_search`
|
|
103
|
+
needs `html.duckduckgo.com` to be allowed.
|
|
104
|
+
* `exec` matches the executable name. While any restriction is active, `shell` accepts only simple commands (no
|
|
105
|
+
`&|;<>` redirects, substitution or chaining) so the check is exact; path-like and URL arguments are also checked
|
|
106
|
+
against `fs`/`net`. This is best effort for programs that touch files or the network on their own.
|
|
107
|
+
* Nested restrictions of one kind intersect and can never widen.
|
|
108
|
+
|
|
109
|
+
## Reproducibility
|
|
110
|
+
|
|
111
|
+
Control flow, arithmetic and interpolation are deterministic. The Copilot SDK has no `temperature` or `seed` setting,
|
|
112
|
+
so the language has none either. `think` levels map to the SDK reasoning effort; if the selected model does not support
|
|
113
|
+
it a warning is logged and the model default is used. Guard against variance with `assert` and `fmt='object'`.
|
|
114
|
+
|
|
115
|
+
## Variable store
|
|
116
|
+
|
|
117
|
+
`--store vars.json` loads top-level variables from the file at start and writes them back at exit (also on failure).
|
|
118
|
+
`--store-module ./mystore.mjs` plugs in your own store: export `createStore()` returning `{ load(): Record, save(vars) }`.
|
|
119
|
+
|
|
120
|
+
## Examples
|
|
121
|
+
|
|
122
|
+
All in [examples/](examples); run from the project root, `--cwd <dir>` points a script at another project. The example
|
|
123
|
+
syntax is checked by `tests/examples.test.ts`; `triage-issues` is also run on Copilot there.
|
|
124
|
+
|
|
125
|
+
| Script | Shows |
|
|
126
|
+
|--------|-------|
|
|
127
|
+
| `commit-message.spl` | `shell` under `restrict exec 'git'`, validation helper, retry loop |
|
|
128
|
+
| `triage-issues.spl` | `fmt='object'` + `assert` validation with feedback retries, aggregation, summary |
|
|
129
|
+
| `extract-invoice.spl` | structured extraction cross-checked with arithmetic before it is accepted |
|
|
130
|
+
| `code-review.spl` | `think` then structure, `restrict` on read and on write, `fail` as a CI gate |
|
|
131
|
+
| `release-notes.spl` | nested `forbid`/`enforce`, git history to a file inside `./out` |
|
|
132
|
+
| `web-digest.spl` | `restrict net`, `web_fetch`, catching a blocked second host |
|
|
133
|
+
| `fix-failing-tests.spl` | agent loop: run `npm test`, let the LLM fix the source, repeat |
|
|
134
|
+
| `translate-docs.spl` | `write_file` without `content` (LLM writes, path pinned), dict iteration |
|
|
135
|
+
| `journal.spl` | state across runs with `--store` |
|
|
136
|
+
| `basics.spl`, `live.spl`, `restrict-live.spl`, `tools-live.spl` | small feature tours |
|
|
137
|
+
|
|
138
|
+
## Develop
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
npm test # builds, then runs parser, interpreter, restriction and CLI tests, plus live tests on Copilot
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The live tests (`tests/live.test.ts`, one example in `tests/examples.test.ts`) call the real GitHub Copilot service and
|
|
145
|
+
are skipped automatically when the runtime is not authenticated: run `spl login` first to include them.
|
|
146
|
+
|
|
147
|
+
The VS Code extension (syntax highlighting, snippets) lives in `../vscode`.
|
package/dist/ast.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
export type StrPart = string | Expr;
|
|
2
|
+
export type Expr = {
|
|
3
|
+
t: 'num';
|
|
4
|
+
v: number;
|
|
5
|
+
} | {
|
|
6
|
+
t: 'str';
|
|
7
|
+
parts: StrPart[];
|
|
8
|
+
} | {
|
|
9
|
+
t: 'bool';
|
|
10
|
+
v: boolean;
|
|
11
|
+
} | {
|
|
12
|
+
t: 'none';
|
|
13
|
+
} | {
|
|
14
|
+
t: 'name';
|
|
15
|
+
name: string;
|
|
16
|
+
line: number;
|
|
17
|
+
} | {
|
|
18
|
+
t: 'list';
|
|
19
|
+
items: Expr[];
|
|
20
|
+
} | {
|
|
21
|
+
t: 'dict';
|
|
22
|
+
entries: [Expr, Expr][];
|
|
23
|
+
} | {
|
|
24
|
+
t: 'unary';
|
|
25
|
+
op: 'not' | '-' | '+';
|
|
26
|
+
e: Expr;
|
|
27
|
+
} | {
|
|
28
|
+
t: 'binary';
|
|
29
|
+
op: string;
|
|
30
|
+
l: Expr;
|
|
31
|
+
r: Expr;
|
|
32
|
+
} | {
|
|
33
|
+
t: 'logic';
|
|
34
|
+
op: 'and' | 'or';
|
|
35
|
+
l: Expr;
|
|
36
|
+
r: Expr;
|
|
37
|
+
} | {
|
|
38
|
+
t: 'compare';
|
|
39
|
+
ops: string[];
|
|
40
|
+
operands: Expr[];
|
|
41
|
+
} | {
|
|
42
|
+
t: 'ternary';
|
|
43
|
+
cond: Expr;
|
|
44
|
+
then: Expr;
|
|
45
|
+
else: Expr;
|
|
46
|
+
} | {
|
|
47
|
+
t: 'call';
|
|
48
|
+
callee: Expr;
|
|
49
|
+
args: Expr[];
|
|
50
|
+
} | {
|
|
51
|
+
t: 'member';
|
|
52
|
+
obj: Expr;
|
|
53
|
+
name: string;
|
|
54
|
+
} | {
|
|
55
|
+
t: 'index';
|
|
56
|
+
obj: Expr;
|
|
57
|
+
index: Expr;
|
|
58
|
+
} | {
|
|
59
|
+
t: 'slice';
|
|
60
|
+
obj: Expr;
|
|
61
|
+
lo?: Expr;
|
|
62
|
+
hi?: Expr;
|
|
63
|
+
} | {
|
|
64
|
+
t: 'ask';
|
|
65
|
+
mods: AskMods;
|
|
66
|
+
prompt: Expr;
|
|
67
|
+
} | {
|
|
68
|
+
t: 'think';
|
|
69
|
+
mods: ThinkMods;
|
|
70
|
+
prompt: Expr;
|
|
71
|
+
} | {
|
|
72
|
+
t: 'tool';
|
|
73
|
+
tool: ToolName;
|
|
74
|
+
params: Record<string, Expr>;
|
|
75
|
+
instructions: Expr;
|
|
76
|
+
};
|
|
77
|
+
export interface AskMods {
|
|
78
|
+
len?: 'word' | 'sentence' | 'paragraph' | 'long';
|
|
79
|
+
fmt?: 'text' | 'object';
|
|
80
|
+
}
|
|
81
|
+
export interface ThinkMods {
|
|
82
|
+
level?: 'low' | 'medium' | 'high';
|
|
83
|
+
show?: boolean;
|
|
84
|
+
}
|
|
85
|
+
export type ToolName = 'read_file' | 'write_file' | 'web_fetch' | 'web_search' | 'shell';
|
|
86
|
+
export type RestrictKind = 'fs' | 'net' | 'exec';
|
|
87
|
+
export type AssignTarget = {
|
|
88
|
+
t: 'name';
|
|
89
|
+
name: string;
|
|
90
|
+
} | {
|
|
91
|
+
t: 'member';
|
|
92
|
+
obj: Expr;
|
|
93
|
+
name: string;
|
|
94
|
+
} | {
|
|
95
|
+
t: 'index';
|
|
96
|
+
obj: Expr;
|
|
97
|
+
index: Expr;
|
|
98
|
+
};
|
|
99
|
+
interface Base {
|
|
100
|
+
line: number;
|
|
101
|
+
}
|
|
102
|
+
export type Stmt = Base & ({
|
|
103
|
+
t: 'let';
|
|
104
|
+
target: AssignTarget;
|
|
105
|
+
op: '=' | '+=' | '-=' | '*=' | '/=' | '//=' | '%=';
|
|
106
|
+
value: Expr;
|
|
107
|
+
} | {
|
|
108
|
+
t: 'block';
|
|
109
|
+
body: Stmt[];
|
|
110
|
+
} | {
|
|
111
|
+
t: 'expr';
|
|
112
|
+
e: Expr;
|
|
113
|
+
} | {
|
|
114
|
+
t: 'if';
|
|
115
|
+
branches: {
|
|
116
|
+
cond: Expr;
|
|
117
|
+
body: Stmt[];
|
|
118
|
+
}[];
|
|
119
|
+
orelse?: Stmt[];
|
|
120
|
+
} | {
|
|
121
|
+
t: 'for';
|
|
122
|
+
vars: string[];
|
|
123
|
+
iter: Expr;
|
|
124
|
+
body: Stmt[];
|
|
125
|
+
} | {
|
|
126
|
+
t: 'while';
|
|
127
|
+
cond: Expr;
|
|
128
|
+
body: Stmt[];
|
|
129
|
+
} | {
|
|
130
|
+
t: 'break';
|
|
131
|
+
} | {
|
|
132
|
+
t: 'continue';
|
|
133
|
+
} | {
|
|
134
|
+
t: 'pass';
|
|
135
|
+
} | {
|
|
136
|
+
t: 'def';
|
|
137
|
+
name: string;
|
|
138
|
+
params: string[];
|
|
139
|
+
body: Stmt[];
|
|
140
|
+
} | {
|
|
141
|
+
t: 'return';
|
|
142
|
+
value?: Expr;
|
|
143
|
+
} | {
|
|
144
|
+
t: 'try';
|
|
145
|
+
body: Stmt[];
|
|
146
|
+
errName: string;
|
|
147
|
+
handler: Stmt[];
|
|
148
|
+
} | {
|
|
149
|
+
t: 'assert';
|
|
150
|
+
cond: Expr;
|
|
151
|
+
message?: Expr;
|
|
152
|
+
} | {
|
|
153
|
+
t: 'fail';
|
|
154
|
+
reason: Expr;
|
|
155
|
+
} | {
|
|
156
|
+
t: 'success';
|
|
157
|
+
value?: Expr;
|
|
158
|
+
} | {
|
|
159
|
+
t: 'constraint';
|
|
160
|
+
kind: 'enforce' | 'forbid';
|
|
161
|
+
text: Expr;
|
|
162
|
+
body: Stmt[];
|
|
163
|
+
} | {
|
|
164
|
+
t: 'restrict';
|
|
165
|
+
kind: RestrictKind;
|
|
166
|
+
arg: Expr;
|
|
167
|
+
body: Stmt[];
|
|
168
|
+
});
|
|
169
|
+
export interface Directives {
|
|
170
|
+
model?: string;
|
|
171
|
+
}
|
|
172
|
+
export interface Program {
|
|
173
|
+
directives: Directives;
|
|
174
|
+
body: Stmt[];
|
|
175
|
+
lines: string[];
|
|
176
|
+
}
|
|
177
|
+
export {};
|
package/dist/ast.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class AuthError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export declare function credentialsPath(): string;
|
|
4
|
+
/** The token saved by `spl login`, if any. Environment tokens (COPILOT_GITHUB_TOKEN, GH_TOKEN, ...) are handled by the SDK. */
|
|
5
|
+
export declare function loadToken(): string | undefined;
|
|
6
|
+
/** Removes the saved token; returns whether there was one. */
|
|
7
|
+
export declare function logout(): boolean;
|
|
8
|
+
export interface AuthStatus {
|
|
9
|
+
isAuthenticated: boolean;
|
|
10
|
+
login?: string;
|
|
11
|
+
statusMessage?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Asks the Copilot runtime who it is authenticated as, optionally with an explicit token. */
|
|
14
|
+
export declare function authStatus(token?: string): Promise<AuthStatus>;
|
|
15
|
+
/**
|
|
16
|
+
* Authenticates against GitHub Copilot. Does nothing when the runtime is already authenticated
|
|
17
|
+
* (saved token, environment token or a Copilot CLI / gh login); otherwise runs the device flow and saves the token.
|
|
18
|
+
*/
|
|
19
|
+
export declare function login(say: (line: string) => void): Promise<AuthStatus>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { CopilotClient } from '@github/copilot-sdk';
|
|
5
|
+
/** OAuth app of the GitHub Copilot CLI; its device flow yields tokens the Copilot runtime accepts. */
|
|
6
|
+
const CLIENT_ID = 'Ov23ctDVkRmgkPke0Mmm';
|
|
7
|
+
const SCOPE = 'read:user';
|
|
8
|
+
const DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
9
|
+
const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
10
|
+
export class AuthError extends Error {
|
|
11
|
+
}
|
|
12
|
+
export function credentialsPath() {
|
|
13
|
+
return path.join(process.env.SPL_HOME ?? path.join(os.homedir(), '.spl'), 'credentials.json');
|
|
14
|
+
}
|
|
15
|
+
/** The token saved by `spl login`, if any. Environment tokens (COPILOT_GITHUB_TOKEN, GH_TOKEN, ...) are handled by the SDK. */
|
|
16
|
+
export function loadToken() {
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(fs.readFileSync(credentialsPath(), 'utf8'));
|
|
19
|
+
return typeof data.token === 'string' && data.token ? data.token : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function saveToken(token) {
|
|
26
|
+
const file = credentialsPath();
|
|
27
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
28
|
+
fs.writeFileSync(file, JSON.stringify({ token }), { mode: 0o600 });
|
|
29
|
+
}
|
|
30
|
+
/** Removes the saved token; returns whether there was one. */
|
|
31
|
+
export function logout() {
|
|
32
|
+
try {
|
|
33
|
+
fs.rmSync(credentialsPath());
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
if (e.code === 'ENOENT')
|
|
38
|
+
return false;
|
|
39
|
+
throw new AuthError(`cannot remove '${credentialsPath()}': ${e.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Asks the Copilot runtime who it is authenticated as, optionally with an explicit token. */
|
|
43
|
+
export async function authStatus(token) {
|
|
44
|
+
const client = new CopilotClient(token ? { gitHubToken: token } : {});
|
|
45
|
+
try {
|
|
46
|
+
await client.start();
|
|
47
|
+
return await client.getAuthStatus();
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
throw new AuthError(`cannot reach the Copilot runtime: ${e.message}`);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await client.stop().catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function postForm(url, body) {
|
|
57
|
+
const res = await fetch(url, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { accept: 'application/json', 'content-type': 'application/x-www-form-urlencoded' },
|
|
60
|
+
body: new URLSearchParams(body),
|
|
61
|
+
signal: AbortSignal.timeout(30_000),
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok)
|
|
64
|
+
throw new AuthError(`GitHub answered HTTP ${res.status} for ${url}`);
|
|
65
|
+
return (await res.json());
|
|
66
|
+
}
|
|
67
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
68
|
+
/** GitHub device flow: the user enters a code in the browser, the token is polled for. */
|
|
69
|
+
async function deviceFlow(say) {
|
|
70
|
+
const code = await postForm(DEVICE_CODE_URL, { client_id: CLIENT_ID, scope: SCOPE });
|
|
71
|
+
say(`Open ${code.verification_uri} and enter the code: ${code.user_code}`);
|
|
72
|
+
say('Waiting for authorization...');
|
|
73
|
+
let interval = (code.interval ?? 5) * 1000;
|
|
74
|
+
const deadline = Date.now() + code.expires_in * 1000;
|
|
75
|
+
while (Date.now() < deadline) {
|
|
76
|
+
await sleep(interval);
|
|
77
|
+
const res = await postForm(ACCESS_TOKEN_URL, {
|
|
78
|
+
client_id: CLIENT_ID,
|
|
79
|
+
device_code: code.device_code,
|
|
80
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
81
|
+
});
|
|
82
|
+
if (res.access_token)
|
|
83
|
+
return res.access_token;
|
|
84
|
+
switch (res.error) {
|
|
85
|
+
case 'authorization_pending': break;
|
|
86
|
+
case 'slow_down':
|
|
87
|
+
interval += 5000;
|
|
88
|
+
break;
|
|
89
|
+
case 'access_denied': throw new AuthError('authorization was denied');
|
|
90
|
+
case 'expired_token': throw new AuthError('the device code expired; run login again');
|
|
91
|
+
default: throw new AuthError(res.error_description ?? res.error ?? 'unexpected response from GitHub');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw new AuthError('the device code expired; run login again');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Authenticates against GitHub Copilot. Does nothing when the runtime is already authenticated
|
|
98
|
+
* (saved token, environment token or a Copilot CLI / gh login); otherwise runs the device flow and saves the token.
|
|
99
|
+
*/
|
|
100
|
+
export async function login(say) {
|
|
101
|
+
const current = await authStatus(loadToken());
|
|
102
|
+
if (current.isAuthenticated)
|
|
103
|
+
return current;
|
|
104
|
+
const token = await deviceFlow(say);
|
|
105
|
+
const status = await authStatus(token);
|
|
106
|
+
if (!status.isAuthenticated)
|
|
107
|
+
throw new AuthError(status.statusMessage ?? 'the token was not accepted by the Copilot runtime');
|
|
108
|
+
saveToken(token);
|
|
109
|
+
return status;
|
|
110
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type BuiltinFunction, type Value } from './values.js';
|
|
2
|
+
export interface BuiltinContext {
|
|
3
|
+
log: (message: string) => void;
|
|
4
|
+
}
|
|
5
|
+
/** Values that can be iterated by `for`, `list()`, `sorted()` etc. */
|
|
6
|
+
export declare function iterate(v: Value): Value[];
|
|
7
|
+
export declare function makeBuiltins(ctx: BuiltinContext): Map<string, BuiltinFunction>;
|
|
8
|
+
export declare function callMethod(self: Value, name: string, args: Value[]): Value;
|