carveout 0.1.1 → 0.1.2
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/AGENTS.md +21 -3
- package/NOTICE.md +2 -1
- package/README.md +248 -473
- package/dist/src/account.d.ts +37 -0
- package/dist/src/account.js +275 -0
- package/dist/src/api.d.ts +148 -0
- package/dist/src/api.js +120 -0
- package/dist/src/carve.d.ts +788 -0
- package/dist/src/carve.js +1721 -125
- package/dist/src/cli.d.ts +10 -0
- package/dist/src/cli.js +674 -47
- package/dist/src/command.d.ts +10 -0
- package/dist/src/core.d.ts +11 -0
- package/dist/src/cut.d.ts +78 -0
- package/dist/src/cut.js +25 -7
- package/dist/src/extract/compiler-config.d.ts +49 -0
- package/dist/src/extract/fixture.d.ts +103 -0
- package/dist/src/extract/hash.d.ts +6 -0
- package/dist/src/extract/package-registry.d.ts +47 -0
- package/dist/src/extract/package-types.d.ts +52 -0
- package/dist/src/extract/surface.d.ts +44 -0
- package/dist/src/extract/typescript.d.ts +180 -0
- package/dist/src/gate.d.ts +55 -0
- package/dist/src/gate.js +1 -0
- package/dist/src/graph.d.ts +142 -0
- package/dist/src/graph.js +4 -3
- package/dist/src/impact.d.ts +113 -0
- package/dist/src/imports.d.ts +67 -0
- package/dist/src/mergeback.d.ts +182 -0
- package/dist/src/mergeback.js +580 -35
- package/dist/src/platform.d.ts +69 -0
- package/dist/src/platform.js +184 -0
- package/dist/src/policy.d.ts +181 -0
- package/dist/src/policy.js +716 -230
- package/dist/src/project/build.d.ts +23 -0
- package/dist/src/project/catalog.d.ts +25 -0
- package/dist/src/project/project.d.ts +287 -0
- package/dist/src/project/project.js +479 -52
- package/dist/src/project/provenance.d.ts +73 -0
- package/dist/src/project/provenance.js +12 -0
- package/dist/src/project/runner.d.ts +19 -0
- package/dist/src/project/secrets.d.ts +64 -0
- package/dist/src/project/vitest-config.d.ts +70 -0
- package/dist/src/project/vitest-config.js +291 -0
- package/dist/src/pseudonyms/aliases.d.ts +107 -0
- package/dist/src/pseudonyms/aliases.js +394 -0
- package/dist/src/pseudonyms/draft.d.ts +64 -0
- package/dist/src/pseudonyms/draft.js +284 -0
- package/dist/src/pseudonyms/folder.d.ts +172 -0
- package/dist/src/pseudonyms/folder.js +527 -0
- package/dist/src/pseudonyms/index.d.ts +11 -0
- package/dist/src/pseudonyms/index.js +11 -0
- package/dist/src/pseudonyms/vocabulary.d.ts +113 -0
- package/dist/src/pseudonyms/vocabulary.js +522 -0
- package/dist/src/readiness.d.ts +71 -0
- package/dist/src/readiness.js +131 -0
- package/dist/src/receipt.d.ts +154 -0
- package/dist/src/receipt.js +296 -0
- package/dist/src/sandbox.d.ts +53 -0
- package/dist/src/sandbox.js +154 -0
- package/dist/src/slice/analysis.d.ts +75 -0
- package/dist/src/slice/externals.d.ts +51 -0
- package/dist/src/slice/graph.d.ts +69 -0
- package/dist/src/slice/manifest.d.ts +262 -0
- package/dist/src/slice/manifest.js +4 -3
- package/dist/src/slice/reach.d.ts +204 -0
- package/dist/src/slice/reach.js +11 -5
- package/dist/src/slice/report.d.ts +49 -0
- package/dist/src/slice/report.js +5 -2
- package/dist/src/specifiers.d.ts +48 -0
- package/dist/src/status.d.ts +46 -0
- package/dist/src/status.js +3 -2
- package/dist/src/test-integrity.d.ts +54 -0
- package/dist/src/test-integrity.js +812 -0
- package/package.json +24 -5
- package/skills/carveout/SKILL.md +517 -0
- package/skills/carveout-gate/SKILL.md +98 -0
- package/SKILL.md +0 -423
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `carveout login`, `whoami` and `logout` against the hosted platform
|
|
3
|
+
* (`platform.ts`). Each takes its argv and the outside world as `deps`, so
|
|
4
|
+
* test/platform.test.ts drives them against a fake platform with no real
|
|
5
|
+
* waiting; `cli.ts` runs them before it reads any slice option.
|
|
6
|
+
*
|
|
7
|
+
* login is RFC 8628's device flow on the platform's Better Auth:
|
|
8
|
+
* POST /api/auth/device/code {client_id} → device_code, user_code, verification_uri(_complete), expires_in, interval
|
|
9
|
+
* POST /api/auth/device/token {grant_type, device_code, client_id} → access_token (a session that may only mint this client's token)
|
|
10
|
+
* GET /api/v1/me, GET /api/v1/me/orgs (with that session)
|
|
11
|
+
* POST /api/v1/orgs/:orgId/tokens/cli {client, label} → { token, secret }: a 90-day lnx_ token; the session ends
|
|
12
|
+
* The token is saved, never printed.
|
|
13
|
+
*/
|
|
14
|
+
export interface AccountDeps {
|
|
15
|
+
readonly env: NodeJS.ProcessEnv;
|
|
16
|
+
readonly out: (text: string) => void;
|
|
17
|
+
readonly err: (text: string) => void;
|
|
18
|
+
readonly isTTY: boolean;
|
|
19
|
+
readonly ask: (question: string) => Promise<string>;
|
|
20
|
+
readonly sleep: (ms: number) => Promise<unknown>;
|
|
21
|
+
readonly now: () => number;
|
|
22
|
+
readonly fetchImpl: typeof fetch;
|
|
23
|
+
readonly openUrl: (url: string) => void;
|
|
24
|
+
readonly hostname: () => string;
|
|
25
|
+
}
|
|
26
|
+
export interface ClientId {
|
|
27
|
+
readonly client: string;
|
|
28
|
+
readonly version: string;
|
|
29
|
+
}
|
|
30
|
+
export type AccountCommand = "login" | "whoami" | "logout";
|
|
31
|
+
/** The process's own world; tests pass their own. */
|
|
32
|
+
export declare function defaultDeps(): AccountDeps;
|
|
33
|
+
/** Runs one of the three, printing a PlatformError as one line on stderr. Answers the exit code. */
|
|
34
|
+
export declare function runAccount(command: AccountCommand, argv: readonly string[], id: ClientId, deps?: AccountDeps): Promise<number>;
|
|
35
|
+
export declare function login(argv: readonly string[], id: ClientId, deps: AccountDeps): Promise<number>;
|
|
36
|
+
export declare function whoami(argv: readonly string[], { client, version }: ClientId, deps: AccountDeps): Promise<number>;
|
|
37
|
+
export declare function logout(argv: readonly string[], { client }: ClientId, deps: AccountDeps): Promise<number>;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `carveout login`, `whoami` and `logout` against the hosted platform
|
|
3
|
+
* (`platform.ts`). Each takes its argv and the outside world as `deps`, so
|
|
4
|
+
* test/platform.test.ts drives them against a fake platform with no real
|
|
5
|
+
* waiting; `cli.ts` runs them before it reads any slice option.
|
|
6
|
+
*
|
|
7
|
+
* login is RFC 8628's device flow on the platform's Better Auth:
|
|
8
|
+
* POST /api/auth/device/code {client_id} → device_code, user_code, verification_uri(_complete), expires_in, interval
|
|
9
|
+
* POST /api/auth/device/token {grant_type, device_code, client_id} → access_token (a session that may only mint this client's token)
|
|
10
|
+
* GET /api/v1/me, GET /api/v1/me/orgs (with that session)
|
|
11
|
+
* POST /api/v1/orgs/:orgId/tokens/cli {client, label} → { token, secret }: a 90-day lnx_ token; the session ends
|
|
12
|
+
* The token is saved, never printed.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { hostname as osHostname } from "node:os";
|
|
16
|
+
import { createInterface } from "node:readline/promises";
|
|
17
|
+
import { parseArgs } from "node:util";
|
|
18
|
+
import { credentialFor, credentialsPath, platformUrl, PlatformError, request, saveCredential } from "./platform.js";
|
|
19
|
+
const GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
20
|
+
const ADMIN_ROLES = new Set(["owner", "admin"]);
|
|
21
|
+
/** The process's own world; tests pass their own. */
|
|
22
|
+
export function defaultDeps() {
|
|
23
|
+
return {
|
|
24
|
+
env: process.env,
|
|
25
|
+
out: (text) => process.stdout.write(text),
|
|
26
|
+
err: (text) => process.stderr.write(text),
|
|
27
|
+
isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
28
|
+
ask: async (question) => {
|
|
29
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
30
|
+
try {
|
|
31
|
+
return await rl.question(question);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
rl.close();
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
38
|
+
now: () => Date.now(),
|
|
39
|
+
fetchImpl: fetch,
|
|
40
|
+
openUrl,
|
|
41
|
+
hostname: osHostname,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** The OS's browser opener, detached; a failure is only a missed convenience. */
|
|
45
|
+
function openUrl(url) {
|
|
46
|
+
const [command, args] = process.platform === "darwin"
|
|
47
|
+
? ["open", [url]]
|
|
48
|
+
: process.platform === "win32"
|
|
49
|
+
? ["cmd", ["/c", "start", '""', url]]
|
|
50
|
+
: ["xdg-open", [url]];
|
|
51
|
+
try {
|
|
52
|
+
spawn(command, args, { stdio: "ignore", detached: true })
|
|
53
|
+
.on("error", () => { })
|
|
54
|
+
.unref();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// The URL is printed either way.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Whether a held role (Better Auth joins several with commas) may mint a token. */
|
|
61
|
+
const isAdmin = (role) => String(role)
|
|
62
|
+
.split(",")
|
|
63
|
+
.some((one) => ADMIN_ROLES.has(one.trim()));
|
|
64
|
+
const describe = (org) => `${org.slug} (${org.name}, ${org.role}) ${org.id}`;
|
|
65
|
+
function options(argv, spec, usage) {
|
|
66
|
+
try {
|
|
67
|
+
return parseArgs({
|
|
68
|
+
args: [...argv],
|
|
69
|
+
options: { ...spec, help: { type: "boolean", short: "h" } },
|
|
70
|
+
allowPositionals: false,
|
|
71
|
+
}).values;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new PlatformError(`${error.message}\n${usage}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const text = (value) => (typeof value === "string" ? value : undefined);
|
|
78
|
+
/** Runs one of the three, printing a PlatformError as one line on stderr. Answers the exit code. */
|
|
79
|
+
export async function runAccount(command, argv, id, deps = defaultDeps()) {
|
|
80
|
+
try {
|
|
81
|
+
return await { login, whoami, logout }[command](argv, id, deps);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (!(error instanceof PlatformError))
|
|
85
|
+
throw error;
|
|
86
|
+
deps.err(`${id.client} ${command}: ${error.message}\n`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Picks the workspace to mint in: --org, the only one an owner or admin holds, or a choice on a TTY. */
|
|
91
|
+
async function chooseOrg(orgs, wanted, client, deps) {
|
|
92
|
+
const eligible = orgs.filter((org) => isAdmin(org.role));
|
|
93
|
+
const listing = (list) => list.map((org) => ` ${describe(org)}`).join("\n");
|
|
94
|
+
const matches = (org, key) => org.id === key || org.slug.toLowerCase() === key.replace(/^@/, "").toLowerCase();
|
|
95
|
+
if (wanted !== undefined) {
|
|
96
|
+
const found = orgs.find((org) => matches(org, wanted));
|
|
97
|
+
if (found === undefined)
|
|
98
|
+
throw new PlatformError(`No workspace ${wanted} for this account. It has:\n${listing(orgs)}`);
|
|
99
|
+
if (!isAdmin(found.role)) {
|
|
100
|
+
throw new PlatformError(`You are ${found.role} in ${found.slug}; only its owners and admins can give ${client} a token.`);
|
|
101
|
+
}
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
const [only] = eligible;
|
|
105
|
+
if (eligible.length === 1 && only !== undefined)
|
|
106
|
+
return only;
|
|
107
|
+
if (eligible.length === 0) {
|
|
108
|
+
throw new PlatformError(orgs.length === 0
|
|
109
|
+
? "This account has no workspace yet. Create one in the platform's web app, then run login again."
|
|
110
|
+
: `You are not an owner or admin of any workspace, and only they can give ${client} a token. This account has:\n${listing(orgs)}`);
|
|
111
|
+
}
|
|
112
|
+
if (!deps.isTTY) {
|
|
113
|
+
throw new PlatformError(`This account can sign in to ${eligible.length} workspaces; name one with --org <handle|id>:\n${listing(eligible)}`);
|
|
114
|
+
}
|
|
115
|
+
deps.out(`\nWorkspaces you can sign ${client} in to:\n${eligible.map((org, i) => ` ${i + 1}. ${describe(org)}`).join("\n")}\n`);
|
|
116
|
+
for (let tries = 0; tries < 3; tries += 1) {
|
|
117
|
+
const answer = (await deps.ask(`Workspace [1-${eligible.length}]: `)).trim();
|
|
118
|
+
const index = Number(answer);
|
|
119
|
+
const picked = Number.isInteger(index) && index >= 1 ? eligible[index - 1] : eligible.find((org) => matches(org, answer));
|
|
120
|
+
if (picked !== undefined)
|
|
121
|
+
return picked;
|
|
122
|
+
}
|
|
123
|
+
throw new PlatformError("No workspace chosen.");
|
|
124
|
+
}
|
|
125
|
+
/** Polls the token endpoint at the server's interval, five seconds slower after each `slow_down`, until approval, denial or expiry. */
|
|
126
|
+
async function pollForSession(base, code, { client, version }, deps) {
|
|
127
|
+
let interval = Math.max(1, Number(code.interval) || 5) * 1000;
|
|
128
|
+
const deadline = deps.now() + Math.max(1, Number(code.expires_in) || 900) * 1000;
|
|
129
|
+
for (;;) {
|
|
130
|
+
await deps.sleep(interval);
|
|
131
|
+
if (deps.now() >= deadline)
|
|
132
|
+
throw new PlatformError("The code expired before it was approved. Run login again.");
|
|
133
|
+
try {
|
|
134
|
+
const granted = await request({
|
|
135
|
+
base,
|
|
136
|
+
method: "POST",
|
|
137
|
+
path: "/api/auth/device/token",
|
|
138
|
+
body: { grant_type: GRANT, device_code: code.device_code, client_id: client },
|
|
139
|
+
client,
|
|
140
|
+
version,
|
|
141
|
+
fetchImpl: deps.fetchImpl,
|
|
142
|
+
});
|
|
143
|
+
if (typeof granted.access_token !== "string")
|
|
144
|
+
throw new PlatformError("The platform approved the sign-in but sent no session.");
|
|
145
|
+
return granted.access_token;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (!(error instanceof PlatformError))
|
|
149
|
+
throw error;
|
|
150
|
+
if (error.code === "authorization_pending")
|
|
151
|
+
continue;
|
|
152
|
+
if (error.code === "slow_down") {
|
|
153
|
+
interval += 5000;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (error.code === "expired_token")
|
|
157
|
+
throw new PlatformError("The code expired before it was approved. Run login again.");
|
|
158
|
+
if (error.code === "access_denied")
|
|
159
|
+
throw new PlatformError("The sign-in was denied in the browser. Nothing was saved.");
|
|
160
|
+
if (error.code === "invalid_grant")
|
|
161
|
+
throw new PlatformError("The code is no longer valid (already used, or unknown). Run login again.");
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export async function login(argv, id, deps) {
|
|
167
|
+
const { client, version } = id;
|
|
168
|
+
const usage = `usage: ${client} login [--platform <url>] [--org <handle|id>] [--open]`;
|
|
169
|
+
const values = options(argv, { platform: { type: "string" }, org: { type: "string" }, open: { type: "boolean" } }, usage);
|
|
170
|
+
if (values["help"] === true) {
|
|
171
|
+
deps.out(`${usage}\n\nSigns ${client} in to the Lunox platform (default ${platformUrl(undefined, {})}; LUNOX_PLATFORM_URL) with a code\n` +
|
|
172
|
+
`you approve in the browser, and saves a workspace token in ${credentialsPath(deps.env)} (LUNOX_CREDENTIALS).\n` +
|
|
173
|
+
`--org picks the workspace; --open opens the approval page.\n`);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
const base = platformUrl(text(values["platform"]), deps.env);
|
|
177
|
+
const call = (more) => request({ base, client, version, fetchImpl: deps.fetchImpl, ...more });
|
|
178
|
+
const code = await call({ method: "POST", path: "/api/auth/device/code", body: { client_id: client } });
|
|
179
|
+
if (typeof code.device_code !== "string" || typeof code.user_code !== "string") {
|
|
180
|
+
throw new PlatformError(`${base} answered the sign-in request without a code.`);
|
|
181
|
+
}
|
|
182
|
+
const page = code.verification_uri_complete ?? code.verification_uri ?? `${base}/device`;
|
|
183
|
+
const minutes = Math.round((Number(code.expires_in) || 900) / 60);
|
|
184
|
+
deps.out(`To sign ${client} in to ${base}, open this page and approve the code:\n\n ${page}\n\n` +
|
|
185
|
+
` Code: ${code.user_code} (expires in ${minutes} minute${minutes === 1 ? "" : "s"})\n\n`);
|
|
186
|
+
if (values["open"] === true)
|
|
187
|
+
deps.openUrl(page);
|
|
188
|
+
deps.out("Waiting for approval...\n");
|
|
189
|
+
const session = await pollForSession(base, code, id, deps);
|
|
190
|
+
const asSession = (more) => call({ token: session, session: true, ...more });
|
|
191
|
+
let minted;
|
|
192
|
+
let me;
|
|
193
|
+
let org;
|
|
194
|
+
try {
|
|
195
|
+
me = (await asSession({ path: "/api/v1/me" })).user;
|
|
196
|
+
const orgs = (await asSession({ path: "/api/v1/me/orgs" })).organizations ?? [];
|
|
197
|
+
org = await chooseOrg(orgs, text(values["org"]), client, deps);
|
|
198
|
+
const label = String(deps.hostname() ?? "")
|
|
199
|
+
.trim()
|
|
200
|
+
.slice(0, 60);
|
|
201
|
+
minted = await asSession({
|
|
202
|
+
method: "POST",
|
|
203
|
+
path: `/api/v1/orgs/${encodeURIComponent(org.id)}/tokens/cli`,
|
|
204
|
+
body: label === "" ? { client } : { client, label },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
// Minting ends the session; a sign-in that stopped short ends it here, best effort.
|
|
209
|
+
await asSession({ method: "POST", path: "/api/auth/sign-out", body: {} }).catch(() => { });
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
if (typeof minted.secret !== "string" || !minted.secret.startsWith("lnx_")) {
|
|
213
|
+
throw new PlatformError(`${base} answered without a workspace token.`);
|
|
214
|
+
}
|
|
215
|
+
const path = credentialsPath(deps.env);
|
|
216
|
+
const expiresAt = minted.token?.expiresAt ?? null;
|
|
217
|
+
saveCredential(path, base, client, {
|
|
218
|
+
token: minted.secret,
|
|
219
|
+
organizationId: org.id,
|
|
220
|
+
organizationHandle: org.slug,
|
|
221
|
+
client,
|
|
222
|
+
createdAt: minted.token?.createdAt ?? new Date(deps.now()).toISOString(),
|
|
223
|
+
expiresAt,
|
|
224
|
+
});
|
|
225
|
+
const who = me ? `${me.name} <${me.email}>` : "you";
|
|
226
|
+
const expiry = expiresAt ? `; it expires ${expiresAt.slice(0, 10)}` : "";
|
|
227
|
+
deps.out(`Signed in as ${who}, workspace ${org.name} (@${org.slug}) on ${base}.\nToken saved to ${path}${expiry}.\n`);
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
export async function whoami(argv, { client, version }, deps) {
|
|
231
|
+
const usage = `usage: ${client} whoami [--platform <url>]`;
|
|
232
|
+
const values = options(argv, { platform: { type: "string" } }, usage);
|
|
233
|
+
if (values["help"] === true) {
|
|
234
|
+
deps.out(`${usage}\n\nThe account, workspace and token expiry ${client} is signed in with.\n`);
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
const base = platformUrl(text(values["platform"]), deps.env);
|
|
238
|
+
const saved = credentialFor(credentialsPath(deps.env), base, client);
|
|
239
|
+
if (saved === undefined)
|
|
240
|
+
throw new PlatformError(`Not signed in to ${base}. Run \`${client} login\`.`);
|
|
241
|
+
const call = (path) => request({ base, token: saved.token, path, client, version, fetchImpl: deps.fetchImpl });
|
|
242
|
+
const { user } = await call("/api/v1/me");
|
|
243
|
+
const { organization, role } = await call(`/api/v1/orgs/${encodeURIComponent(saved.organizationId)}`);
|
|
244
|
+
const expires = saved.expiresAt ? `expires ${saved.expiresAt.slice(0, 10)}` : "does not expire";
|
|
245
|
+
deps.out([
|
|
246
|
+
`user ${user.name} <${user.email}>${user.username ? ` (@${user.username})` : ""}`,
|
|
247
|
+
`workspace ${organization.name} (@${organization.slug}), ${role}`,
|
|
248
|
+
`platform ${base}`,
|
|
249
|
+
`token ${client}, ${expires}`,
|
|
250
|
+
"",
|
|
251
|
+
].join("\n"));
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
export async function logout(argv, { client }, deps) {
|
|
255
|
+
const usage = `usage: ${client} logout [--platform <url>]`;
|
|
256
|
+
const values = options(argv, { platform: { type: "string" } }, usage);
|
|
257
|
+
if (values["help"] === true) {
|
|
258
|
+
deps.out(`${usage}\n\nDeletes ${client}'s saved token for the platform.\n`);
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
const base = platformUrl(text(values["platform"]), deps.env);
|
|
262
|
+
const path = credentialsPath(deps.env);
|
|
263
|
+
const saved = credentialFor(path, base, client);
|
|
264
|
+
if (saved === undefined) {
|
|
265
|
+
deps.out(`Not signed in to ${base}; nothing to do.\n`);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
saveCredential(path, base, client, undefined);
|
|
269
|
+
// The platform lets no API token revoke itself (token routes take a person's session), so this is local.
|
|
270
|
+
const until = saved.expiresAt ? `until ${saved.expiresAt.slice(0, 10)}` : "until it is revoked";
|
|
271
|
+
deps.out(`Signed out of ${base}: the token is deleted from ${path}.\n` +
|
|
272
|
+
`The platform has no route for a token to revoke itself, so it stays valid ${until} unless an owner or\n` +
|
|
273
|
+
`admin of @${saved.organizationHandle} revokes it in the platform's web app (the workspace's API tokens).\n`);
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library entry, `carveout/api`: plan, cut and patch for a platform
|
|
3
|
+
* that keeps the receipt itself, on the code paths the CLI runs
|
|
4
|
+
* (`planSlice`, `cutSlice`, `patchProject` in `carve.ts`).
|
|
5
|
+
*
|
|
6
|
+
* What it holds to: it never starts Docker or any container, never runs
|
|
7
|
+
* the folder's code (no install, build or test of it; `check` and `merge`
|
|
8
|
+
* are not here), and never reads or writes `CARVEOUT_HOME` (`~/.carveout`):
|
|
9
|
+
* the receipt goes out of `cut` and into `patch` as data. What it may run
|
|
10
|
+
* is Git, read-only, on the repository (its commit and files) and, for a
|
|
11
|
+
* patch with edits, in a scratch repository of its own; and, only for a
|
|
12
|
+
* `cut` that is not `offline`, npm with scripts off to fetch package types
|
|
13
|
+
* (into `typesCache`, else a temporary folder) and to resolve the folder's
|
|
14
|
+
* lockfile. `test/api.test.ts` holds it to that with `child_process` made
|
|
15
|
+
* to throw.
|
|
16
|
+
*
|
|
17
|
+
* Also exported: the policy's text checks (`integrityOf`,
|
|
18
|
+
* `gainedIntegrity`, `weakenedTest`) and the slice and project modules
|
|
19
|
+
* `core.ts` gathers, forked from sandbox-factory's core.
|
|
20
|
+
*/
|
|
21
|
+
import { type CarveoutRecord, type Patch, type PlanSurface } from "./carve.js";
|
|
22
|
+
import { type CutEdge, type SliceExposure, type SliceExternals, type SliceOrigin, type SliceTestLeft, type TestRunner } from "./core.js";
|
|
23
|
+
import type { Readiness } from "./readiness.js";
|
|
24
|
+
import type { Receipt } from "./receipt.js";
|
|
25
|
+
export * from "./core.js";
|
|
26
|
+
export { gainedIntegrity, integrityOf, weakenedTest, type Refusal } from "./policy.js";
|
|
27
|
+
export type { CarveoutRecord, Patch, PlanSurface, Readiness, Receipt };
|
|
28
|
+
/** The walk's bounds, as `carveout plan` and `cut` take them. */
|
|
29
|
+
export interface WalkOptions {
|
|
30
|
+
/** The repository's checkout root. */
|
|
31
|
+
readonly root: string;
|
|
32
|
+
/** Files or folders, relative to the root. */
|
|
33
|
+
readonly entries: readonly string[];
|
|
34
|
+
/** Import depth from the entries (`--depth`, default 3). */
|
|
35
|
+
readonly depth?: number;
|
|
36
|
+
/** Most files the folder holds (`--max-files`, default 40). */
|
|
37
|
+
readonly maxFiles?: number;
|
|
38
|
+
/** Bring the tests outside the slice that import it (`--with-tests`). */
|
|
39
|
+
readonly withTests?: boolean;
|
|
40
|
+
/** Copy the pure code the slice runs rather than mock it (`--copy-pure`). */
|
|
41
|
+
readonly copyPure?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** What a cut of the entries would hold; nothing is written. */
|
|
44
|
+
export interface PlanResult {
|
|
45
|
+
readonly root: string;
|
|
46
|
+
readonly entries: readonly string[];
|
|
47
|
+
/** Entries that name no script file of the repository. */
|
|
48
|
+
readonly unknownEntries: readonly string[];
|
|
49
|
+
readonly budget: {
|
|
50
|
+
readonly maxFiles: number;
|
|
51
|
+
readonly maxDepth: number;
|
|
52
|
+
};
|
|
53
|
+
/** The files the folder would copy, sorted, with why each is there and its depth. */
|
|
54
|
+
readonly included: readonly string[];
|
|
55
|
+
readonly originOf: Readonly<Record<string, SliceOrigin>>;
|
|
56
|
+
readonly depthOf: Readonly<Record<string, number>>;
|
|
57
|
+
/** Files the walk reached past the file budget: cut instead. */
|
|
58
|
+
readonly dropped: readonly string[];
|
|
59
|
+
/** Pure code copied rather than mocked (`copyPure`). */
|
|
60
|
+
readonly pure: readonly string[];
|
|
61
|
+
/** Cut modules (each a stub and a mock), those imported for types only, and every import that crosses out. */
|
|
62
|
+
readonly cuts: {
|
|
63
|
+
readonly modules: readonly string[];
|
|
64
|
+
readonly typeOnly: readonly string[];
|
|
65
|
+
readonly outbound: readonly CutEdge[];
|
|
66
|
+
};
|
|
67
|
+
/** The slice's modules code outside it imports: the names taken and the importers. */
|
|
68
|
+
readonly surface: readonly PlanSurface[];
|
|
69
|
+
/** Tests of the slice: carried, each with its runner; left in the repository, each with the reason. */
|
|
70
|
+
readonly tests: {
|
|
71
|
+
readonly verdict: string;
|
|
72
|
+
readonly carried: readonly {
|
|
73
|
+
readonly file: string;
|
|
74
|
+
readonly runner: TestRunner;
|
|
75
|
+
}[];
|
|
76
|
+
readonly helpers: readonly string[];
|
|
77
|
+
readonly left: readonly (SliceTestLeft & {
|
|
78
|
+
readonly why: string;
|
|
79
|
+
})[];
|
|
80
|
+
};
|
|
81
|
+
/** Bare packages the slice imports, and of those the repository's own workspaces. */
|
|
82
|
+
readonly packages: readonly string[];
|
|
83
|
+
readonly workspacePackages: readonly string[];
|
|
84
|
+
/** Services and environment the slice reads. */
|
|
85
|
+
readonly externals: SliceExternals;
|
|
86
|
+
/** What the folder would show of the repository, by origin. */
|
|
87
|
+
readonly exposure: SliceExposure;
|
|
88
|
+
/** What stops the cut, one line each, as `carveout plan` prints them. */
|
|
89
|
+
readonly blockers: readonly string[];
|
|
90
|
+
}
|
|
91
|
+
/** `carveout plan`: the files, cuts, public surface and tests of a cut, from the import graph. Writes nothing. */
|
|
92
|
+
export declare function plan(options: WalkOptions): PlanResult;
|
|
93
|
+
export interface CutOptions extends WalkOptions {
|
|
94
|
+
/** An empty or new folder, outside the repository. */
|
|
95
|
+
readonly out: string;
|
|
96
|
+
/**
|
|
97
|
+
* No network: no package types fetched and no lockfile resolved, so
|
|
98
|
+
* nothing but Git is run. Off: npm, scripts off, does both.
|
|
99
|
+
*/
|
|
100
|
+
readonly offline?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* A folder for someone outside (`--public`): literals widened and the
|
|
103
|
+
* repository's names in pseudonyms under the receipt's table. Off here
|
|
104
|
+
* whatever `carveout.config.json` says, for a caller with its own names.
|
|
105
|
+
*/
|
|
106
|
+
readonly public?: boolean;
|
|
107
|
+
/** Where fetched package types are kept between cuts; a temporary folder, removed after, by default. */
|
|
108
|
+
readonly typesCache?: string;
|
|
109
|
+
/** The cut's progress lines. */
|
|
110
|
+
readonly log?: (line: string) => void;
|
|
111
|
+
}
|
|
112
|
+
export interface CutOutput {
|
|
113
|
+
/** The folder, absolute. */
|
|
114
|
+
readonly out: string;
|
|
115
|
+
/** What the folder's `carveout.json` holds. */
|
|
116
|
+
readonly record: CarveoutRecord;
|
|
117
|
+
/** Whether it is written without blockers. */
|
|
118
|
+
readonly ready: boolean;
|
|
119
|
+
readonly blockers: CarveoutRecord["blockers"];
|
|
120
|
+
/** The maintainer's receipt, as data: keep it private, and give it to `patch`. Written nowhere. */
|
|
121
|
+
readonly receipt: Receipt;
|
|
122
|
+
/** What would make the folder's tests run on real code. */
|
|
123
|
+
readonly readiness: Readiness;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `carveout cut`: writes the folder and its `carveout.json` as the CLI
|
|
127
|
+
* does, and returns the receipt as data instead of writing it under
|
|
128
|
+
* `~/.carveout/receipts/`.
|
|
129
|
+
*/
|
|
130
|
+
export declare function cut(options: CutOptions): Promise<CutOutput>;
|
|
131
|
+
export interface PatchInput {
|
|
132
|
+
/** The receipt `cut` returned for this folder. */
|
|
133
|
+
readonly receipt: Receipt;
|
|
134
|
+
/** The folder as it came back. */
|
|
135
|
+
readonly folder: string;
|
|
136
|
+
/** The repository's checkout; by default the one the receipt names. It must hold the cut's commit. */
|
|
137
|
+
readonly repo?: string;
|
|
138
|
+
/** Paths and globs the hand-off policy lets through (`--allow`). */
|
|
139
|
+
readonly allow?: readonly string[];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* `carveout patch`: the folder's edits as a patch for the repository, its
|
|
143
|
+
* imports and names put back, with what the hand-off policy keeps out in
|
|
144
|
+
* `refused` (the CLI's exit 6) and what reaches past the slice in
|
|
145
|
+
* `boundary` (exit 5). Judged by the receipt given, never by the folder's
|
|
146
|
+
* own record; runs nothing from the folder.
|
|
147
|
+
*/
|
|
148
|
+
export declare function patch(input: PatchInput): Promise<Patch>;
|
package/dist/src/api.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library entry, `carveout/api`: plan, cut and patch for a platform
|
|
3
|
+
* that keeps the receipt itself, on the code paths the CLI runs
|
|
4
|
+
* (`planSlice`, `cutSlice`, `patchProject` in `carve.ts`).
|
|
5
|
+
*
|
|
6
|
+
* What it holds to: it never starts Docker or any container, never runs
|
|
7
|
+
* the folder's code (no install, build or test of it; `check` and `merge`
|
|
8
|
+
* are not here), and never reads or writes `CARVEOUT_HOME` (`~/.carveout`):
|
|
9
|
+
* the receipt goes out of `cut` and into `patch` as data. What it may run
|
|
10
|
+
* is Git, read-only, on the repository (its commit and files) and, for a
|
|
11
|
+
* patch with edits, in a scratch repository of its own; and, only for a
|
|
12
|
+
* `cut` that is not `offline`, npm with scripts off to fetch package types
|
|
13
|
+
* (into `typesCache`, else a temporary folder) and to resolve the folder's
|
|
14
|
+
* lockfile. `test/api.test.ts` holds it to that with `child_process` made
|
|
15
|
+
* to throw.
|
|
16
|
+
*
|
|
17
|
+
* Also exported: the policy's text checks (`integrityOf`,
|
|
18
|
+
* `gainedIntegrity`, `weakenedTest`) and the slice and project modules
|
|
19
|
+
* `core.ts` gathers, forked from sandbox-factory's core.
|
|
20
|
+
*/
|
|
21
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { cutSlice, patchProject, planBlockers, planSlice, } from "./carve.js";
|
|
25
|
+
import { SLICE_BUDGET_DEFAULTS, TEST_LEFT_REASON, testsVerdict, } from "./core.js";
|
|
26
|
+
export * from "./core.js";
|
|
27
|
+
export { gainedIntegrity, integrityOf, weakenedTest } from "./policy.js";
|
|
28
|
+
const budgetOf = (options) => ({
|
|
29
|
+
maxFiles: options.maxFiles ?? SLICE_BUDGET_DEFAULTS.maxFiles,
|
|
30
|
+
maxDepth: options.depth ?? SLICE_BUDGET_DEFAULTS.maxDepth,
|
|
31
|
+
});
|
|
32
|
+
/** The walk's options as `SliceRequest` fields; unset ones are left to the repository's `carveout.config.json`. */
|
|
33
|
+
const requestOf = (options) => ({
|
|
34
|
+
repo: options.root,
|
|
35
|
+
entryPoints: options.entries,
|
|
36
|
+
budget: budgetOf(options),
|
|
37
|
+
...(options.withTests === undefined ? {} : { tests: options.withTests }),
|
|
38
|
+
...(options.copyPure === undefined ? {} : { copyPure: options.copyPure }),
|
|
39
|
+
});
|
|
40
|
+
/** `carveout plan`: the files, cuts, public surface and tests of a cut, from the import graph. Writes nothing. */
|
|
41
|
+
export function plan(options) {
|
|
42
|
+
const planned = planSlice(requestOf(options));
|
|
43
|
+
const { walk } = planned;
|
|
44
|
+
return {
|
|
45
|
+
root: planned.repo,
|
|
46
|
+
entries: planned.entryPoints,
|
|
47
|
+
unknownEntries: walk.unknownEntryPoints,
|
|
48
|
+
budget: planned.budget,
|
|
49
|
+
included: walk.included,
|
|
50
|
+
originOf: walk.originOf,
|
|
51
|
+
depthOf: walk.depthOf,
|
|
52
|
+
dropped: walk.dropped,
|
|
53
|
+
pure: walk.pure,
|
|
54
|
+
cuts: { modules: planned.cutModules, typeOnly: planned.typeOnlyCuts, outbound: walk.cuts.outbound },
|
|
55
|
+
surface: planned.surface,
|
|
56
|
+
tests: {
|
|
57
|
+
verdict: testsVerdict(walk),
|
|
58
|
+
carried: walk.tests.flatMap((file) => {
|
|
59
|
+
const runner = planned.testRunners[file];
|
|
60
|
+
return runner === undefined ? [] : [{ file, runner }];
|
|
61
|
+
}),
|
|
62
|
+
helpers: walk.helpers,
|
|
63
|
+
left: walk.testsLeft.map((left) => ({ ...left, why: TEST_LEFT_REASON[left.reason] })),
|
|
64
|
+
},
|
|
65
|
+
packages: planned.packages,
|
|
66
|
+
workspacePackages: planned.workspacePackages,
|
|
67
|
+
externals: planned.externals,
|
|
68
|
+
exposure: planned.exposure,
|
|
69
|
+
blockers: planBlockers(planned),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `carveout cut`: writes the folder and its `carveout.json` as the CLI
|
|
74
|
+
* does, and returns the receipt as data instead of writing it under
|
|
75
|
+
* `~/.carveout/receipts/`.
|
|
76
|
+
*/
|
|
77
|
+
export async function cut(options) {
|
|
78
|
+
const offline = options.offline === true;
|
|
79
|
+
const types = offline || options.typesCache !== undefined ? null : mkdtempSync(join(tmpdir(), "carveout-types-"));
|
|
80
|
+
try {
|
|
81
|
+
const result = await cutSlice({
|
|
82
|
+
...requestOf(options),
|
|
83
|
+
out: options.out,
|
|
84
|
+
offline,
|
|
85
|
+
public: options.public ?? false,
|
|
86
|
+
receiptInMemory: true,
|
|
87
|
+
...(options.typesCache !== undefined ? { typesCache: options.typesCache } : types === null ? {} : { typesCache: types }),
|
|
88
|
+
...(options.log === undefined ? {} : { log: options.log }),
|
|
89
|
+
});
|
|
90
|
+
const receipt = result.receiptData;
|
|
91
|
+
if (receipt === null)
|
|
92
|
+
throw new Error("The cut kept no receipt.");
|
|
93
|
+
return {
|
|
94
|
+
out: result.out,
|
|
95
|
+
record: receipt.folderRecord ?? receipt.record,
|
|
96
|
+
ready: result.record.ready,
|
|
97
|
+
blockers: result.record.blockers,
|
|
98
|
+
receipt,
|
|
99
|
+
readiness: result.readiness,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (types !== null)
|
|
104
|
+
rmSync(types, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `carveout patch`: the folder's edits as a patch for the repository, its
|
|
109
|
+
* imports and names put back, with what the hand-off policy keeps out in
|
|
110
|
+
* `refused` (the CLI's exit 6) and what reaches past the slice in
|
|
111
|
+
* `boundary` (exit 5). Judged by the receipt given, never by the folder's
|
|
112
|
+
* own record; runs nothing from the folder.
|
|
113
|
+
*/
|
|
114
|
+
export function patch(input) {
|
|
115
|
+
return patchProject(input.folder, {
|
|
116
|
+
receiptData: input.receipt,
|
|
117
|
+
...(input.repo === undefined ? {} : { repo: input.repo }),
|
|
118
|
+
...(input.allow === undefined ? {} : { allow: input.allow }),
|
|
119
|
+
});
|
|
120
|
+
}
|