@shipd-ai/cli 0.1.0
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 +133 -0
- package/dist/auth.d.ts +26 -0
- package/dist/auth.js +75 -0
- package/dist/client.d.ts +51 -0
- package/dist/client.js +70 -0
- package/dist/commands/auth.d.ts +22 -0
- package/dist/commands/auth.js +109 -0
- package/dist/commands/checks.d.ts +12 -0
- package/dist/commands/checks.js +669 -0
- package/dist/contract.d.ts +129 -0
- package/dist/contract.js +24 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +49 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.js +93 -0
- package/dist/harbor.d.ts +25 -0
- package/dist/harbor.js +137 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +95 -0
- package/dist/plan.d.ts +65 -0
- package/dist/plan.js +367 -0
- package/dist/quests.d.ts +24 -0
- package/dist/quests.js +181 -0
- package/dist/report.d.ts +62 -0
- package/dist/report.js +107 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Shipd CLI (`@shipd-ai/cli`)
|
|
2
|
+
|
|
3
|
+
`shipd` is the admin command line for Shipd quests. Point it at a Harbor task
|
|
4
|
+
directory (or an existing task) and it runs the quest's checks on the quest's
|
|
5
|
+
own infrastructure, then returns the same structured output the product shows.
|
|
6
|
+
Olympus is the first quest; every quest that implements the small
|
|
7
|
+
`adhocChecks:*` contract works the same way.
|
|
8
|
+
|
|
9
|
+
Admins only: every quest entrypoint checks the caller's effective admin role.
|
|
10
|
+
|
|
11
|
+
## Install and log in
|
|
12
|
+
|
|
13
|
+
Admin docs, the supported checks, and a copy-paste setup prompt for coding agents live at https://shipd.ai/admin/cli (admins only; direct link to the prompt: https://shipd.ai/admin/cli#agent-setup-prompt).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @shipd-ai/cli
|
|
17
|
+
shipd login # opens https://shipd.ai/quests/olympus/cli-auth
|
|
18
|
+
shipd whoami
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`login` opens the token page in your browser; paste the token back into the
|
|
22
|
+
terminal. It is stored at `~/.shipd/credentials.json` (mode 0600). The token is
|
|
23
|
+
personal: never commit it, never pass it on a command line. `shipd logout`
|
|
24
|
+
deletes it.
|
|
25
|
+
|
|
26
|
+
## Three input modes
|
|
27
|
+
|
|
28
|
+
| Mode | Command | What happens | Writes to production |
|
|
29
|
+
| --- | --- | --- | --- |
|
|
30
|
+
| Harbor, stateless (default) | `shipd checks run <harbor-dir> --check …` | Adapts the bundle, runs the checks on the quest's infrastructure, returns their normal output. | Nothing. |
|
|
31
|
+
| Harbor, create | `shipd checks run <harbor-dir> --create --check …` | Turns the bundle into a real draft task under your account (every authoring guard applies), then runs the checks live on it. | A draft task + version and the normal job rows. Not submitted for review. |
|
|
32
|
+
| Existing task | `shipd checks run --task <problemId> --check …` | Runs the checks live on the task's latest version. | The normal job rows; the version's visible results are re-pointed. |
|
|
33
|
+
|
|
34
|
+
Both live modes print a `Writes to production:` line before anything is
|
|
35
|
+
dispatched; `--create` prints the new task URL as soon as the draft exists.
|
|
36
|
+
Checks cost real compute: use `--dry-run` first and confirm `--all` with a
|
|
37
|
+
human when an agent is driving.
|
|
38
|
+
|
|
39
|
+
## Harbor layout
|
|
40
|
+
|
|
41
|
+
Only these paths are read; everything else in the directory is ignored.
|
|
42
|
+
|
|
43
|
+
| Path | Used as | Required for |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `task.toml` | `[metadata]` (`problem_title`, `category`, `language`, `difficulty`, `repository_url`, `base_commit_hash`), `[environment].docker_image`, `[verifier].timeout_sec` | all |
|
|
46
|
+
| `instruction.md` | task description | description-bearing checks |
|
|
47
|
+
| `environment/Dockerfile` | sandbox image | sandbox checks without `docker_image` |
|
|
48
|
+
| `tests/test.patch` | test patch | every check except `verifyBuild` |
|
|
49
|
+
| `tests/test.sh` | carried for future native runs | unused by v1 checks |
|
|
50
|
+
| `solution/solution.patch` | solution patch | solution-bearing checks |
|
|
51
|
+
| `solution/solve.sh` | carried for future native runs | unused by v1 checks |
|
|
52
|
+
|
|
53
|
+
A missing or unparsable `task.toml` is a bundle error (exit 2). The seven
|
|
54
|
+
files may total at most 4 MiB. `--repo`, `--commit`, `--title`, `--category`,
|
|
55
|
+
`--language` and `--difficulty` override the `task.toml` metadata, which is how
|
|
56
|
+
a non-Olympus bundle fills in what it lacks.
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
shipd checks list --quest olympus
|
|
62
|
+
shipd checks run ./task --all --dry-run # eligibility + adapted inputs, spends nothing
|
|
63
|
+
shipd checks run ./task --check verifyBuild,descriptionQuality
|
|
64
|
+
shipd checks run ./task --all --json --out report.json
|
|
65
|
+
shipd checks run ./task --create --check autoReview # creates a draft, runs live
|
|
66
|
+
shipd checks run --task <problemId> --check scopeGate # runs live on an existing task
|
|
67
|
+
shipd checks run ./task --check verifyBuild --no-wait # prints handles as JSON lines
|
|
68
|
+
shipd checks status '<handle json>' # or: shipd checks status report.json
|
|
69
|
+
shipd checks cancel '<handle json>' # or: shipd checks cancel report.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`run` options: `--quest` (default `olympus`), `--check a,b` or `--all`,
|
|
73
|
+
`--dry-run`, `--no-wait`, `--json`, `--out <file>`, metadata overrides,
|
|
74
|
+
`--model` and `--effort low|medium|high|xhigh` (honored where the production
|
|
75
|
+
dispatcher honors them), `--poll <seconds>` (default 15), `--timeout <minutes>`
|
|
76
|
+
per step (default 180, the longest worker timeout).
|
|
77
|
+
|
|
78
|
+
Checks whose required bundle parts are missing are reported as `skipped` with
|
|
79
|
+
the missing list. With `--all` that is informational; with `--check` it fails
|
|
80
|
+
the run. Multi-step checks (for example `autoReview`) run as a step plan the
|
|
81
|
+
quest describes: independent steps in parallel, each later step fed the
|
|
82
|
+
earlier outputs, a failed step marking its dependents `skipped: upstream
|
|
83
|
+
failed`. Prechecks (`precheck:<stage>`) run synchronously.
|
|
84
|
+
|
|
85
|
+
Ctrl-C asks whether to cancel the in-flight steps; a second Ctrl-C leaves them
|
|
86
|
+
running. Handles stay in the JSON report, so `checks status` and
|
|
87
|
+
`checks cancel` keep working on them afterwards.
|
|
88
|
+
|
|
89
|
+
## Exit codes
|
|
90
|
+
|
|
91
|
+
| Code | Meaning |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| 0 | every run check passed or warned |
|
|
94
|
+
| 1 | a check failed or errored (or an explicitly requested check was skipped) |
|
|
95
|
+
| 2 | usage, auth, or bundle error (nothing was dispatched) |
|
|
96
|
+
| 3 | cancelled |
|
|
97
|
+
|
|
98
|
+
## Report
|
|
99
|
+
|
|
100
|
+
`--json` and `--out` produce the same document: `quest`, `mode`, `source`
|
|
101
|
+
(bundle directory and `inputHash`, or the task ids, title and URL), a top-level
|
|
102
|
+
`taskUrl` in live modes, one entry per
|
|
103
|
+
check with `status`, `verdict`, `output`, `error`, `durationMs`, and `steps`
|
|
104
|
+
(each step's handle and output), plus `prechecks` keyed by stage id.
|
|
105
|
+
|
|
106
|
+
## Environment
|
|
107
|
+
|
|
108
|
+
| Variable | Effect |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `SHIPD_<QUEST>_URL` | Override the quest's base URL (for example `SHIPD_OLYMPUS_URL`). |
|
|
111
|
+
| `SHIPD_<QUEST>_CONVEX_URL` | Skip `/api/cli/config` and talk to this Convex deployment. |
|
|
112
|
+
| `SHIPD_HOME` | Credentials and caches directory (default `~/.shipd`). |
|
|
113
|
+
| `SHIPD_NO_UPDATE_CHECK=1` | Skip the version check. |
|
|
114
|
+
| `NODE_ENV=development` | Use the localhost quest URLs. |
|
|
115
|
+
|
|
116
|
+
## Developer notes
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pnpm --dir packages/shipd-cli typecheck
|
|
120
|
+
pnpm --dir packages/shipd-cli test
|
|
121
|
+
pnpm --dir packages/shipd-cli build
|
|
122
|
+
pnpm --dir packages/shipd-cli cli checks list --quest olympus # runs src/ with tsx
|
|
123
|
+
NODE_ENV=development pnpm --dir packages/shipd-cli cli checks list # against a local quest
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The CLI contains no quest logic. It calls `adhocChecks:*` by name through
|
|
127
|
+
Convex's `anyApi`; the shared argument and return shapes live in
|
|
128
|
+
`src/contract.ts`, and each quest mirrors them with validators. The plan
|
|
129
|
+
executor (`src/plan.ts`) and bundle reader (`src/harbor.ts`) are unit-tested
|
|
130
|
+
against a fake quest and a fixture bundle in `test/`.
|
|
131
|
+
|
|
132
|
+
Publishing: `pnpm --dir packages/shipd-cli build && npm publish --access public`
|
|
133
|
+
from an account in the `@shipd-ai` npm org.
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare function shipdDir(): string;
|
|
2
|
+
export interface DecodedIdentity {
|
|
3
|
+
sub: string;
|
|
4
|
+
email: string;
|
|
5
|
+
username: string;
|
|
6
|
+
name: string;
|
|
7
|
+
picture: string;
|
|
8
|
+
exp: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Decode the JWT payload without verifying the signature. The quest's Convex
|
|
12
|
+
* deployment verifies it on every request; the CLI only reads the payload for
|
|
13
|
+
* display (name, email) and the local expiry check.
|
|
14
|
+
*/
|
|
15
|
+
export declare function decodeJwtPayload(token: string): DecodedIdentity;
|
|
16
|
+
export declare function ensureShipdDir(): string;
|
|
17
|
+
export declare function saveCredentials(token: string): DecodedIdentity;
|
|
18
|
+
export declare function loadCredentials(): {
|
|
19
|
+
token: string;
|
|
20
|
+
identity: DecodedIdentity;
|
|
21
|
+
} | null;
|
|
22
|
+
export declare function clearCredentials(): boolean;
|
|
23
|
+
export declare function requireAuth(): {
|
|
24
|
+
token: string;
|
|
25
|
+
identity: DecodedIdentity;
|
|
26
|
+
};
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { AuthError } from "./errors.js";
|
|
5
|
+
export function shipdDir() {
|
|
6
|
+
return resolve(process.env.SHIPD_HOME ?? resolve(homedir(), ".shipd"));
|
|
7
|
+
}
|
|
8
|
+
function credentialsPath() {
|
|
9
|
+
return resolve(shipdDir(), "credentials.json");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Decode the JWT payload without verifying the signature. The quest's Convex
|
|
13
|
+
* deployment verifies it on every request; the CLI only reads the payload for
|
|
14
|
+
* display (name, email) and the local expiry check.
|
|
15
|
+
*/
|
|
16
|
+
export function decodeJwtPayload(token) {
|
|
17
|
+
const parts = token.split(".");
|
|
18
|
+
if (parts.length !== 3) {
|
|
19
|
+
throw new AuthError("Invalid token: expected a JWT with three segments.");
|
|
20
|
+
}
|
|
21
|
+
const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
|
|
22
|
+
const decoded = JSON.parse(payload);
|
|
23
|
+
if (typeof decoded.exp !== "number" || typeof decoded.sub !== "string") {
|
|
24
|
+
throw new AuthError("Invalid token: payload is missing sub or exp.");
|
|
25
|
+
}
|
|
26
|
+
return decoded;
|
|
27
|
+
}
|
|
28
|
+
export function ensureShipdDir() {
|
|
29
|
+
const dir = shipdDir();
|
|
30
|
+
if (!existsSync(dir)) {
|
|
31
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
32
|
+
}
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
|
35
|
+
export function saveCredentials(token) {
|
|
36
|
+
const identity = decodeJwtPayload(token);
|
|
37
|
+
const expiresAt = new Date(identity.exp * 1000).toISOString();
|
|
38
|
+
ensureShipdDir();
|
|
39
|
+
const data = { token, expiresAt };
|
|
40
|
+
writeFileSync(credentialsPath(), `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
41
|
+
return identity;
|
|
42
|
+
}
|
|
43
|
+
export function loadCredentials() {
|
|
44
|
+
const path = credentialsPath();
|
|
45
|
+
if (!existsSync(path))
|
|
46
|
+
return null;
|
|
47
|
+
try {
|
|
48
|
+
const stored = JSON.parse(readFileSync(path, "utf-8"));
|
|
49
|
+
if (!stored.token)
|
|
50
|
+
return null;
|
|
51
|
+
const identity = decodeJwtPayload(stored.token);
|
|
52
|
+
const now = Math.floor(Date.now() / 1000);
|
|
53
|
+
if (identity.exp <= now)
|
|
54
|
+
return null;
|
|
55
|
+
return { token: stored.token, identity };
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function clearCredentials() {
|
|
62
|
+
const path = credentialsPath();
|
|
63
|
+
if (existsSync(path)) {
|
|
64
|
+
unlinkSync(path);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
export function requireAuth() {
|
|
70
|
+
const creds = loadCredentials();
|
|
71
|
+
if (!creds) {
|
|
72
|
+
throw new AuthError("Not logged in or the token has expired. Run: shipd login");
|
|
73
|
+
}
|
|
74
|
+
return creds;
|
|
75
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AdaptResult, Catalog, Handle, HarborBundle, LiveStatus, Options, PrecheckResult, StartStepResult, StepStatus, TaskRef } from "./contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* Everything the plan executor needs from a quest. `QuestClient` implements it
|
|
4
|
+
* over Convex; tests implement it with a fake.
|
|
5
|
+
*/
|
|
6
|
+
export interface QuestApi {
|
|
7
|
+
readonly quest: string;
|
|
8
|
+
listChecks(): Promise<Catalog>;
|
|
9
|
+
adapt(bundle: HarborBundle): Promise<AdaptResult>;
|
|
10
|
+
runPrecheckStage(bundle: HarborBundle, stageId: string): Promise<PrecheckResult>;
|
|
11
|
+
startStep(bundle: HarborBundle, checkKey: string, stepId: string, priorOutputs: Record<string, unknown>, options?: Options): Promise<StartStepResult>;
|
|
12
|
+
getStep(handle: Handle): Promise<StepStatus>;
|
|
13
|
+
cancelStep(handle: Handle): Promise<{
|
|
14
|
+
cancelled: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
fetchArtifact(handle: Handle, key: string): Promise<string | null>;
|
|
17
|
+
createFromHarbor(bundle: HarborBundle): Promise<TaskRef>;
|
|
18
|
+
resolveTask(problemId: string): Promise<TaskRef | null>;
|
|
19
|
+
runLiveCheck(versionId: string, problemId: string, checkKey: string, options?: Options): Promise<{
|
|
20
|
+
started: boolean;
|
|
21
|
+
detail?: string;
|
|
22
|
+
}>;
|
|
23
|
+
getLiveCheck(versionId: string, checkKey: string): Promise<LiveStatus>;
|
|
24
|
+
nudgeSynthesis(versionId: string): Promise<null>;
|
|
25
|
+
}
|
|
26
|
+
/** The bundle as sent over the wire: `dir`, `metadata`, `files` and nothing else. */
|
|
27
|
+
export declare function toWireBundle(bundle: HarborBundle): HarborBundle;
|
|
28
|
+
export declare class QuestClient implements QuestApi {
|
|
29
|
+
readonly quest: string;
|
|
30
|
+
private readonly convex;
|
|
31
|
+
constructor(quest: string, convexUrl: string, token: string);
|
|
32
|
+
listChecks(): Promise<Catalog>;
|
|
33
|
+
adapt(bundle: HarborBundle): Promise<AdaptResult>;
|
|
34
|
+
runPrecheckStage(bundle: HarborBundle, stageId: string): Promise<PrecheckResult>;
|
|
35
|
+
startStep(bundle: HarborBundle, checkKey: string, stepId: string, priorOutputs: Record<string, unknown>, options?: Options): Promise<StartStepResult>;
|
|
36
|
+
getStep(handle: Handle): Promise<StepStatus>;
|
|
37
|
+
cancelStep(handle: Handle): Promise<{
|
|
38
|
+
cancelled: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
fetchArtifact(handle: Handle, key: string): Promise<string | null>;
|
|
41
|
+
createFromHarbor(bundle: HarborBundle): Promise<TaskRef>;
|
|
42
|
+
resolveTask(problemId: string): Promise<TaskRef | null>;
|
|
43
|
+
runLiveCheck(versionId: string, problemId: string, checkKey: string, options?: Options): Promise<{
|
|
44
|
+
started: boolean;
|
|
45
|
+
detail?: string;
|
|
46
|
+
}>;
|
|
47
|
+
getLiveCheck(versionId: string, checkKey: string): Promise<LiveStatus>;
|
|
48
|
+
nudgeSynthesis(versionId: string): Promise<null>;
|
|
49
|
+
}
|
|
50
|
+
/** Build an authenticated client for a quest, or throw an AuthError/UsageError. */
|
|
51
|
+
export declare function connectQuest(quest: string): Promise<QuestClient>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
2
|
+
import { anyApi } from "convex/server";
|
|
3
|
+
import { requireAuth } from "./auth.js";
|
|
4
|
+
import { questConvexUrl } from "./quests.js";
|
|
5
|
+
/** The bundle as sent over the wire: `dir`, `metadata`, `files` and nothing else. */
|
|
6
|
+
export function toWireBundle(bundle) {
|
|
7
|
+
return { dir: bundle.dir, metadata: bundle.metadata, files: bundle.files };
|
|
8
|
+
}
|
|
9
|
+
function withOptions(args, options) {
|
|
10
|
+
return options && Object.keys(options).length > 0 ? { ...args, options } : args;
|
|
11
|
+
}
|
|
12
|
+
// Functions are addressed by name through `anyApi` so the CLI has no
|
|
13
|
+
// dependency on any quest's generated API at typecheck time.
|
|
14
|
+
const fns = anyApi.adhocChecks;
|
|
15
|
+
export class QuestClient {
|
|
16
|
+
quest;
|
|
17
|
+
convex;
|
|
18
|
+
constructor(quest, convexUrl, token) {
|
|
19
|
+
this.quest = quest;
|
|
20
|
+
this.convex = new ConvexHttpClient(convexUrl);
|
|
21
|
+
this.convex.setAuth(token);
|
|
22
|
+
}
|
|
23
|
+
listChecks() {
|
|
24
|
+
return this.convex.query(fns.listChecks, {});
|
|
25
|
+
}
|
|
26
|
+
adapt(bundle) {
|
|
27
|
+
return this.convex.query(fns.adapt, { bundle: toWireBundle(bundle) });
|
|
28
|
+
}
|
|
29
|
+
runPrecheckStage(bundle, stageId) {
|
|
30
|
+
return this.convex.action(fns.runPrecheckStage, {
|
|
31
|
+
bundle: toWireBundle(bundle),
|
|
32
|
+
stageId,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
startStep(bundle, checkKey, stepId, priorOutputs, options) {
|
|
36
|
+
return this.convex.action(fns.startStep, withOptions({ bundle: toWireBundle(bundle), checkKey, stepId, priorOutputs }, options));
|
|
37
|
+
}
|
|
38
|
+
getStep(handle) {
|
|
39
|
+
return this.convex.action(fns.getStep, { handle });
|
|
40
|
+
}
|
|
41
|
+
cancelStep(handle) {
|
|
42
|
+
return this.convex.action(fns.cancelStep, { handle });
|
|
43
|
+
}
|
|
44
|
+
fetchArtifact(handle, key) {
|
|
45
|
+
return this.convex.action(fns.fetchArtifact, { handle, key });
|
|
46
|
+
}
|
|
47
|
+
createFromHarbor(bundle) {
|
|
48
|
+
return this.convex.action(fns.createFromHarbor, {
|
|
49
|
+
bundle: toWireBundle(bundle),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
resolveTask(problemId) {
|
|
53
|
+
return this.convex.query(fns.resolveTask, { problemId });
|
|
54
|
+
}
|
|
55
|
+
runLiveCheck(versionId, problemId, checkKey, options) {
|
|
56
|
+
return this.convex.action(fns.runLiveCheck, withOptions({ versionId, problemId, checkKey }, options));
|
|
57
|
+
}
|
|
58
|
+
getLiveCheck(versionId, checkKey) {
|
|
59
|
+
return this.convex.query(fns.getLiveCheck, { versionId, checkKey });
|
|
60
|
+
}
|
|
61
|
+
nudgeSynthesis(versionId) {
|
|
62
|
+
return this.convex.action(fns.nudgeSynthesis, { versionId });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Build an authenticated client for a quest, or throw an AuthError/UsageError. */
|
|
66
|
+
export async function connectQuest(quest) {
|
|
67
|
+
const { token } = requireAuth();
|
|
68
|
+
const convexUrl = await questConvexUrl(quest);
|
|
69
|
+
return new QuestClient(quest, convexUrl, token);
|
|
70
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const login: import("citty").CommandDef<{
|
|
2
|
+
readonly token: {
|
|
3
|
+
readonly type: "string";
|
|
4
|
+
readonly description: "Paste the token directly instead of prompting";
|
|
5
|
+
};
|
|
6
|
+
readonly "no-browser": {
|
|
7
|
+
readonly type: "boolean";
|
|
8
|
+
readonly description: "Print the login URL instead of opening it";
|
|
9
|
+
};
|
|
10
|
+
readonly quest: {
|
|
11
|
+
readonly type: "string";
|
|
12
|
+
readonly description: "Quest whose login page to open (default: olympus)";
|
|
13
|
+
readonly default: "olympus";
|
|
14
|
+
};
|
|
15
|
+
}>;
|
|
16
|
+
export declare const logout: import("citty").CommandDef<import("citty").ArgsDef>;
|
|
17
|
+
export declare const whoami: import("citty").CommandDef<{
|
|
18
|
+
readonly json: {
|
|
19
|
+
readonly type: "boolean";
|
|
20
|
+
readonly description: "Output as JSON";
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { exec, execFile } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import { clearCredentials, loadCredentials, saveCredentials } from "../auth.js";
|
|
5
|
+
import { UsageError } from "../errors.js";
|
|
6
|
+
import { printJson } from "../format.js";
|
|
7
|
+
import { DEFAULT_QUEST, questAuthUrl } from "../quests.js";
|
|
8
|
+
function openBrowser(url) {
|
|
9
|
+
if (process.platform === "win32") {
|
|
10
|
+
// `start` is a cmd.exe built-in, not a standalone executable.
|
|
11
|
+
exec(`start "" "${url.replace(/"/g, '\\"')}"`, () => { });
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
15
|
+
execFile(cmd, [url], () => { });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function prompt(question) {
|
|
19
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
rl.question(question, (answer) => {
|
|
22
|
+
rl.close();
|
|
23
|
+
resolve(answer.trim());
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const questArg = {
|
|
28
|
+
quest: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: `Quest whose login page to open (default: ${DEFAULT_QUEST})`,
|
|
31
|
+
default: DEFAULT_QUEST,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
export const login = defineCommand({
|
|
35
|
+
meta: { name: "login", description: "Sign in via the browser and store the CLI token" },
|
|
36
|
+
args: {
|
|
37
|
+
...questArg,
|
|
38
|
+
token: { type: "string", description: "Paste the token directly instead of prompting" },
|
|
39
|
+
"no-browser": { type: "boolean", description: "Print the login URL instead of opening it" },
|
|
40
|
+
},
|
|
41
|
+
run: async ({ args }) => {
|
|
42
|
+
const authUrl = questAuthUrl(args.quest);
|
|
43
|
+
let token = args.token?.trim();
|
|
44
|
+
if (!token) {
|
|
45
|
+
if (args["no-browser"]) {
|
|
46
|
+
console.log(`\n Open this page and copy the token:\n ${authUrl}\n`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(`\n Opening browser...\n ${authUrl}\n`);
|
|
50
|
+
openBrowser(authUrl);
|
|
51
|
+
}
|
|
52
|
+
token = await prompt(" Paste your token: ");
|
|
53
|
+
}
|
|
54
|
+
if (!token)
|
|
55
|
+
throw new UsageError("No token provided.");
|
|
56
|
+
const identity = saveCredentials(token);
|
|
57
|
+
const expiresAt = new Date(identity.exp * 1000).toLocaleDateString();
|
|
58
|
+
console.log(`\n Logged in as ${identity.name || identity.email || identity.sub}`);
|
|
59
|
+
if (identity.username)
|
|
60
|
+
console.log(` Username: @${identity.username}`);
|
|
61
|
+
console.log(` Expires: ${expiresAt}\n`);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
export const logout = defineCommand({
|
|
65
|
+
meta: { name: "logout", description: "Delete the stored CLI token" },
|
|
66
|
+
run: async () => {
|
|
67
|
+
const removed = clearCredentials();
|
|
68
|
+
console.log(removed ? " Logged out." : " Not logged in.");
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
export const whoami = defineCommand({
|
|
72
|
+
meta: { name: "whoami", description: "Show who the stored token belongs to" },
|
|
73
|
+
args: {
|
|
74
|
+
json: { type: "boolean", description: "Output as JSON" },
|
|
75
|
+
},
|
|
76
|
+
run: async ({ args }) => {
|
|
77
|
+
const creds = loadCredentials();
|
|
78
|
+
if (!creds) {
|
|
79
|
+
if (args.json) {
|
|
80
|
+
printJson({ authenticated: false });
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(" Not logged in. Run: shipd login");
|
|
84
|
+
}
|
|
85
|
+
process.exitCode = 2;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const { identity } = creds;
|
|
89
|
+
const expiresAt = new Date(identity.exp * 1000);
|
|
90
|
+
const daysLeft = Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
91
|
+
if (args.json) {
|
|
92
|
+
printJson({
|
|
93
|
+
authenticated: true,
|
|
94
|
+
userId: identity.sub,
|
|
95
|
+
email: identity.email,
|
|
96
|
+
username: identity.username,
|
|
97
|
+
name: identity.name,
|
|
98
|
+
expiresAt: expiresAt.toISOString(),
|
|
99
|
+
daysLeft,
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log(` Logged in as ${identity.name || identity.email || identity.sub}`);
|
|
104
|
+
if (identity.username)
|
|
105
|
+
console.log(` Username: @${identity.username}`);
|
|
106
|
+
console.log(` User ID: ${identity.sub}`);
|
|
107
|
+
console.log(` Expires: ${expiresAt.toLocaleDateString()} (${daysLeft}d)`);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Catalog, type CheckDef, type TaskRef } from "../contract.js";
|
|
2
|
+
import { type Report } from "../report.js";
|
|
3
|
+
/** "a + b → c": steps grouped by dependency depth. */
|
|
4
|
+
export declare function formatSteps(check: CheckDef): string;
|
|
5
|
+
export declare function selectChecks(catalog: Catalog, explicit: string | undefined, all: boolean): {
|
|
6
|
+
checks: CheckDef[];
|
|
7
|
+
selection: Report["selection"];
|
|
8
|
+
};
|
|
9
|
+
/** Lines for `--dry-run --task`: what would be dispatched, without dispatching. */
|
|
10
|
+
export declare function dryRunTaskLines(task: TaskRef, checks: CheckDef[]): string[];
|
|
11
|
+
declare const _default: import("citty").CommandDef<import("citty").ArgsDef>;
|
|
12
|
+
export default _default;
|