shiply-cli 0.27.2 → 0.29.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/dist/access.js +54 -0
- package/dist/data.js +1 -0
- package/dist/db.js +2 -0
- package/dist/domain.js +30 -0
- package/dist/drive.js +15 -4
- package/dist/index.js +151 -8
- package/dist/login.js +9 -3
- package/dist/publish.js +6 -1
- package/dist/sending-domains.js +1 -0
- package/dist/sites.js +1 -0
- package/dist/variable.js +80 -0
- package/package.json +42 -42
- package/skill/CHANGELOG.md +10 -0
- package/skill/SKILL.md +256 -253
- package/skill/references/client-work.md +93 -12
- package/skill/references/custom-domains.md +2 -0
- package/skill/references/databases.md +5 -0
- package/skill/references/drives.md +70 -0
- package/skill/references/email.md +24 -0
- package/skill/references/publishing.md +3 -1
- package/skill/references/site-features.md +26 -8
- package/skill/references/ssr-frameworks.md +57 -57
package/dist/access.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
const headers = (apiKey) => ({
|
|
3
|
+
'content-type': 'application/json',
|
|
4
|
+
authorization: `Bearer ${apiKey}`,
|
|
5
|
+
});
|
|
6
|
+
/** Stable machine marker (mirrors SITE_READY / VERIFY) so agents can parse the
|
|
7
|
+
* policy without scraping the human lines. Counts, not values — emails can
|
|
8
|
+
* contain characters that would break k=v tokenizing. */
|
|
9
|
+
function marker(slug, p) {
|
|
10
|
+
return `ACCESS slug=${slug} mode=${p.mode} password=${p.hasPassword ? 'set' : 'none'} emails=${p.allowedEmails.length} domains=${p.allowedDomains.length}`;
|
|
11
|
+
}
|
|
12
|
+
function printPolicy(slug, p) {
|
|
13
|
+
if (p.mode === 'public') {
|
|
14
|
+
console.log(` ${slug} is public — anyone with the URL can view it`);
|
|
15
|
+
}
|
|
16
|
+
else if (p.mode === 'password') {
|
|
17
|
+
console.log(` ${slug} is password-protected${p.hasPassword ? '' : ' (no password set yet — visitors are locked out)'}`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(` ${slug} is invite-only (restricted)`);
|
|
21
|
+
for (const e of p.allowedEmails)
|
|
22
|
+
console.log(` email: ${e}`);
|
|
23
|
+
for (const d of p.allowedDomains)
|
|
24
|
+
console.log(` domain: @${d.replace(/^@/, '')}`);
|
|
25
|
+
if (p.allowedEmails.length === 0 && p.allowedDomains.length === 0) {
|
|
26
|
+
console.log(' (no emails or domains allowed — only the owner can view)');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
console.log(marker(slug, p));
|
|
30
|
+
}
|
|
31
|
+
export async function accessShow(ctx, slug) {
|
|
32
|
+
const base = resolveBase(ctx.base);
|
|
33
|
+
const p = await api(`${base}/api/v1/publishes/${encodeURIComponent(slug)}/access`, {
|
|
34
|
+
headers: headers(ctx.apiKey),
|
|
35
|
+
});
|
|
36
|
+
printPolicy(slug, p);
|
|
37
|
+
}
|
|
38
|
+
export async function accessSet(ctx, slug, input) {
|
|
39
|
+
const base = resolveBase(ctx.base);
|
|
40
|
+
const body = { mode: input.mode };
|
|
41
|
+
if (input.mode === 'password')
|
|
42
|
+
body.password = input.password;
|
|
43
|
+
if (input.mode === 'restricted') {
|
|
44
|
+
body.allowedEmails = input.allowedEmails ?? [];
|
|
45
|
+
body.allowedDomains = input.allowedDomains ?? [];
|
|
46
|
+
}
|
|
47
|
+
const p = await api(`${base}/api/v1/publishes/${encodeURIComponent(slug)}/access`, {
|
|
48
|
+
method: 'PATCH',
|
|
49
|
+
headers: headers(ctx.apiKey),
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
});
|
|
52
|
+
console.log(`✔ access updated — existing visitor cookies are now invalid`);
|
|
53
|
+
printPolicy(slug, p);
|
|
54
|
+
}
|
package/dist/data.js
CHANGED
|
@@ -105,6 +105,7 @@ export async function dataExport(ctx, slug, collection, opts) {
|
|
|
105
105
|
export async function dataWipeCollection(ctx, slug, collection, opts) {
|
|
106
106
|
if (!opts.yes) {
|
|
107
107
|
console.error(` refusing — wipe-collection deletes EVERY record in "${collection}" on ${slug}. Re-run with --yes to confirm.`);
|
|
108
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
const base = resolveBase(ctx.base);
|
package/dist/db.js
CHANGED
|
@@ -144,6 +144,7 @@ export async function dbMigrate(ctx, idOrName, dir) {
|
|
|
144
144
|
export async function dbDelete(ctx, idOrName, opts) {
|
|
145
145
|
if (!opts.yes) {
|
|
146
146
|
console.log(` refusing — \`db delete\` drops the database "${idOrName}" and every row in it. Re-run with --yes to confirm.`);
|
|
147
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
149
150
|
const id = await resolveId(ctx, idOrName);
|
|
@@ -195,6 +196,7 @@ export async function dbBranches(ctx, parentNameOrId) {
|
|
|
195
196
|
export async function dbDeleteBranch(ctx, branchIdOrName, opts) {
|
|
196
197
|
if (!opts.yes) {
|
|
197
198
|
console.log(` refusing — \`db delete-branch\` drops the branch "${branchIdOrName}" and its Neon endpoint. Re-run with --yes to confirm.`);
|
|
199
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
198
200
|
return;
|
|
199
201
|
}
|
|
200
202
|
const base = resolveBase(ctx.base);
|
package/dist/domain.js
CHANGED
|
@@ -143,6 +143,36 @@ export async function domainPrimary(ctx, domain, hostname) {
|
|
|
143
143
|
console.log(` ${s.hostname}`);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
+
export async function domainRm(ctx, domain, opts = {}) {
|
|
147
|
+
if (!opts.yes) {
|
|
148
|
+
throw new Error(`refusing to remove ${domain} without --yes (subdomains mapped to it stop resolving)`);
|
|
149
|
+
}
|
|
150
|
+
const base = resolveBase(ctx.base);
|
|
151
|
+
const r = await api(`${base}/api/v1/custom-domains/${enc(domain)}`, {
|
|
152
|
+
method: 'DELETE',
|
|
153
|
+
headers: headers(ctx.apiKey),
|
|
154
|
+
});
|
|
155
|
+
console.log(`✔ removed ${r.removed}`);
|
|
156
|
+
}
|
|
157
|
+
/** Re-poll CF cert status + live TLS/HTTPS probe for all subdomains of a
|
|
158
|
+
* registered custom domain. Run this after `domain connect` / `domain sub add`
|
|
159
|
+
* until every subdomain reports ready=true. */
|
|
160
|
+
export async function domainCheck(ctx, domain) {
|
|
161
|
+
const base = resolveBase(ctx.base);
|
|
162
|
+
const r = await api(`${base}/api/v1/custom-domains/${enc(domain)}/check`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: headers(ctx.apiKey),
|
|
165
|
+
body: '{}',
|
|
166
|
+
});
|
|
167
|
+
if (!r.subdomains?.length) {
|
|
168
|
+
console.log(`No subdomains mapped under ${domain} yet. shiply domain sub add <host> --site <slug>`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
for (const s of r.subdomains) {
|
|
172
|
+
console.log(` ${s.hostname} ${s.ready ? '✔ ready' : '✖ not ready'}`);
|
|
173
|
+
console.log(`DOMAIN_CHECK hostname=${s.hostname} ready=${s.ready}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
146
176
|
export async function domainSync(ctx, domain) {
|
|
147
177
|
const base = resolveBase(ctx.base);
|
|
148
178
|
const r = await api(`${base}/api/v1/custom-domains/${enc(domain)}/sync-dns`, { method: 'POST', headers: headers(ctx.apiKey), body: '{}' });
|
package/dist/drive.js
CHANGED
|
@@ -30,8 +30,12 @@ export async function drivePut(ctx, file, asPath) {
|
|
|
30
30
|
const stage = await api(`${base}/api/v1/drives/${id}/files/uploads`, { method: 'POST', headers: headers(ctx.apiKey), body: JSON.stringify({ files: [meta] }) });
|
|
31
31
|
for (const u of stage.uploads) {
|
|
32
32
|
const res = await fetch(u.url, { method: 'PUT', body: new Uint8Array(body) });
|
|
33
|
-
if (!res.ok)
|
|
34
|
-
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
if (res.status === 401 || res.status === 403) {
|
|
35
|
+
throw new Error(`upload failed for ${u.path}: HTTP ${res.status} — the upload URL is not authorized. Re-run \`shiply drive put\` to mint a fresh one.`);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`upload failed for ${u.path}: HTTP ${res.status} — the object store rejected the file. Re-run \`shiply drive put\`; if it keeps failing, check the file size/name.`);
|
|
38
|
+
}
|
|
35
39
|
}
|
|
36
40
|
await api(`${base}/api/v1/drives/${id}/files/finalize`, {
|
|
37
41
|
method: 'POST',
|
|
@@ -45,8 +49,15 @@ export async function driveGet(ctx, path, out) {
|
|
|
45
49
|
const id = await defaultDrive(ctx);
|
|
46
50
|
const { url } = await api(`${base}/api/v1/drives/${id}/files/${path.split('/').map(encodeURIComponent).join('/')}`, { headers: headers(ctx.apiKey) });
|
|
47
51
|
const res = await fetch(url);
|
|
48
|
-
if (!res.ok)
|
|
49
|
-
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
if (res.status === 404) {
|
|
54
|
+
throw new Error(`download failed: HTTP 404 — no file at "${path}" in this drive. Check the path with \`shiply drive ls\`.`);
|
|
55
|
+
}
|
|
56
|
+
if (res.status === 401 || res.status === 403) {
|
|
57
|
+
throw new Error(`download failed: HTTP ${res.status} — the download URL is not authorized. Re-run \`shiply drive get\` to mint a fresh one.`);
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`download failed: HTTP ${res.status} for "${path}" — re-run \`shiply drive get\`; if it keeps failing, the object may be corrupted.`);
|
|
60
|
+
}
|
|
50
61
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
51
62
|
if (out) {
|
|
52
63
|
await writeFile(out, buf);
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { detectIsr } from './isr.js';
|
|
|
15
15
|
import { runClaimVerify } from './claim.js';
|
|
16
16
|
import { dbAttach, dbBranch, dbBranches, dbCreate, dbDelete, dbDeleteBranch, dbLs, dbMerge, dbMigrate, dbSql, } from './db.js';
|
|
17
17
|
import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
|
|
18
|
-
import { domainAdd, domainConnect, domainLs, domainPrimary, domainSubAdd, domainSync } from './domain.js';
|
|
18
|
+
import { domainAdd, domainCheck, domainConnect, domainLs, domainPrimary, domainRm, domainSubAdd, domainSync } from './domain.js';
|
|
19
19
|
import { runProject } from './projects.js';
|
|
20
20
|
import { runClient, parseClientArg } from './clients.js';
|
|
21
21
|
import { runListing } from './listings.js';
|
|
@@ -33,8 +33,10 @@ import { readState, writeState } from './state.js';
|
|
|
33
33
|
import { readPreview, writePreview } from './previews.js';
|
|
34
34
|
import { checkReadiness, targetToHostname } from './status.js';
|
|
35
35
|
import { runStatus } from './status-cmd.js';
|
|
36
|
+
import { accessSet, accessShow } from './access.js';
|
|
36
37
|
import { sitesLs, sitesPromote, sitesRm, sitesRollback } from './sites.js';
|
|
37
38
|
import { verifySite } from './verify.js';
|
|
39
|
+
import { variableAttach, variableDetach, variableLs, variableRm, variableSet } from './variable.js';
|
|
38
40
|
// Resolve the package.json beside the compiled dist/ so `shiply --version`
|
|
39
41
|
// can print the running CLI version (works in dev tsc output + npm install).
|
|
40
42
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -62,6 +64,10 @@ Usage:
|
|
|
62
64
|
shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
|
|
63
65
|
(no arg → account/plan status, see below)
|
|
64
66
|
shiply duplicate <slug> Server-side copy of an owned site → new URL
|
|
67
|
+
shiply ls List sites in your account
|
|
68
|
+
shiply rm <slug> --yes Permanently delete an owned site
|
|
69
|
+
shiply rollback <slug> [versionId] Re-point a site to a prior finalized deploy
|
|
70
|
+
(omit versionId to list deploys and pick one)
|
|
65
71
|
shiply promote <preview-slug> --to <dest-slug>
|
|
66
72
|
Copy the exact previewed bytes into a production site (no rebuild)
|
|
67
73
|
shiply detect [dir] Diagnose: print framework + dir that would upload
|
|
@@ -75,12 +81,26 @@ Usage:
|
|
|
75
81
|
shiply db branches <db> List branches of a Neon database
|
|
76
82
|
shiply db delete-branch <branch-or-id> --yes Delete a Neon branch
|
|
77
83
|
shiply db merge <branch> No-op alias — prints the pg_dump migration tip
|
|
84
|
+
shiply access <slug> Show who can view the site (public/password/restricted)
|
|
85
|
+
shiply access <slug> --public Open the site to everyone
|
|
86
|
+
shiply access <slug> --password <pw> Password-protect the site (paid plan)
|
|
87
|
+
shiply access <slug> --restricted --email a@b.com --domain example.com
|
|
88
|
+
Invite-only: only listed emails / email domains
|
|
89
|
+
(--email/--domain repeatable; replaces the previous lists)
|
|
78
90
|
shiply domain ls List custom domains on your account
|
|
79
91
|
shiply domain add <domain> Register a custom domain
|
|
80
92
|
shiply domain connect <domain> One-click OAuth DNS setup (Cloudflare/Domain Connect)
|
|
81
93
|
shiply domain sub add <host> --site <slug> Map a subdomain (e.g. app.example.com) to a slug
|
|
82
94
|
shiply domain sync <domain> Re-sync DNS records for a connected domain
|
|
83
95
|
shiply domain primary <domain> <hostname> Mark hostname as primary; siblings 301-redirect (SEO)
|
|
96
|
+
shiply domain check <domain> Re-poll cert/DNS/HTTPS readiness for every subdomain
|
|
97
|
+
shiply domain rm <domain> --yes Remove a custom domain (its subdomains stop resolving)
|
|
98
|
+
shiply variable <ls|set|rm|attach|detach> Account-level encrypted variables (e.g. API keys for sites)
|
|
99
|
+
shiply variable set <NAME> <value> Save/update a variable (or: --stdin to read the value)
|
|
100
|
+
shiply variable ls [--reveal] List variables (masked by default)
|
|
101
|
+
shiply variable rm <NAME> --yes Delete a variable
|
|
102
|
+
shiply variable attach <NAME> --site <slug> Expose a saved variable to one site's Worker env
|
|
103
|
+
shiply variable detach <NAME> --site <slug> Stop exposing a variable to a site's Worker env
|
|
84
104
|
shiply project <ls|create|get|archive|restore>
|
|
85
105
|
Customer-intake projects + AI brief generator
|
|
86
106
|
shiply project create <label> [--customer-email <e>] [--customer-name <n>]
|
|
@@ -213,7 +233,7 @@ async function main() {
|
|
|
213
233
|
// parseArgs sees it. (A prior lookahead that cancelled the flag when a
|
|
214
234
|
// positional followed made `shiply publish --json ./dist` swallow the dir as
|
|
215
235
|
// the --json value, leaving no <dir>.)
|
|
216
|
-
const BARE_JSON_CMDS = new Set(['status', 'publish', 'update', 'logs']);
|
|
236
|
+
const BARE_JSON_CMDS = new Set(['status', 'publish', 'update', 'logs', 'variable']);
|
|
217
237
|
const bareJsonFlag = BARE_JSON_CMDS.has(rawArgv[0]) && rawArgv.includes('--json');
|
|
218
238
|
if (bareJsonFlag) {
|
|
219
239
|
const idx = rawArgv.indexOf('--json');
|
|
@@ -231,7 +251,13 @@ async function main() {
|
|
|
231
251
|
key: { type: 'string' },
|
|
232
252
|
anonymous: { type: 'boolean' },
|
|
233
253
|
base: { type: 'string' },
|
|
234
|
-
email
|
|
254
|
+
// `--email` is repeatable for `access --restricted`; `login --email` uses
|
|
255
|
+
// the last value. `--domain` is the access counterpart for whole domains.
|
|
256
|
+
email: { type: 'string', multiple: true },
|
|
257
|
+
domain: { type: 'string', multiple: true },
|
|
258
|
+
public: { type: 'boolean' },
|
|
259
|
+
password: { type: 'string' },
|
|
260
|
+
restricted: { type: 'boolean' },
|
|
235
261
|
name: { type: 'string' },
|
|
236
262
|
as: { type: 'string' },
|
|
237
263
|
wait: { type: 'boolean' },
|
|
@@ -250,6 +276,8 @@ async function main() {
|
|
|
250
276
|
ts: { type: 'boolean' },
|
|
251
277
|
'preview-branch': { type: 'string' },
|
|
252
278
|
yes: { type: 'boolean' },
|
|
279
|
+
reveal: { type: 'boolean' },
|
|
280
|
+
stdin: { type: 'boolean' },
|
|
253
281
|
'no-confetti': { type: 'boolean' },
|
|
254
282
|
json: { type: 'string' },
|
|
255
283
|
limit: { type: 'string' },
|
|
@@ -617,6 +645,47 @@ async function main() {
|
|
|
617
645
|
process.exitCode = ready ? 0 : 1;
|
|
618
646
|
return;
|
|
619
647
|
}
|
|
648
|
+
case 'access': {
|
|
649
|
+
const usage = 'usage: shiply access <slug> [--public | --password <pw> | --restricted --email a@b.com --domain example.com]';
|
|
650
|
+
if (!dir)
|
|
651
|
+
throw new Error(usage);
|
|
652
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
653
|
+
if (!apiKey)
|
|
654
|
+
throw new Error('access needs an API key — run `shiply login` first');
|
|
655
|
+
const actx = { base: values.base, apiKey };
|
|
656
|
+
const modes = [
|
|
657
|
+
values.public ? 'public' : null,
|
|
658
|
+
values.password !== undefined ? 'password' : null,
|
|
659
|
+
values.restricted ? 'restricted' : null,
|
|
660
|
+
].filter(Boolean);
|
|
661
|
+
if (modes.length === 0) {
|
|
662
|
+
// No mode flag → read-only: print the current policy.
|
|
663
|
+
await accessShow(actx, dir);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (modes.length > 1)
|
|
667
|
+
throw new Error(`pick ONE mode — ${usage}`);
|
|
668
|
+
const mode = modes[0];
|
|
669
|
+
// --email/--domain are repeatable and accept comma-separated lists.
|
|
670
|
+
const split = (vals) => (vals ?? []).flatMap((v) => v.split(',')).map((s) => s.trim()).filter(Boolean);
|
|
671
|
+
const allowedEmails = split(values.email);
|
|
672
|
+
const allowedDomains = split(values.domain);
|
|
673
|
+
if (mode === 'password' && !values.password?.trim()) {
|
|
674
|
+
throw new Error('--password needs a value, e.g. shiply access <slug> --password "s3cret-phrase"');
|
|
675
|
+
}
|
|
676
|
+
if (mode !== 'restricted' && (allowedEmails.length > 0 || allowedDomains.length > 0)) {
|
|
677
|
+
throw new Error('--email/--domain only apply with --restricted');
|
|
678
|
+
}
|
|
679
|
+
if (mode === 'restricted' && allowedEmails.length === 0 && allowedDomains.length === 0) {
|
|
680
|
+
throw new Error('--restricted needs at least one --email or --domain (otherwise only you could view the site)');
|
|
681
|
+
}
|
|
682
|
+
await accessSet(actx, dir, {
|
|
683
|
+
mode,
|
|
684
|
+
...(mode === 'password' ? { password: values.password.trim() } : {}),
|
|
685
|
+
...(mode === 'restricted' ? { allowedEmails, allowedDomains } : {}),
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
620
689
|
case 'promote': {
|
|
621
690
|
const dest = values.to;
|
|
622
691
|
if (!dir || !dest)
|
|
@@ -778,8 +847,18 @@ async function main() {
|
|
|
778
847
|
throw new Error('usage: shiply domain primary <domain> <hostname>');
|
|
779
848
|
await domainPrimary(dctx, arg, arg2);
|
|
780
849
|
return;
|
|
850
|
+
case 'rm':
|
|
851
|
+
if (!arg)
|
|
852
|
+
throw new Error('usage: shiply domain rm <domain> --yes');
|
|
853
|
+
await domainRm(dctx, arg, { yes: Boolean(values.yes) });
|
|
854
|
+
return;
|
|
855
|
+
case 'check':
|
|
856
|
+
if (!arg)
|
|
857
|
+
throw new Error('usage: shiply domain check <domain>');
|
|
858
|
+
await domainCheck(dctx, arg);
|
|
859
|
+
return;
|
|
781
860
|
default:
|
|
782
|
-
throw new Error('usage: shiply domain <ls|add|connect|sub|sync|primary>');
|
|
861
|
+
throw new Error('usage: shiply domain <ls|add|connect|sub|sync|primary|rm|check>');
|
|
783
862
|
}
|
|
784
863
|
}
|
|
785
864
|
case 'db': {
|
|
@@ -850,6 +929,55 @@ async function main() {
|
|
|
850
929
|
throw new Error('usage: shiply db <create|ls|sql|migrate|delete|attach|branch|branches|delete-branch|merge>');
|
|
851
930
|
}
|
|
852
931
|
}
|
|
932
|
+
case 'variable': {
|
|
933
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
934
|
+
if (!apiKey)
|
|
935
|
+
throw new Error('variable commands need an API key — run `shiply login` first');
|
|
936
|
+
const vctx = { base: values.base, apiKey };
|
|
937
|
+
const jsonOut = bareJsonFlag;
|
|
938
|
+
const sub = dir; // positionals[1]
|
|
939
|
+
const arg = positionals[2]; // name
|
|
940
|
+
const arg2 = positionals[3]; // value (set) — unused for others
|
|
941
|
+
switch (sub) {
|
|
942
|
+
case 'ls':
|
|
943
|
+
await variableLs(vctx, { reveal: Boolean(values.reveal), json: jsonOut });
|
|
944
|
+
return;
|
|
945
|
+
case 'set': {
|
|
946
|
+
if (!arg)
|
|
947
|
+
throw new Error("usage: shiply variable set <NAME> <value> (or: shiply variable set <NAME> --stdin)");
|
|
948
|
+
let value = arg2;
|
|
949
|
+
if (values.stdin) {
|
|
950
|
+
const { readFileSync } = await import('node:fs');
|
|
951
|
+
value = readFileSync(0, 'utf8').replace(/\n$/, '');
|
|
952
|
+
}
|
|
953
|
+
if (value === undefined)
|
|
954
|
+
throw new Error("usage: shiply variable set <NAME> <value> (or: shiply variable set <NAME> --stdin)");
|
|
955
|
+
await variableSet(vctx, arg, value, { json: jsonOut });
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
case 'rm':
|
|
959
|
+
if (!arg)
|
|
960
|
+
throw new Error('usage: shiply variable rm <NAME> --yes');
|
|
961
|
+
await variableRm(vctx, arg, { yes: Boolean(values.yes), json: jsonOut });
|
|
962
|
+
return;
|
|
963
|
+
case 'attach':
|
|
964
|
+
if (!arg)
|
|
965
|
+
throw new Error('usage: shiply variable attach <NAME> --site <slug>');
|
|
966
|
+
if (!values.site)
|
|
967
|
+
throw new Error('--site <slug> is required for `shiply variable attach`');
|
|
968
|
+
await variableAttach(vctx, arg, values.site, { json: jsonOut });
|
|
969
|
+
return;
|
|
970
|
+
case 'detach':
|
|
971
|
+
if (!arg)
|
|
972
|
+
throw new Error('usage: shiply variable detach <NAME> --site <slug>');
|
|
973
|
+
if (!values.site)
|
|
974
|
+
throw new Error('--site <slug> is required for `shiply variable detach`');
|
|
975
|
+
await variableDetach(vctx, arg, values.site, { json: jsonOut });
|
|
976
|
+
return;
|
|
977
|
+
default:
|
|
978
|
+
throw new Error('usage: shiply variable <ls|set|rm|attach|detach>');
|
|
979
|
+
}
|
|
980
|
+
}
|
|
853
981
|
case 'project': {
|
|
854
982
|
await runProject(positionals.slice(1));
|
|
855
983
|
break;
|
|
@@ -993,14 +1121,17 @@ async function main() {
|
|
|
993
1121
|
break;
|
|
994
1122
|
}
|
|
995
1123
|
case 'claim': {
|
|
1124
|
+
// Normalized to exit 1 (was 2) — every other usage error in this CLI
|
|
1125
|
+
// exits 1, and automation that checks `exit !== 0` shouldn't need a
|
|
1126
|
+
// special case for this one command.
|
|
996
1127
|
if (dir !== 'verify') {
|
|
997
1128
|
console.error('Usage: shiply claim verify <SHIPLY-XXXXXXXX>');
|
|
998
|
-
process.exit(
|
|
1129
|
+
process.exit(1);
|
|
999
1130
|
}
|
|
1000
1131
|
const code = positionals[2];
|
|
1001
1132
|
if (!code) {
|
|
1002
1133
|
console.error('Usage: shiply claim verify <SHIPLY-XXXXXXXX>');
|
|
1003
|
-
process.exit(
|
|
1134
|
+
process.exit(1);
|
|
1004
1135
|
}
|
|
1005
1136
|
const r = await runClaimVerify({ code });
|
|
1006
1137
|
console.log(r.message);
|
|
@@ -1082,7 +1213,7 @@ async function main() {
|
|
|
1082
1213
|
// Default path: device flow. One click in the browser, no email round-trip.
|
|
1083
1214
|
// The --email flag opts into the legacy code-by-email path (useful for
|
|
1084
1215
|
// scripted/non-interactive runs that can't open a browser).
|
|
1085
|
-
if (!values.email) {
|
|
1216
|
+
if (!values.email?.length) {
|
|
1086
1217
|
const result = await loginViaDeviceFlow(base, values.name);
|
|
1087
1218
|
if (!result.ok) {
|
|
1088
1219
|
console.error(`✗ login failed: ${result.reason}`);
|
|
@@ -1097,7 +1228,7 @@ async function main() {
|
|
|
1097
1228
|
}
|
|
1098
1229
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1099
1230
|
try {
|
|
1100
|
-
const email = values.email;
|
|
1231
|
+
const email = values.email[values.email.length - 1];
|
|
1101
1232
|
await api(`${base}/api/auth/agent/request-code`, {
|
|
1102
1233
|
method: 'POST',
|
|
1103
1234
|
headers: { 'content-type': 'application/json' },
|
|
@@ -1131,6 +1262,18 @@ main().catch((e) => {
|
|
|
1131
1262
|
console.error(` upgrade: https://shiply.now/dashboard/plan`);
|
|
1132
1263
|
process.exit(1);
|
|
1133
1264
|
}
|
|
1265
|
+
// Auth refusals (server returns 401/403): a bare "sign in" tells an agent
|
|
1266
|
+
// nothing about HOW, and a revoked key vs a plan/ownership limit need
|
|
1267
|
+
// different next actions — branch on the code instead of printing raw text.
|
|
1268
|
+
if (e instanceof ApiError && e.code === 'unauthorized') {
|
|
1269
|
+
console.error(`✖ API key missing, invalid, or revoked — run \`shiply login\` for a fresh key (or check ~/.shiply/credentials)`);
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
if (e instanceof ApiError && e.code === 'forbidden') {
|
|
1273
|
+
console.error(`✖ ${e.message}`);
|
|
1274
|
+
console.error(` if this is a plan limit: https://shiply.now/dashboard/plan`);
|
|
1275
|
+
process.exit(1);
|
|
1276
|
+
}
|
|
1134
1277
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1135
1278
|
console.error(`✖ ${msg}`);
|
|
1136
1279
|
process.exit(1);
|
package/dist/login.js
CHANGED
|
@@ -36,9 +36,15 @@ export async function loginViaDeviceFlow(base, agentNameOverride) {
|
|
|
36
36
|
}
|
|
37
37
|
catch (e) {
|
|
38
38
|
// Transient network blip — don't kill the whole flow, the user has time.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
// Covers both server-side 5xx (ApiError) AND transport failures (DNS
|
|
40
|
+
// hiccup, connection reset, timeout) that never make it to an ApiError —
|
|
41
|
+
// those used to abort the whole device-flow wait on one dropped packet.
|
|
42
|
+
if (e instanceof ApiError) {
|
|
43
|
+
if (e.status >= 500)
|
|
44
|
+
continue;
|
|
45
|
+
throw e;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
42
48
|
}
|
|
43
49
|
if (poll.status === 'approved' && poll.api_key) {
|
|
44
50
|
return { ok: true, apiKey: poll.api_key, slug: poll.slug_claimed ?? null };
|
package/dist/publish.js
CHANGED
|
@@ -251,7 +251,12 @@ async function uploadAll(dir, targets) {
|
|
|
251
251
|
`(they're short-lived). Re-run \`shiply publish\` to mint fresh ones — ` +
|
|
252
252
|
`unchanged files are hash-skipped, so the retry is cheap.`);
|
|
253
253
|
}
|
|
254
|
-
|
|
254
|
+
if (res.status === 401 || res.status === 403) {
|
|
255
|
+
throw new Error(`upload failed for ${t.path}: HTTP ${res.status} — the upload URL is not authorized. ` +
|
|
256
|
+
`Re-run \`shiply publish\` to mint fresh presigned URLs (unchanged files are hash-skipped, so the retry is cheap).`);
|
|
257
|
+
}
|
|
258
|
+
throw new Error(`upload failed for ${t.path}: HTTP ${res.status} — the object store rejected the file. ` +
|
|
259
|
+
`Re-run \`shiply publish\`; if it keeps failing on this file, check its size/name for anything unusual.`);
|
|
255
260
|
}
|
|
256
261
|
}
|
|
257
262
|
};
|
package/dist/sending-domains.js
CHANGED
|
@@ -104,6 +104,7 @@ async function verifyAction(ctx, id, flags) {
|
|
|
104
104
|
async function removeAction(ctx, id, flags) {
|
|
105
105
|
if (!flags.yes) {
|
|
106
106
|
console.log(` refusing — \`sending-domain rm\` deletes the domain and forces existing demand tests to fall back to the shared shiply sender. Re-run with --yes to confirm.`);
|
|
107
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
107
108
|
return;
|
|
108
109
|
}
|
|
109
110
|
const base = resolveBase(ctx.base);
|
package/dist/sites.js
CHANGED
|
@@ -20,6 +20,7 @@ export async function sitesLs(ctx) {
|
|
|
20
20
|
export async function sitesRm(ctx, slug, opts) {
|
|
21
21
|
if (!opts.yes) {
|
|
22
22
|
console.error(` refusing — \`shiply rm\` PERMANENTLY deletes ${slug} and its files. Re-run with --yes to confirm.`);
|
|
23
|
+
process.exitCode = 1; // refusal is not success — scripts must see a non-zero exit
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
const base = resolveBase(ctx.base);
|
package/dist/variable.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
const headers = (apiKey) => ({
|
|
3
|
+
'content-type': 'application/json',
|
|
4
|
+
authorization: `Bearer ${apiKey}`,
|
|
5
|
+
});
|
|
6
|
+
export async function variableLs(ctx, opts = {}) {
|
|
7
|
+
const base = resolveBase(ctx.base);
|
|
8
|
+
const reveal = Boolean(opts.reveal);
|
|
9
|
+
const r = await api(`${base}/api/v1/variables${reveal ? '?reveal=1' : ''}`, { headers: headers(ctx.apiKey) });
|
|
10
|
+
if (opts.json) {
|
|
11
|
+
process.stdout.write(`${JSON.stringify(r.variables)}\n`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (r.variables.length === 0) {
|
|
15
|
+
console.log('No variables saved. shiply variable set <NAME> <value>');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
for (const v of r.variables) {
|
|
19
|
+
console.log(` ${v.name} ${reveal ? (v.value ?? '') : (v.masked ?? '')}`);
|
|
20
|
+
console.log(`VARIABLE name=${v.name} value=${reveal ? (v.value ?? '') : (v.masked ?? '')}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function variableSet(ctx, name, value, opts = {}) {
|
|
24
|
+
const base = resolveBase(ctx.base);
|
|
25
|
+
await api(`${base}/api/v1/variables`, {
|
|
26
|
+
method: 'PUT',
|
|
27
|
+
headers: headers(ctx.apiKey),
|
|
28
|
+
body: JSON.stringify({ name, value }),
|
|
29
|
+
});
|
|
30
|
+
const upper = name.trim().toUpperCase();
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
process.stdout.write(`${JSON.stringify({ name: upper, set: true })}\n`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(`✔ ${upper} saved`);
|
|
36
|
+
console.log(' not exposed to any site yet — attach it: shiply variable attach ' + upper + ' --site <slug>');
|
|
37
|
+
}
|
|
38
|
+
export async function variableRm(ctx, name, opts = {}) {
|
|
39
|
+
if (!opts.yes) {
|
|
40
|
+
throw new Error(`refusing to delete ${name} without --yes (this cannot be undone)`);
|
|
41
|
+
}
|
|
42
|
+
const base = resolveBase(ctx.base);
|
|
43
|
+
await api(`${base}/api/v1/variables/${encodeURIComponent(name)}`, {
|
|
44
|
+
method: 'DELETE',
|
|
45
|
+
headers: headers(ctx.apiKey),
|
|
46
|
+
});
|
|
47
|
+
if (opts.json) {
|
|
48
|
+
process.stdout.write(`${JSON.stringify({ name: name.trim().toUpperCase(), removed: true })}\n`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.log(`✔ ${name.trim().toUpperCase()} removed`);
|
|
52
|
+
}
|
|
53
|
+
export async function variableAttach(ctx, name, slug, opts = {}) {
|
|
54
|
+
const base = resolveBase(ctx.base);
|
|
55
|
+
await api(`${base}/api/v1/variables/${encodeURIComponent(name)}/attach`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: headers(ctx.apiKey),
|
|
58
|
+
body: JSON.stringify({ slug }),
|
|
59
|
+
});
|
|
60
|
+
const upper = name.trim().toUpperCase();
|
|
61
|
+
if (opts.json) {
|
|
62
|
+
process.stdout.write(`${JSON.stringify({ name: upper, slug, attached: true })}\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log(`✔ ${upper} attached to ${slug} — takes effect on the site's next function deploy`);
|
|
66
|
+
}
|
|
67
|
+
export async function variableDetach(ctx, name, slug, opts = {}) {
|
|
68
|
+
const base = resolveBase(ctx.base);
|
|
69
|
+
await api(`${base}/api/v1/variables/${encodeURIComponent(name)}/detach`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: headers(ctx.apiKey),
|
|
72
|
+
body: JSON.stringify({ slug }),
|
|
73
|
+
});
|
|
74
|
+
const upper = name.trim().toUpperCase();
|
|
75
|
+
if (opts.json) {
|
|
76
|
+
process.stdout.write(`${JSON.stringify({ name: upper, slug, detached: true })}\n`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log(`✔ ${upper} detached from ${slug} — takes effect on the site's next function deploy`);
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "shiply-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Publish static sites to shiply.now from the command line
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"shiply": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"dist",
|
|
12
|
-
"skill",
|
|
13
|
-
"README.md"
|
|
14
|
-
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsc -p tsconfig.build.json",
|
|
17
|
-
"prepublishOnly": "pnpm build",
|
|
18
|
-
"test": "vitest run",
|
|
19
|
-
"typecheck": "tsc --noEmit"
|
|
20
|
-
},
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=18"
|
|
23
|
-
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"shiply",
|
|
26
|
-
"hosting",
|
|
27
|
-
"static",
|
|
28
|
-
"deploy",
|
|
29
|
-
"agents",
|
|
30
|
-
"publish"
|
|
31
|
-
],
|
|
32
|
-
"homepage": "https://shiply.now",
|
|
33
|
-
"devDependencies": {
|
|
34
|
-
"@types/node": "^22.15.0",
|
|
35
|
-
"typescript": "^5.8.0",
|
|
36
|
-
"vitest": "^3.1.0"
|
|
37
|
-
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"esbuild": "^0.25.12",
|
|
40
|
-
"smol-toml": "^1.7.0"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "shiply-cli",
|
|
3
|
+
"version": "0.29.0",
|
|
4
|
+
"description": "Publish static sites to shiply.now from the command line — instant web hosting for agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"shiply": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"skill",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.build.json",
|
|
17
|
+
"prepublishOnly": "pnpm build",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"shiply",
|
|
26
|
+
"hosting",
|
|
27
|
+
"static",
|
|
28
|
+
"deploy",
|
|
29
|
+
"agents",
|
|
30
|
+
"publish"
|
|
31
|
+
],
|
|
32
|
+
"homepage": "https://shiply.now",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.15.0",
|
|
35
|
+
"typescript": "^5.8.0",
|
|
36
|
+
"vitest": "^3.1.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"esbuild": "^0.25.12",
|
|
40
|
+
"smol-toml": "^1.7.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/skill/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ entry here, that capability may be missing from your copy — re-install with
|
|
|
6
6
|
(This is the agent-facing capability log; the CLI's own release notes live in
|
|
7
7
|
the npm package changelog.)
|
|
8
8
|
|
|
9
|
+
## 2026-07-14
|
|
10
|
+
|
|
11
|
+
- **New** — `shiply access <slug>` manages site privacy without raw REST:
|
|
12
|
+
no flags prints the current policy (with a stable `ACCESS slug=… mode=…`
|
|
13
|
+
marker line), `--password <pw>` password-protects, `--restricted
|
|
14
|
+
--email a@b.com --domain example.com` makes it invite-only (flags
|
|
15
|
+
repeatable; each set replaces the previous lists), `--public` opens it
|
|
16
|
+
back up. Password/restricted modes need a paid plan. Details in
|
|
17
|
+
references/site-features.md.
|
|
18
|
+
|
|
9
19
|
## 2026-07-08
|
|
10
20
|
|
|
11
21
|
- **Fix** — re-publishing an unchanged large SSR site (e.g. a 171-file
|