@sprigr/cli 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/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @sprigr/cli
2
+
3
+ Sprigr Team CLI — deploy static sites and Next.js apps to **Sprigr Tenant Hosting** from your terminal.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @sprigr/cli
9
+ # or run without installing
10
+ npx @sprigr/cli --help
11
+ ```
12
+
13
+ Requires Node.js **20+**.
14
+
15
+ ## Quick start
16
+
17
+ ```bash
18
+ # 1. Get an API key from https://team.sprigr.com → Settings → API keys
19
+ export SPRIGR_API_KEY=sk_...
20
+
21
+ # 2. Deploy a static site
22
+ sprigr deploy my-site --dir ./public
23
+
24
+ # 3. Or deploy a Next.js app (built with @opennextjs/cloudflare)
25
+ sprigr deploy my-app --dir ./.open-next --framework next
26
+ ```
27
+
28
+ ## Commands
29
+
30
+ ### `sprigr deploy <siteId> --dir <dir>`
31
+
32
+ Bundle a directory and deploy it as a new build for the given website.
33
+
34
+ | Flag | Description | Default |
35
+ | --- | --- | --- |
36
+ | `--dir <path>` | Directory to bundle | required |
37
+ | `--framework <static\|next\|astro\|remix>` | Framework hint | `static` |
38
+ | `--endpoint <url>` | API endpoint override | `https://api.team.sprigr.com` |
39
+ | `--api-key <key>` | API key (overrides env) | from `SPRIGR_API_KEY` |
40
+
41
+ The CLI streams progress and prints the build id. Use `sprigr builds get` to follow status, or watch the portal's Builds panel.
42
+
43
+ ### `sprigr builds list <siteId>`
44
+
45
+ List recent builds for a site.
46
+
47
+ | Flag | Description | Default |
48
+ | --- | --- | --- |
49
+ | `--limit <N>` | Number of rows | `20` |
50
+
51
+ ### `sprigr builds get <siteId> <buildId>`
52
+
53
+ Fetch a single build's status, including logs once available.
54
+
55
+ ## Environment variables
56
+
57
+ | Variable | Purpose |
58
+ | --- | --- |
59
+ | `SPRIGR_API_KEY` | Authentication. Create one in the portal. |
60
+ | `SPRIGR_ENDPOINT` | API endpoint. Accepts a full URL, or the shortcuts `staging` / `prod`. |
61
+
62
+ ## How deploys work
63
+
64
+ 1. The CLI tarballs your `--dir` (gzip).
65
+ 2. It POSTs to the Sprigr Team **provisioning** worker, which enqueues a build job.
66
+ 3. The build farm container picks it up, runs the framework adapter (no-op for `static`, `@opennextjs/cloudflare build` for `next`), and streams output back.
67
+ 4. On success the bundle is uploaded to **Workers for Platforms** for SSR sites or to **R2 + website-serve** for static, then the site's production deployment pointer flips atomically.
68
+
69
+ Static sites are billed by request count; SSR sites are billed by request count + container-seconds during the build. Plan limits (max sites, retention, custom domains) are enforced at deploy time.
70
+
71
+ ## Roadmap
72
+
73
+ - PKCE login (`sprigr login`) so you don't need to copy an API key from the portal.
74
+ - Binary file support in the bundler (currently text + UTF-8 only).
75
+ - `sprigr logs <buildId> --follow` for live tailing.
76
+
77
+ ## Links
78
+
79
+ - Sprigr — <https://sprigr.com>
80
+ - Issues — <https://github.com/greben99/sprigr-team/issues>
81
+
82
+ ## License
83
+
84
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Thin HTTP client for the Sprigr public API. All routes go through the
3
+ * gateway (`/api/v1/...`), authenticated by the same `sk_mcp_*` Bearer
4
+ * token the MCP layer accepts. The gateway resolves the key to a
5
+ * `companyId` and rewrites the path to `/api/data/...` before forwarding
6
+ * to the provisioning worker.
7
+ */
8
+ /**
9
+ * Wire shape for an individual file in a build manifest. UTF-8 text files
10
+ * ride as bare strings (back-compat); binary files (images, fonts, etc.)
11
+ * ride as `{ encoding: 'base64', content }`. The server enforces the same
12
+ * 5 MiB-per-file cap on decoded bytes regardless of variant.
13
+ */
14
+ export type SourceFile = string | {
15
+ encoding: 'base64';
16
+ content: string;
17
+ };
18
+ export interface ApiClientOptions {
19
+ endpoint: string;
20
+ apiKey: string;
21
+ /** Override the global fetch — useful for tests. */
22
+ fetchImpl?: typeof fetch;
23
+ }
24
+ export interface BuildSummary {
25
+ id: string;
26
+ status: 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'timeout';
27
+ framework: string | null;
28
+ source: string;
29
+ deploymentId: string | null;
30
+ containerSeconds: number | null;
31
+ durationMs: number | null;
32
+ errorSummary: string | null;
33
+ /** R2 key for the captured stdout/stderr. Null for static builds (no
34
+ * shell-out) and for early-failure builds where the container never
35
+ * responded. Pass through `getBuildLog` to fetch the bytes. */
36
+ logR2Key: string | null;
37
+ startedAt: string | null;
38
+ finishedAt: string | null;
39
+ createdAt: string;
40
+ }
41
+ export declare class ApiError extends Error {
42
+ readonly status: number;
43
+ readonly body?: unknown | undefined;
44
+ constructor(status: number, message: string, body?: unknown | undefined);
45
+ }
46
+ export declare class ApiClient {
47
+ private readonly endpoint;
48
+ private readonly apiKey;
49
+ private readonly fetchImpl;
50
+ constructor(opts: ApiClientOptions);
51
+ /** POST /api/v1/data/websites/:siteId/builds */
52
+ startBuild(args: {
53
+ siteId: string;
54
+ files: Record<string, SourceFile>;
55
+ framework?: 'static' | 'next' | 'astro' | 'remix';
56
+ source?: 'upload' | 'cli' | 'agent';
57
+ }): Promise<{
58
+ buildId: string;
59
+ status: string;
60
+ framework: string;
61
+ source: string;
62
+ }>;
63
+ /**
64
+ * POST /api/v1/data/marketplace/apps/publish
65
+ *
66
+ * Publish a new version of a marketplace app. The body carries the
67
+ * parsed manifest separately from the bundle so the server doesn't
68
+ * re-parse the bundle to route the build. Server re-runs all the
69
+ * validators a second time (defence in depth) and returns the new
70
+ * version + an optional buildId when SSR builds are queued.
71
+ */
72
+ publishMarketplaceApp(args: {
73
+ manifest: unknown;
74
+ files: Record<string, SourceFile>;
75
+ }): Promise<{
76
+ slug: string;
77
+ version: string;
78
+ appId: string;
79
+ versionId: string;
80
+ buildId: string | null;
81
+ /**
82
+ * Auto-upgrade fan-out outcome. Present on every publish into an existing
83
+ * app; omitted for the new-app branch (no installs to fan out to). When
84
+ * `failed > 0` the publisher should expect some installs are pinned to
85
+ * the new version but still serving the old WFP bundle — recover with
86
+ * `sprigr app upgrade <slug> --force` on those tenants, or grep
87
+ * system_logs AE for `marketplace.upgrade.fanout_failed` /
88
+ * `marketplace.upgrade.fanout_threw` by `app_slug`.
89
+ */
90
+ fanout?: {
91
+ upgraded: number;
92
+ skipped: number;
93
+ failed: number;
94
+ threw?: string;
95
+ };
96
+ }>;
97
+ /**
98
+ * POST /api/v1/data/apps/:slug/install/upgrade
99
+ *
100
+ * Manually upgrade this company's install of `appSlug` to the latest
101
+ * approved version. Bumps `pinned_version_id` and enqueues a fresh
102
+ * build via `enqueueBuildForMarketplaceVersion`, so the per-install
103
+ * WFP script picks up the new bundle. Returns the `buildId` for
104
+ * status polling.
105
+ *
106
+ * Idempotent — returns HTTP 400 "Already on latest version" if the
107
+ * install is already pinned to current_version (unless `force=true`).
108
+ * Integration/tool kind only; agent templates use a different update
109
+ * path.
110
+ *
111
+ * Use this to recover from a silent publish-fanout enqueue failure
112
+ * (the symptom: a fresh version exists but no build with
113
+ * `triggeredBy: 'publish-fanout'` appears in the install's
114
+ * website_status after publish). Pass `force=true` when the pin
115
+ * already moved but no build deployed — the endpoint will re-run
116
+ * `applyMarketplaceInstallUpgrade` to re-enqueue the build.
117
+ */
118
+ upgradeMarketplaceInstall(args: {
119
+ appSlug: string;
120
+ /** Bypass the "Already on latest version" guard. See doc above. */
121
+ force?: boolean;
122
+ }): Promise<{
123
+ ok: true;
124
+ installationId: string;
125
+ oldVersionId: string | null;
126
+ newVersionId: string;
127
+ newVersion: string;
128
+ buildId: string;
129
+ }>;
130
+ /**
131
+ * DELETE /api/v1/data/apps/:slug
132
+ *
133
+ * Hard-deletes a marketplace app + all its versions + any leftover
134
+ * install rows owned by the publisher. Releases the slug so it can be
135
+ * republished under a different publisher. Only the current publisher
136
+ * can call this (server-side check on company_id).
137
+ *
138
+ * Use case: migrating an app between publishers (e.g. personal
139
+ * staging tenant -> sprigr-hq). Walk the runbook at
140
+ * sprigr-apps/docs/migrate-app-publisher.md; uninstall on every
141
+ * installer tenant first, THEN delete from the old publisher, THEN
142
+ * re-publish under the new publisher.
143
+ *
144
+ * Returns `{ ok: true }` on success. 403 if the calling profile is
145
+ * not the current publisher. 404 if the slug doesn't exist.
146
+ */
147
+ deleteMarketplaceApp(args: {
148
+ appSlug: string;
149
+ }): Promise<{
150
+ ok: true;
151
+ }>;
152
+ /**
153
+ * POST /api/v1/data/apps/:slug/install
154
+ *
155
+ * Install a marketplace app on the calling tenant. Body shape is
156
+ * snake_case (granted_scopes, install_secrets); camelCase parses but
157
+ * the server silently ignores unknown keys and then 400s with
158
+ * "Missing required secrets: ..." — the publisher-migration footgun
159
+ * documented in sprigr-apps/docs/migrate-app-publisher.md.
160
+ *
161
+ * `grantedScopes` MUST be a subset of manifest.permissions.scopes.
162
+ * `installSecrets` keys MUST appear in manifest.secrets[]; every
163
+ * `required: true` entry must be present and non-empty.
164
+ *
165
+ * Returns the install row id + the per-install website + the
166
+ * triggered build id (for SSR apps).
167
+ */
168
+ /**
169
+ * POST /api/v1/data/apps/:slug/share
170
+ *
171
+ * Generate a share token + auto-bump trust_tier from `private` to
172
+ * `shared` so other tenants in the same env can install the app
173
+ * without the publisher's credentials. Publisher-only — 404 if the
174
+ * caller doesn't own the app.
175
+ *
176
+ * Calling again rotates the token (the previous one stops working)
177
+ * but the trust_tier stays at whatever it already is — only the
178
+ * private→shared promotion happens automatically.
179
+ */
180
+ shareMarketplaceApp(args: {
181
+ appSlug: string;
182
+ }): Promise<{
183
+ share_token: string;
184
+ share_url: string;
185
+ }>;
186
+ installMarketplaceApp(args: {
187
+ appSlug: string;
188
+ grantedScopes: string[];
189
+ installSecrets?: Record<string, string>;
190
+ agentIds?: string[] | null;
191
+ config?: Record<string, unknown>;
192
+ }): Promise<{
193
+ installation: {
194
+ id: string;
195
+ integration_id?: string;
196
+ website_id?: string | null;
197
+ website_slug?: string | null;
198
+ build_id?: string | null;
199
+ };
200
+ }>;
201
+ /**
202
+ * POST /api/v1/data/apps/:slug/publisher-secrets
203
+ *
204
+ * Seed or rotate the publisher-provided secret bag on the app row.
205
+ * Auth: caller must be the app's publisher. Plaintext values are
206
+ * never returned — `updated_keys` and `total_keys` confirm what
207
+ * landed without revealing values.
208
+ */
209
+ setMarketplaceAppPublisherSecrets(args: {
210
+ appSlug: string;
211
+ secrets: Record<string, string>;
212
+ }): Promise<{
213
+ ok: true;
214
+ updated_keys: string[];
215
+ total_keys: number;
216
+ }>;
217
+ /** GET /api/v1/data/websites/:siteId/builds */
218
+ listBuilds(siteId: string, limit?: number): Promise<{
219
+ builds: BuildSummary[];
220
+ }>;
221
+ /** GET /api/v1/data/websites/:siteId/builds/:buildId */
222
+ getBuild(siteId: string, buildId: string): Promise<{
223
+ build: BuildSummary;
224
+ }>;
225
+ /**
226
+ * GET /api/v1/data/websites/:siteId/builds/:buildId/log
227
+ *
228
+ * Returns the captured stdout/stderr as plain text, or `null` when the
229
+ * server reports no log exists (404). Other failures throw `ApiError`
230
+ * so the caller can distinguish "no log" from "unreachable".
231
+ */
232
+ getBuildLog(siteId: string, buildId: string): Promise<string | null>;
233
+ private request;
234
+ }
package/dist/api.js ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Thin HTTP client for the Sprigr public API. All routes go through the
3
+ * gateway (`/api/v1/...`), authenticated by the same `sk_mcp_*` Bearer
4
+ * token the MCP layer accepts. The gateway resolves the key to a
5
+ * `companyId` and rewrites the path to `/api/data/...` before forwarding
6
+ * to the provisioning worker.
7
+ */
8
+ export class ApiError extends Error {
9
+ status;
10
+ body;
11
+ constructor(status, message, body) {
12
+ super(message);
13
+ this.status = status;
14
+ this.body = body;
15
+ this.name = 'ApiError';
16
+ }
17
+ }
18
+ export class ApiClient {
19
+ endpoint;
20
+ apiKey;
21
+ fetchImpl;
22
+ constructor(opts) {
23
+ this.endpoint = opts.endpoint;
24
+ this.apiKey = opts.apiKey;
25
+ this.fetchImpl = opts.fetchImpl ?? fetch;
26
+ }
27
+ /** POST /api/v1/data/websites/:siteId/builds */
28
+ async startBuild(args) {
29
+ return this.request('POST', `/api/v1/data/websites/${encodeURIComponent(args.siteId)}/builds`, {
30
+ files: args.files,
31
+ framework: args.framework ?? 'static',
32
+ source: args.source ?? 'cli',
33
+ });
34
+ }
35
+ /**
36
+ * POST /api/v1/data/marketplace/apps/publish
37
+ *
38
+ * Publish a new version of a marketplace app. The body carries the
39
+ * parsed manifest separately from the bundle so the server doesn't
40
+ * re-parse the bundle to route the build. Server re-runs all the
41
+ * validators a second time (defence in depth) and returns the new
42
+ * version + an optional buildId when SSR builds are queued.
43
+ */
44
+ async publishMarketplaceApp(args) {
45
+ return this.request('POST', '/api/v1/data/marketplace/apps/publish', {
46
+ manifest: args.manifest,
47
+ files: args.files,
48
+ });
49
+ }
50
+ /**
51
+ * POST /api/v1/data/apps/:slug/install/upgrade
52
+ *
53
+ * Manually upgrade this company's install of `appSlug` to the latest
54
+ * approved version. Bumps `pinned_version_id` and enqueues a fresh
55
+ * build via `enqueueBuildForMarketplaceVersion`, so the per-install
56
+ * WFP script picks up the new bundle. Returns the `buildId` for
57
+ * status polling.
58
+ *
59
+ * Idempotent — returns HTTP 400 "Already on latest version" if the
60
+ * install is already pinned to current_version (unless `force=true`).
61
+ * Integration/tool kind only; agent templates use a different update
62
+ * path.
63
+ *
64
+ * Use this to recover from a silent publish-fanout enqueue failure
65
+ * (the symptom: a fresh version exists but no build with
66
+ * `triggeredBy: 'publish-fanout'` appears in the install's
67
+ * website_status after publish). Pass `force=true` when the pin
68
+ * already moved but no build deployed — the endpoint will re-run
69
+ * `applyMarketplaceInstallUpgrade` to re-enqueue the build.
70
+ */
71
+ async upgradeMarketplaceInstall(args) {
72
+ return this.request('POST', `/api/v1/data/apps/${encodeURIComponent(args.appSlug)}/install/upgrade`, args.force ? { force: true } : {});
73
+ }
74
+ /**
75
+ * DELETE /api/v1/data/apps/:slug
76
+ *
77
+ * Hard-deletes a marketplace app + all its versions + any leftover
78
+ * install rows owned by the publisher. Releases the slug so it can be
79
+ * republished under a different publisher. Only the current publisher
80
+ * can call this (server-side check on company_id).
81
+ *
82
+ * Use case: migrating an app between publishers (e.g. personal
83
+ * staging tenant -> sprigr-hq). Walk the runbook at
84
+ * sprigr-apps/docs/migrate-app-publisher.md; uninstall on every
85
+ * installer tenant first, THEN delete from the old publisher, THEN
86
+ * re-publish under the new publisher.
87
+ *
88
+ * Returns `{ ok: true }` on success. 403 if the calling profile is
89
+ * not the current publisher. 404 if the slug doesn't exist.
90
+ */
91
+ async deleteMarketplaceApp(args) {
92
+ return this.request('DELETE', `/api/v1/data/apps/${encodeURIComponent(args.appSlug)}`);
93
+ }
94
+ /**
95
+ * POST /api/v1/data/apps/:slug/install
96
+ *
97
+ * Install a marketplace app on the calling tenant. Body shape is
98
+ * snake_case (granted_scopes, install_secrets); camelCase parses but
99
+ * the server silently ignores unknown keys and then 400s with
100
+ * "Missing required secrets: ..." — the publisher-migration footgun
101
+ * documented in sprigr-apps/docs/migrate-app-publisher.md.
102
+ *
103
+ * `grantedScopes` MUST be a subset of manifest.permissions.scopes.
104
+ * `installSecrets` keys MUST appear in manifest.secrets[]; every
105
+ * `required: true` entry must be present and non-empty.
106
+ *
107
+ * Returns the install row id + the per-install website + the
108
+ * triggered build id (for SSR apps).
109
+ */
110
+ /**
111
+ * POST /api/v1/data/apps/:slug/share
112
+ *
113
+ * Generate a share token + auto-bump trust_tier from `private` to
114
+ * `shared` so other tenants in the same env can install the app
115
+ * without the publisher's credentials. Publisher-only — 404 if the
116
+ * caller doesn't own the app.
117
+ *
118
+ * Calling again rotates the token (the previous one stops working)
119
+ * but the trust_tier stays at whatever it already is — only the
120
+ * private→shared promotion happens automatically.
121
+ */
122
+ async shareMarketplaceApp(args) {
123
+ return this.request('POST', `/api/v1/data/apps/${encodeURIComponent(args.appSlug)}/share`, {});
124
+ }
125
+ async installMarketplaceApp(args) {
126
+ const body = { granted_scopes: args.grantedScopes };
127
+ if (args.installSecrets)
128
+ body.install_secrets = args.installSecrets;
129
+ if (args.agentIds !== undefined)
130
+ body.agent_ids = args.agentIds;
131
+ if (args.config !== undefined)
132
+ body.config = args.config;
133
+ return this.request('POST', `/api/v1/data/apps/${encodeURIComponent(args.appSlug)}/install`, body);
134
+ }
135
+ /**
136
+ * POST /api/v1/data/apps/:slug/publisher-secrets
137
+ *
138
+ * Seed or rotate the publisher-provided secret bag on the app row.
139
+ * Auth: caller must be the app's publisher. Plaintext values are
140
+ * never returned — `updated_keys` and `total_keys` confirm what
141
+ * landed without revealing values.
142
+ */
143
+ async setMarketplaceAppPublisherSecrets(args) {
144
+ return this.request('POST', `/api/v1/data/apps/${encodeURIComponent(args.appSlug)}/publisher-secrets`, { secrets: args.secrets });
145
+ }
146
+ /** GET /api/v1/data/websites/:siteId/builds */
147
+ async listBuilds(siteId, limit = 20) {
148
+ return this.request('GET', `/api/v1/data/websites/${encodeURIComponent(siteId)}/builds?limit=${limit}`);
149
+ }
150
+ /** GET /api/v1/data/websites/:siteId/builds/:buildId */
151
+ async getBuild(siteId, buildId) {
152
+ return this.request('GET', `/api/v1/data/websites/${encodeURIComponent(siteId)}/builds/${encodeURIComponent(buildId)}`);
153
+ }
154
+ /**
155
+ * GET /api/v1/data/websites/:siteId/builds/:buildId/log
156
+ *
157
+ * Returns the captured stdout/stderr as plain text, or `null` when the
158
+ * server reports no log exists (404). Other failures throw `ApiError`
159
+ * so the caller can distinguish "no log" from "unreachable".
160
+ */
161
+ async getBuildLog(siteId, buildId) {
162
+ const path = `/api/v1/data/websites/${encodeURIComponent(siteId)}/builds/${encodeURIComponent(buildId)}/log`;
163
+ const res = await this.fetchImpl(`${this.endpoint}${path}`, {
164
+ method: 'GET',
165
+ headers: {
166
+ Authorization: `Bearer ${this.apiKey}`,
167
+ Accept: 'text/plain, application/json',
168
+ },
169
+ });
170
+ if (res.status === 404)
171
+ return null;
172
+ const text = await res.text();
173
+ if (!res.ok) {
174
+ let parsed = text;
175
+ try {
176
+ parsed = text.length > 0 ? JSON.parse(text) : undefined;
177
+ }
178
+ catch { /* keep as text */ }
179
+ const p = parsed;
180
+ const message = typeof p === 'object' && p && typeof p.message === 'string'
181
+ ? p.message
182
+ : (typeof p === 'object' && p && typeof p.error === 'string'
183
+ ? p.error
184
+ : `HTTP ${res.status} ${res.statusText}`);
185
+ throw new ApiError(res.status, message, parsed);
186
+ }
187
+ return text;
188
+ }
189
+ async request(method, path, body) {
190
+ const headers = {
191
+ Authorization: `Bearer ${this.apiKey}`,
192
+ Accept: 'application/json',
193
+ };
194
+ let bodyText;
195
+ if (body !== undefined) {
196
+ headers['Content-Type'] = 'application/json';
197
+ bodyText = JSON.stringify(body);
198
+ }
199
+ const res = await this.fetchImpl(`${this.endpoint}${path}`, { method, headers, body: bodyText });
200
+ const text = await res.text();
201
+ let parsed;
202
+ try {
203
+ parsed = text.length > 0 ? JSON.parse(text) : undefined;
204
+ }
205
+ catch {
206
+ parsed = text;
207
+ }
208
+ if (!res.ok) {
209
+ const p = parsed;
210
+ const message = typeof p === 'object' && p && typeof p.message === 'string'
211
+ ? p.message
212
+ : (typeof p === 'object' && p && typeof p.error === 'string'
213
+ ? p.error
214
+ : `HTTP ${res.status} ${res.statusText}`);
215
+ throw new ApiError(res.status, message, parsed);
216
+ }
217
+ return parsed;
218
+ }
219
+ }
220
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/auth.d.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Resolve the Sprigr API key.
3
+ *
4
+ * Resolution order:
5
+ * 1. `--api-key <key>` flag
6
+ * 2. `SPRIGR_API_KEY` env var
7
+ * 3. Credential file
8
+ * - When `profile` is set: `~/.config/sprigr/credentials/<profile>.json`
9
+ * - Otherwise (legacy default): `~/.config/sprigr/credentials.json`
10
+ *
11
+ * If none are set, returns null. Callers must surface a clear "run
12
+ * `sprigr login` or set SPRIGR_API_KEY" message rather than failing
13
+ * silently.
14
+ */
15
+ export declare function resolveApiKey(opts: {
16
+ flag?: string;
17
+ profile?: string;
18
+ }): string | null;
19
+ /**
20
+ * Compute the credentials file path. Honors $XDG_CONFIG_HOME so users
21
+ * who customize their config layout still land in the right place;
22
+ * falls back to ~/.config/sprigr otherwise (Linux + macOS convention).
23
+ *
24
+ * When `profile` is given (e.g. `--profile staging` or `SPRIGR_PROFILE=...`),
25
+ * the file lives under `<config>/sprigr/credentials/<profile>.json` so
26
+ * multiple tenants can be logged in side-by-side without overwriting each
27
+ * other. When omitted, returns the legacy single-file path so existing
28
+ * setups keep working unchanged.
29
+ */
30
+ export declare function credentialsFilePath(profile?: string): string;
31
+ /**
32
+ * Profile names map to filenames on disk; reject anything that could
33
+ * escape the credentials directory or look like a path traversal. The
34
+ * regex below also doubles as a sanity check on length (max 64 chars).
35
+ */
36
+ export declare function assertValidProfileName(name: string): void;
37
+ /**
38
+ * Stored credentials. Only `apiKey` is required to authenticate; the
39
+ * rest is metadata the CLI surfaces to the user (e.g. `sprigr whoami`)
40
+ * and to make audit-style writes attributable.
41
+ */
42
+ export interface StoredCredentials {
43
+ apiKey: string;
44
+ keyPrefix?: string;
45
+ keyId?: string;
46
+ companyId?: string;
47
+ userId?: string;
48
+ endpoint?: string;
49
+ /** ISO 8601 timestamp from when the file was written. */
50
+ loggedInAt: string;
51
+ /**
52
+ * The profile this credential set was saved under. Stamped on write
53
+ * so `sprigr whoami` can echo it back; never read for authentication
54
+ * (the disk path is the source of truth for which profile is active).
55
+ */
56
+ profile?: string;
57
+ }
58
+ /**
59
+ * Read the `endpoint` field from the credentials file written by
60
+ * `sprigr login`. Used by `resolveEndpoint` so a `--endpoint` passed
61
+ * to `login` sticks for subsequent commands in the same session
62
+ * without the user having to re-pass it (or export SPRIGR_ENDPOINT).
63
+ *
64
+ * Without this, `sprigr login --endpoint https://staging-api-team.sprigr.com`
65
+ * mints a key against staging, but the next `sprigr app publish` (no
66
+ * flag) sends that staging key to the prod gateway — which rejects it
67
+ * 401 because the key row only exists in the staging registry DB.
68
+ * The CLI then surfaces "HTTP 401 — Unauthorized" with no hint that
69
+ * the user is talking to the wrong env.
70
+ */
71
+ export declare function readEndpointFromFileSync(profile?: string): string | null;
72
+ /** Read the full credentials record (used by `whoami`). Returns null on miss. */
73
+ export declare function readCredentialsSync(profile?: string): StoredCredentials | null;
74
+ /**
75
+ * List every profile we have credentials for. Returns the literal names
76
+ * (sans .json suffix); when only the legacy single-file credentials exist
77
+ * the special name `'(default)'` is included so `sprigr whoami --all` can
78
+ * show it. Returns an empty array if nothing is logged in.
79
+ */
80
+ export declare function listProfilesSync(): Array<{
81
+ name: string;
82
+ isLegacy: boolean;
83
+ }>;
84
+ /**
85
+ * Write credentials to disk with mode 0600 (owner read/write only).
86
+ * The directory is created with mode 0700 if it doesn't yet exist —
87
+ * sibling tools like `vercel`, `gh`, `wrangler` follow the same
88
+ * convention. Overwrites any existing credentials.
89
+ *
90
+ * When `profile` is set the file lives at `<config>/sprigr/credentials/<profile>.json`
91
+ * (and the parent `credentials/` dir is created at 0700 too). Without a
92
+ * profile we write the legacy `<config>/sprigr/credentials.json` so
93
+ * existing setups stay byte-identical.
94
+ */
95
+ export declare function writeCredentials(creds: StoredCredentials, profile?: string): Promise<string>;
96
+ /**
97
+ * Delete the credential file. Returns true if a file was removed,
98
+ * false if no file was there to begin with. Other errors (permission,
99
+ * etc.) propagate so the caller can surface a useful message.
100
+ */
101
+ export declare function deleteCredentials(profile?: string): Promise<boolean>;
102
+ /** Friendly message shown when no API key is configured. */
103
+ export declare const MISSING_API_KEY_MESSAGE = "No API key found.\n\nRun `sprigr login` to authenticate via the portal, or set\nSPRIGR_API_KEY in your shell:\n\n export SPRIGR_API_KEY=sk_mcp_xxxxx\n\nYou can also pass --api-key on the command line. Manage keys at\nteam.sprigr.com under Settings \u2192 API keys.\n\nTo log in to a specific tenant without overwriting another, use a profile:\n\n sprigr login --profile staging --endpoint staging\n sprigr login --profile prod --endpoint prod\n sprigr --profile staging app publish --dir <dir>\n SPRIGR_PROFILE=prod sprigr app publish --dir <dir>\n";