shiply-cli 0.12.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db.js +29 -5
- package/dist/functions.js +342 -0
- package/dist/index.js +118 -7
- package/dist/listings.js +169 -0
- package/dist/projects.js +171 -0
- package/dist/sending-domains.js +159 -0
- package/dist/skill.js +5 -1
- package/dist/status-cmd.js +117 -0
- package/package.json +1 -1
- package/skill/SKILL.md +188 -2
package/dist/listings.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
import { loadApiKey } from './config.js';
|
|
3
|
+
const headers = (apiKey) => ({
|
|
4
|
+
'content-type': 'application/json',
|
|
5
|
+
authorization: `Bearer ${apiKey}`,
|
|
6
|
+
});
|
|
7
|
+
function readFlags(argv) {
|
|
8
|
+
const flags = {};
|
|
9
|
+
const rest = [];
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const a = argv[i];
|
|
12
|
+
if (a.startsWith('--')) {
|
|
13
|
+
const eq = a.indexOf('=');
|
|
14
|
+
if (eq >= 0) {
|
|
15
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next && !next.startsWith('--')) {
|
|
20
|
+
flags[a.slice(2)] = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
flags[a.slice(2)] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
rest.push(a);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { rest, flags };
|
|
33
|
+
}
|
|
34
|
+
async function ctxFromEnv(flags) {
|
|
35
|
+
const apiKey = typeof flags.key === 'string' ? flags.key : await loadApiKey();
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
throw new Error('listing commands need an API key — run `shiply login` first');
|
|
38
|
+
}
|
|
39
|
+
const base = typeof flags.base === 'string' ? flags.base : undefined;
|
|
40
|
+
return { base, apiKey };
|
|
41
|
+
}
|
|
42
|
+
function printJson(data) {
|
|
43
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
function money(cents, currency = 'usd') {
|
|
46
|
+
return `${(cents / 100).toFixed(2)} ${currency.toUpperCase()}`;
|
|
47
|
+
}
|
|
48
|
+
async function listAction(ctx, flags) {
|
|
49
|
+
const base = resolveBase(ctx.base);
|
|
50
|
+
// The public REST surface doesn't yet expose GET /api/v1/listings. To list
|
|
51
|
+
// the seller's own listings without inventing an endpoint, defer to the
|
|
52
|
+
// dashboard URL the seller can open. Surface the same hint as the dashboard
|
|
53
|
+
// surfaces; agents wanting machine-readable listings should call the MCP
|
|
54
|
+
// `list_listings` tool.
|
|
55
|
+
if (flags.json) {
|
|
56
|
+
printJson({
|
|
57
|
+
note: 'GET /api/v1/listings is not exposed; use the MCP `list_listings` tool for machine-readable output.',
|
|
58
|
+
dashboardUrl: `${resolveBase(ctx.base)}/dashboard/sales`,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log('No REST listing index endpoint yet — use the MCP tool `list_listings` for machine output.');
|
|
63
|
+
console.log(` dashboard: ${base}/dashboard/sales`);
|
|
64
|
+
// suppress unused-var warning while keeping the auth check active.
|
|
65
|
+
void headers(ctx.apiKey);
|
|
66
|
+
}
|
|
67
|
+
async function createAction(ctx, slug, flags) {
|
|
68
|
+
const priceFlag = flags.price;
|
|
69
|
+
if (typeof priceFlag !== 'string') {
|
|
70
|
+
throw new Error('usage: shiply listing create <site-slug> --price <cents> --jurisdiction "<region>" [--pitch "<text>"] [--terms standard|custom] [--terms-custom "<text>"] [--status draft|live]');
|
|
71
|
+
}
|
|
72
|
+
const priceCents = Number.parseInt(priceFlag, 10);
|
|
73
|
+
if (!Number.isFinite(priceCents) || priceCents < 100 || priceCents > 999_900) {
|
|
74
|
+
throw new Error('--price must be cents between 100 and 999900 (i.e. $1.00 – $9999.00)');
|
|
75
|
+
}
|
|
76
|
+
const jurisdiction = typeof flags.jurisdiction === 'string' ? flags.jurisdiction : undefined;
|
|
77
|
+
if (!jurisdiction || jurisdiction.length < 2) {
|
|
78
|
+
throw new Error('--jurisdiction is required, e.g. --jurisdiction "California, USA"');
|
|
79
|
+
}
|
|
80
|
+
const termsMode = (typeof flags.terms === 'string' ? flags.terms : 'standard');
|
|
81
|
+
if (termsMode !== 'standard' && termsMode !== 'custom') {
|
|
82
|
+
throw new Error('--terms must be "standard" or "custom"');
|
|
83
|
+
}
|
|
84
|
+
const status = typeof flags.status === 'string' ? flags.status : undefined;
|
|
85
|
+
if (status && status !== 'draft' && status !== 'live') {
|
|
86
|
+
throw new Error('--status must be "draft" or "live" on create');
|
|
87
|
+
}
|
|
88
|
+
const body = {
|
|
89
|
+
slug,
|
|
90
|
+
priceCents,
|
|
91
|
+
jurisdiction,
|
|
92
|
+
termsMode,
|
|
93
|
+
};
|
|
94
|
+
if (typeof flags.pitch === 'string')
|
|
95
|
+
body.pitch = flags.pitch;
|
|
96
|
+
if (typeof flags['terms-custom'] === 'string')
|
|
97
|
+
body.termsCustom = flags['terms-custom'];
|
|
98
|
+
if (status)
|
|
99
|
+
body.status = status;
|
|
100
|
+
const base = resolveBase(ctx.base);
|
|
101
|
+
const r = await api(`${base}/api/v1/listings`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: headers(ctx.apiKey),
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
});
|
|
106
|
+
if (flags.json) {
|
|
107
|
+
printJson(r);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log(`✔ listed ${slug} for ${money(r.priceCents, r.currency)} [${r.status}]`);
|
|
111
|
+
console.log(` id: ${r.id}`);
|
|
112
|
+
console.log(` marketplace: ${base}/marketplace/${slug}`);
|
|
113
|
+
console.log(` edit later: shiply listing update ${slug} --price <cents>`);
|
|
114
|
+
}
|
|
115
|
+
async function removeAction(ctx, slug, flags) {
|
|
116
|
+
// The REST surface for unlisting is PATCH /api/v1/listings/{id} with
|
|
117
|
+
// status: "draft". We don't have a slug→id resolver in REST, so the CLI
|
|
118
|
+
// currently requires the listing id via --id. Agents that only have the
|
|
119
|
+
// slug should use the MCP `delete_listing` tool which resolves slug→id
|
|
120
|
+
// server-side.
|
|
121
|
+
const id = typeof flags.id === 'string' ? flags.id : undefined;
|
|
122
|
+
if (!id) {
|
|
123
|
+
throw new Error('usage: shiply listing rm <site-slug> --id <listing-id> (use the MCP `delete_listing` tool to unlist by slug without needing the id)');
|
|
124
|
+
}
|
|
125
|
+
const base = resolveBase(ctx.base);
|
|
126
|
+
const r = await api(`${base}/api/v1/listings/${encodeURIComponent(id)}`, {
|
|
127
|
+
method: 'PATCH',
|
|
128
|
+
headers: headers(ctx.apiKey),
|
|
129
|
+
body: JSON.stringify({ status: 'draft' }),
|
|
130
|
+
});
|
|
131
|
+
if (flags.json) {
|
|
132
|
+
printJson(r);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(`✔ unlisted ${slug} [${r.status}]`);
|
|
136
|
+
}
|
|
137
|
+
export async function runListing(argv) {
|
|
138
|
+
const sub = argv[0];
|
|
139
|
+
const { rest, flags } = readFlags(argv.slice(1));
|
|
140
|
+
if (!sub || sub === 'help') {
|
|
141
|
+
console.log('usage: shiply listing <ls|create|rm> [args] [--json]');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const ctx = await ctxFromEnv(flags);
|
|
145
|
+
switch (sub) {
|
|
146
|
+
case 'ls':
|
|
147
|
+
case 'list':
|
|
148
|
+
await listAction(ctx, flags);
|
|
149
|
+
return;
|
|
150
|
+
case 'create': {
|
|
151
|
+
const slug = rest[0];
|
|
152
|
+
if (!slug) {
|
|
153
|
+
throw new Error('usage: shiply listing create <site-slug> --price <cents> --jurisdiction "<region>"');
|
|
154
|
+
}
|
|
155
|
+
await createAction(ctx, slug, flags);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
case 'rm':
|
|
159
|
+
case 'delete': {
|
|
160
|
+
const slug = rest[0];
|
|
161
|
+
if (!slug)
|
|
162
|
+
throw new Error('usage: shiply listing rm <site-slug> --id <listing-id>');
|
|
163
|
+
await removeAction(ctx, slug, flags);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
throw new Error(`unknown listing subcommand: ${sub} (try ls|create|rm)`);
|
|
168
|
+
}
|
|
169
|
+
}
|
package/dist/projects.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
import { loadApiKey } from './config.js';
|
|
3
|
+
const headers = (apiKey) => ({
|
|
4
|
+
'content-type': 'application/json',
|
|
5
|
+
authorization: `Bearer ${apiKey}`,
|
|
6
|
+
});
|
|
7
|
+
function readFlags(argv) {
|
|
8
|
+
const flags = {};
|
|
9
|
+
const rest = [];
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const a = argv[i];
|
|
12
|
+
if (a.startsWith('--')) {
|
|
13
|
+
const eq = a.indexOf('=');
|
|
14
|
+
if (eq >= 0) {
|
|
15
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next && !next.startsWith('--')) {
|
|
20
|
+
flags[a.slice(2)] = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
flags[a.slice(2)] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
rest.push(a);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { rest, flags };
|
|
33
|
+
}
|
|
34
|
+
async function ctxFromEnv(flags) {
|
|
35
|
+
const apiKey = typeof flags.key === 'string' ? flags.key : await loadApiKey();
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
throw new Error('project commands need an API key — run `shiply login` first');
|
|
38
|
+
}
|
|
39
|
+
const base = typeof flags.base === 'string' ? flags.base : undefined;
|
|
40
|
+
return { base, apiKey };
|
|
41
|
+
}
|
|
42
|
+
function printJson(data) {
|
|
43
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
function shortRow(p) {
|
|
46
|
+
const customer = p.customerEmail ?? p.customerName ?? 'no customer';
|
|
47
|
+
return `${p.id} [${p.status}] ${p.label} (${customer})`;
|
|
48
|
+
}
|
|
49
|
+
async function listAction(ctx, flags) {
|
|
50
|
+
const base = resolveBase(ctx.base);
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (typeof flags.status === 'string')
|
|
53
|
+
params.set('status', flags.status);
|
|
54
|
+
if (typeof flags.q === 'string')
|
|
55
|
+
params.set('q', flags.q);
|
|
56
|
+
const qs = params.toString();
|
|
57
|
+
const url = `${base}/api/v1/projects${qs ? `?${qs}` : ''}`;
|
|
58
|
+
const r = await api(url, { headers: headers(ctx.apiKey) });
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
printJson(r.projects);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (r.projects.length === 0) {
|
|
64
|
+
console.log('No projects yet. Create one: shiply project create <label>');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const p of r.projects)
|
|
68
|
+
console.log(shortRow(p));
|
|
69
|
+
}
|
|
70
|
+
async function createAction(ctx, label, flags) {
|
|
71
|
+
const base = resolveBase(ctx.base);
|
|
72
|
+
const body = { label };
|
|
73
|
+
if (typeof flags['customer-email'] === 'string')
|
|
74
|
+
body.customerEmail = flags['customer-email'];
|
|
75
|
+
if (typeof flags['customer-name'] === 'string')
|
|
76
|
+
body.customerName = flags['customer-name'];
|
|
77
|
+
const r = await api(`${base}/api/v1/projects`, { method: 'POST', headers: headers(ctx.apiKey), body: JSON.stringify(body) });
|
|
78
|
+
if (flags.json) {
|
|
79
|
+
printJson(r);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const appBase = base.replace(/\/+$/, '');
|
|
83
|
+
console.log(`✔ created project ${r.id} [${r.status}] ${r.label}`);
|
|
84
|
+
console.log(` intake URL (share with customer):`);
|
|
85
|
+
console.log(` ${appBase}/intake/${r.intakeToken}`);
|
|
86
|
+
if (r.customerEmail)
|
|
87
|
+
console.log(` intake invite emailed to ${r.customerEmail}`);
|
|
88
|
+
}
|
|
89
|
+
async function getAction(ctx, id, flags) {
|
|
90
|
+
const base = resolveBase(ctx.base);
|
|
91
|
+
const r = await api(`${base}/api/v1/projects/${encodeURIComponent(id)}`, { headers: headers(ctx.apiKey) });
|
|
92
|
+
if (flags.json) {
|
|
93
|
+
printJson(r);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(JSON.stringify(r, null, 2));
|
|
97
|
+
}
|
|
98
|
+
async function archiveAction(ctx, id, flags) {
|
|
99
|
+
const base = resolveBase(ctx.base);
|
|
100
|
+
// Server archives via PATCH /api/v1/projects/{id} with { status: "archived" }.
|
|
101
|
+
// There is no dedicated /archive route — PATCH is the documented surface.
|
|
102
|
+
const body = { status: 'archived' };
|
|
103
|
+
if (typeof flags.reason === 'string')
|
|
104
|
+
body.archivedReason = flags.reason;
|
|
105
|
+
const r = await api(`${base}/api/v1/projects/${encodeURIComponent(id)}`, { method: 'PATCH', headers: headers(ctx.apiKey), body: JSON.stringify(body) });
|
|
106
|
+
if (flags.json) {
|
|
107
|
+
printJson(r);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
console.log(`✔ archived ${r.id} [${r.status}] ${r.label}`);
|
|
111
|
+
console.log(` restore later with: shiply project restore ${r.id}`);
|
|
112
|
+
}
|
|
113
|
+
async function restoreAction(ctx, id, flags) {
|
|
114
|
+
const base = resolveBase(ctx.base);
|
|
115
|
+
const r = await api(`${base}/api/v1/projects/${encodeURIComponent(id)}`, {
|
|
116
|
+
method: 'PATCH',
|
|
117
|
+
headers: headers(ctx.apiKey),
|
|
118
|
+
body: JSON.stringify({ status: 'draft' }),
|
|
119
|
+
});
|
|
120
|
+
if (flags.json) {
|
|
121
|
+
printJson(r);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.log(`✔ restored ${r.id} [${r.status}] ${r.label}`);
|
|
125
|
+
}
|
|
126
|
+
export async function runProject(argv) {
|
|
127
|
+
const sub = argv[0];
|
|
128
|
+
const { rest, flags } = readFlags(argv.slice(1));
|
|
129
|
+
if (!sub || sub === 'help') {
|
|
130
|
+
console.log('usage: shiply project <ls|create|get|archive|restore> [args] [--json]');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const ctx = await ctxFromEnv(flags);
|
|
134
|
+
switch (sub) {
|
|
135
|
+
case 'ls':
|
|
136
|
+
case 'list':
|
|
137
|
+
await listAction(ctx, flags);
|
|
138
|
+
return;
|
|
139
|
+
case 'create': {
|
|
140
|
+
const label = rest[0];
|
|
141
|
+
if (!label) {
|
|
142
|
+
throw new Error('usage: shiply project create <label> [--customer-email <email>] [--customer-name <name>]');
|
|
143
|
+
}
|
|
144
|
+
await createAction(ctx, label, flags);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
case 'get': {
|
|
148
|
+
const id = rest[0];
|
|
149
|
+
if (!id)
|
|
150
|
+
throw new Error('usage: shiply project get <id>');
|
|
151
|
+
await getAction(ctx, id, flags);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
case 'archive': {
|
|
155
|
+
const id = rest[0];
|
|
156
|
+
if (!id)
|
|
157
|
+
throw new Error('usage: shiply project archive <id> [--reason "<text>"]');
|
|
158
|
+
await archiveAction(ctx, id, flags);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
case 'restore': {
|
|
162
|
+
const id = rest[0];
|
|
163
|
+
if (!id)
|
|
164
|
+
throw new Error('usage: shiply project restore <id>');
|
|
165
|
+
await restoreAction(ctx, id, flags);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
throw new Error(`unknown project subcommand: ${sub} (try ls|create|get|archive|restore)`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
import { loadApiKey } from './config.js';
|
|
3
|
+
const headers = (apiKey) => ({
|
|
4
|
+
'content-type': 'application/json',
|
|
5
|
+
authorization: `Bearer ${apiKey}`,
|
|
6
|
+
});
|
|
7
|
+
function readFlags(argv) {
|
|
8
|
+
const flags = {};
|
|
9
|
+
const rest = [];
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const a = argv[i];
|
|
12
|
+
if (a.startsWith('--')) {
|
|
13
|
+
const eq = a.indexOf('=');
|
|
14
|
+
if (eq >= 0) {
|
|
15
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next && !next.startsWith('--')) {
|
|
20
|
+
flags[a.slice(2)] = next;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
flags[a.slice(2)] = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
rest.push(a);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { rest, flags };
|
|
33
|
+
}
|
|
34
|
+
async function ctxFromEnv(flags) {
|
|
35
|
+
const apiKey = typeof flags.key === 'string' ? flags.key : await loadApiKey();
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
throw new Error('sending-domain commands need an API key — run `shiply login` first');
|
|
38
|
+
}
|
|
39
|
+
const base = typeof flags.base === 'string' ? flags.base : undefined;
|
|
40
|
+
return { base, apiKey };
|
|
41
|
+
}
|
|
42
|
+
function printJson(data) {
|
|
43
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
44
|
+
}
|
|
45
|
+
function printDns(d) {
|
|
46
|
+
if (!d.dns || d.dns.length === 0) {
|
|
47
|
+
console.log(' (no DNS records returned — call `shiply sending-domain verify <id>` after Resend provisions records)');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(' Add these DNS records at your registrar:');
|
|
51
|
+
for (const r of d.dns) {
|
|
52
|
+
const status = r.status ? ` [${r.status}]` : '';
|
|
53
|
+
console.log(` ${r.type.padEnd(5)} ${r.name} → ${r.value}${status}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function listAction(ctx, flags) {
|
|
57
|
+
const base = resolveBase(ctx.base);
|
|
58
|
+
const r = await api(`${base}/api/v1/sending-domains`, { headers: headers(ctx.apiKey) });
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
printJson(r.domains);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!r.domains.length) {
|
|
64
|
+
console.log('No sending domains yet. Add one: shiply sending-domain add <domain>');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const d of r.domains) {
|
|
68
|
+
const from = d.fromAddress ? ` from=${d.fromAddress}` : '';
|
|
69
|
+
console.log(`${d.domain} [${d.status}] id=${d.id}${from}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function addAction(ctx, domain, flags) {
|
|
73
|
+
const base = resolveBase(ctx.base);
|
|
74
|
+
const r = await api(`${base}/api/v1/sending-domains`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: headers(ctx.apiKey),
|
|
77
|
+
body: JSON.stringify({ domain }),
|
|
78
|
+
});
|
|
79
|
+
if (flags.json) {
|
|
80
|
+
printJson(r);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(`✔ ${r.domain} registered [${r.status}] id=${r.id}`);
|
|
84
|
+
printDns(r);
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(` After DNS propagates: shiply sending-domain verify ${r.id}`);
|
|
87
|
+
}
|
|
88
|
+
async function verifyAction(ctx, id, flags) {
|
|
89
|
+
const base = resolveBase(ctx.base);
|
|
90
|
+
const r = await api(`${base}/api/v1/sending-domains/${encodeURIComponent(id)}/verify`, { method: 'POST', headers: headers(ctx.apiKey), body: '{}' });
|
|
91
|
+
if (flags.json) {
|
|
92
|
+
printJson(r);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(`${r.domain} [${r.status}]`);
|
|
96
|
+
if (r.status === 'verified') {
|
|
97
|
+
console.log(' ✔ ready — shiply will send your demand-test emails from this domain');
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
printDns(r);
|
|
101
|
+
console.log(' Re-run `shiply sending-domain verify ' + r.id + '` after DNS updates propagate.');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function removeAction(ctx, id, flags) {
|
|
105
|
+
if (!flags.yes) {
|
|
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
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const base = resolveBase(ctx.base);
|
|
110
|
+
await api(`${base}/api/v1/sending-domains/${encodeURIComponent(id)}`, {
|
|
111
|
+
method: 'DELETE',
|
|
112
|
+
headers: headers(ctx.apiKey),
|
|
113
|
+
});
|
|
114
|
+
if (flags.json) {
|
|
115
|
+
printJson({ removed: id });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
console.log(`✔ removed sending domain ${id}`);
|
|
119
|
+
}
|
|
120
|
+
export async function runSendingDomain(argv) {
|
|
121
|
+
const sub = argv[0];
|
|
122
|
+
const { rest, flags } = readFlags(argv.slice(1));
|
|
123
|
+
if (!sub || sub === 'help') {
|
|
124
|
+
console.log('usage: shiply sending-domain <ls|add|verify|rm> [args] [--json]');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const ctx = await ctxFromEnv(flags);
|
|
128
|
+
switch (sub) {
|
|
129
|
+
case 'ls':
|
|
130
|
+
case 'list':
|
|
131
|
+
await listAction(ctx, flags);
|
|
132
|
+
return;
|
|
133
|
+
case 'add': {
|
|
134
|
+
const domain = rest[0];
|
|
135
|
+
if (!domain)
|
|
136
|
+
throw new Error('usage: shiply sending-domain add <domain>');
|
|
137
|
+
await addAction(ctx, domain, flags);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
case 'verify': {
|
|
141
|
+
const id = rest[0];
|
|
142
|
+
if (!id)
|
|
143
|
+
throw new Error('usage: shiply sending-domain verify <id>');
|
|
144
|
+
await verifyAction(ctx, id, flags);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
case 'rm':
|
|
148
|
+
case 'remove':
|
|
149
|
+
case 'delete': {
|
|
150
|
+
const id = rest[0];
|
|
151
|
+
if (!id)
|
|
152
|
+
throw new Error('usage: shiply sending-domain rm <id> --yes');
|
|
153
|
+
await removeAction(ctx, id, flags);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
default:
|
|
157
|
+
throw new Error(`unknown sending-domain subcommand: ${sub} (try ls|add|verify|rm)`);
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/skill.js
CHANGED
|
@@ -16,12 +16,16 @@ export function installTargets(project) {
|
|
|
16
16
|
}
|
|
17
17
|
return [{ agent: 'Claude Code (all projects)', dir: join(homedir(), '.claude', 'skills', 'shiply') }];
|
|
18
18
|
}
|
|
19
|
-
export async function installSkill(project) {
|
|
19
|
+
export async function installSkill(project, force = false) {
|
|
20
20
|
const content = await bundledSkill();
|
|
21
21
|
const written = [];
|
|
22
22
|
for (const t of installTargets(project)) {
|
|
23
23
|
await mkdir(t.dir, { recursive: true });
|
|
24
24
|
const file = join(t.dir, 'SKILL.md');
|
|
25
|
+
// writeFile always overwrites; --force is an explicit signal of intent
|
|
26
|
+
// (and matches what users type from the AGENT_PROMPT). Without --force we
|
|
27
|
+
// still overwrite — but a future version may add a sanity check.
|
|
28
|
+
void force;
|
|
25
29
|
await writeFile(file, content);
|
|
26
30
|
written.push(`${t.agent}: ${file}`);
|
|
27
31
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { loadApiKey } from './config.js';
|
|
2
|
+
import { api, resolveBase } from './publish.js';
|
|
3
|
+
// Minimal ANSI helpers — match the rest of the CLI's "spare colors, never
|
|
4
|
+
// rainbow" tone (see drive/db output).
|
|
5
|
+
const isTty = process.stdout.isTTY;
|
|
6
|
+
const ansi = {
|
|
7
|
+
green: (s) => (isTty ? `\x1b[32m${s}\x1b[0m` : s),
|
|
8
|
+
red: (s) => (isTty ? `\x1b[31m${s}\x1b[0m` : s),
|
|
9
|
+
dim: (s) => (isTty ? `\x1b[2m${s}\x1b[0m` : s),
|
|
10
|
+
bold: (s) => (isTty ? `\x1b[1m${s}\x1b[0m` : s),
|
|
11
|
+
};
|
|
12
|
+
function fmtLimit(n) {
|
|
13
|
+
if (n === null || n === undefined)
|
|
14
|
+
return 'unlimited';
|
|
15
|
+
return String(n);
|
|
16
|
+
}
|
|
17
|
+
/** Turn `databases_neon_postgres` → `Neon Postgres`-ish: replace _ with space,
|
|
18
|
+
* capitalise words, leave acronyms (D1, MCP, URL) upper. Keep it simple. */
|
|
19
|
+
function humanise(key) {
|
|
20
|
+
const acronyms = new Set(['d1', 'mcp', 'url', 'api']);
|
|
21
|
+
return key
|
|
22
|
+
.split('_')
|
|
23
|
+
.map((w) => (acronyms.has(w) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)))
|
|
24
|
+
.join(' ');
|
|
25
|
+
}
|
|
26
|
+
export async function runStatus(opts) {
|
|
27
|
+
const apiKey = opts.key ?? (await loadApiKey());
|
|
28
|
+
if (!apiKey) {
|
|
29
|
+
console.error('✖ shiply status needs an API key — run `shiply login` first');
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const base = resolveBase(opts.base);
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = await api(`${base}/api/v1/status`, {
|
|
37
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// Surface 404 cleanly while the sibling /status endpoint rolls out — old
|
|
42
|
+
// server versions will respond with "HTTP 404" and the user just sees a
|
|
43
|
+
// friendly hint instead of a stack-style error.
|
|
44
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
45
|
+
if (/404|not.found/i.test(msg)) {
|
|
46
|
+
console.error('✖ this shiply server does not expose /api/v1/status yet');
|
|
47
|
+
console.error(' upgrade your CLI or check https://shiply.now');
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
throw e;
|
|
52
|
+
}
|
|
53
|
+
if (opts.json) {
|
|
54
|
+
process.stdout.write(`${JSON.stringify(raw, null, 2)}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.log(ansi.bold('shiply status'));
|
|
58
|
+
console.log('');
|
|
59
|
+
// Plan + identity
|
|
60
|
+
if (raw.plan) {
|
|
61
|
+
const sub = raw.plan.subscription_status ? ` (${raw.plan.subscription_status})` : '';
|
|
62
|
+
console.log(`Plan: ${raw.plan.name}${sub}`);
|
|
63
|
+
}
|
|
64
|
+
if (raw.user?.email)
|
|
65
|
+
console.log(`Email: ${raw.user.email}`);
|
|
66
|
+
if (raw.user)
|
|
67
|
+
console.log(`Handle: ${raw.user.handle ?? '(none)'}`);
|
|
68
|
+
console.log('');
|
|
69
|
+
// Capabilities — iterate in server order; tolerate unknown keys.
|
|
70
|
+
if (raw.capabilities) {
|
|
71
|
+
console.log(ansi.bold('Capabilities:'));
|
|
72
|
+
const entries = Object.entries(raw.capabilities);
|
|
73
|
+
const labels = entries.map(([k]) => humanise(k));
|
|
74
|
+
const labelWidth = Math.max(...labels.map((l) => l.length));
|
|
75
|
+
for (let i = 0; i < entries.length; i++) {
|
|
76
|
+
const [, cap] = entries[i];
|
|
77
|
+
const mark = cap.available ? ansi.green('✓') : ansi.red('✗');
|
|
78
|
+
const label = labels[i].padEnd(labelWidth, ' ');
|
|
79
|
+
const hint = [];
|
|
80
|
+
if (cap.plan_required)
|
|
81
|
+
hint.push(`${cap.plan_required}+`);
|
|
82
|
+
if (cap.limit !== undefined && cap.limit !== null)
|
|
83
|
+
hint.push(`limit: ${cap.limit}`);
|
|
84
|
+
if (cap.note)
|
|
85
|
+
hint.push(cap.note);
|
|
86
|
+
const tail = hint.length > 0 ? ansi.dim(` — ${hint.join(' · ')}`) : '';
|
|
87
|
+
console.log(` ${mark} ${label}${tail}`);
|
|
88
|
+
}
|
|
89
|
+
console.log('');
|
|
90
|
+
}
|
|
91
|
+
// Limits
|
|
92
|
+
if (raw.limits) {
|
|
93
|
+
console.log(ansi.bold('Limits:'));
|
|
94
|
+
for (const [k, v] of Object.entries(raw.limits)) {
|
|
95
|
+
console.log(` ${humanise(k)}: ${fmtLimit(v)}`);
|
|
96
|
+
}
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
99
|
+
// Blocked-features summary + upgrade hint
|
|
100
|
+
const blocked = raw.blocked_features ?? [];
|
|
101
|
+
if (blocked.length > 0) {
|
|
102
|
+
console.log(`${blocked.length} feature${blocked.length === 1 ? '' : 's'} need${blocked.length === 1 ? 's' : ''} an upgrade:`);
|
|
103
|
+
const labels = blocked.map(humanise);
|
|
104
|
+
const labelWidth = Math.max(...labels.map((l) => l.length));
|
|
105
|
+
for (let i = 0; i < blocked.length; i++) {
|
|
106
|
+
const key = blocked[i];
|
|
107
|
+
const req = raw.capabilities?.[key]?.plan_required ?? 'higher plan';
|
|
108
|
+
console.log(` ${ansi.red('✗')} ${labels[i].padEnd(labelWidth, ' ')} ${ansi.dim(`— requires ${req}`)}`);
|
|
109
|
+
}
|
|
110
|
+
console.log('');
|
|
111
|
+
if (raw.plan?.upgrade_url)
|
|
112
|
+
console.log(`Upgrade at: ${raw.plan.upgrade_url}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log('Nothing blocked. You have full access.');
|
|
116
|
+
}
|
|
117
|
+
}
|