shiply-cli 0.24.0 → 0.25.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/connect.js +189 -0
- package/dist/data.js +17 -3
- package/dist/index.js +78 -5
- package/dist/skill.js +16 -13
- package/package.json +42 -42
- package/skill/CHANGELOG.md +90 -0
- package/skill/SKILL.md +78 -664
- package/skill/references/client-work.md +159 -0
- package/skill/references/custom-domains.md +87 -0
- package/skill/references/databases.md +108 -0
- package/skill/references/email.md +147 -0
- package/skill/references/functions.md +100 -0
- package/skill/references/publishing.md +103 -0
- package/skill/references/site-features.md +110 -0
- package/skill/references/ssr-frameworks.md +47 -0
package/dist/connect.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
6
|
+
/** The public MCP server package. `npx shiply-mcp` boots a local stdio server
|
|
7
|
+
* that talks to shiply's REST API: anonymous publish works with no key; a
|
|
8
|
+
* saved SHIPLY_API_KEY (shp_…) unlocks owned site/db/domain ops. */
|
|
9
|
+
export const MCP_SERVER_NAME = 'shiply';
|
|
10
|
+
export const MCP_COMMAND = 'npx';
|
|
11
|
+
/** `-y` so the first run installs shiply-mcp without an interactive prompt —
|
|
12
|
+
* the agent spawns this non-interactively. */
|
|
13
|
+
export const MCP_ARGS = ['-y', 'shiply-mcp'];
|
|
14
|
+
/** Where each agent keeps its (global) MCP config. Kept in one place so the
|
|
15
|
+
* detector and the writer never drift. Project-local variants (./.cursor,
|
|
16
|
+
* ./.mcp.json) are intentionally out of scope — `connect` wires the agent
|
|
17
|
+
* once for the whole machine, matching `shiply skill`'s global default. */
|
|
18
|
+
export function knownTargets(home = homedir()) {
|
|
19
|
+
const claudeFile = join(home, '.claude.json');
|
|
20
|
+
const cursorFile = join(home, '.cursor', 'mcp.json');
|
|
21
|
+
const codexFile = join(home, '.codex', 'config.toml');
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
id: 'claude',
|
|
25
|
+
label: 'Claude Code',
|
|
26
|
+
file: claudeFile,
|
|
27
|
+
format: 'claude-json',
|
|
28
|
+
// ~/.claude.json is created on first Claude Code run; ~/.claude/ is the
|
|
29
|
+
// adjacent skills/data dir. Either presence means "Claude Code is here".
|
|
30
|
+
detected: existsSync(claudeFile) || existsSync(join(home, '.claude')),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'cursor',
|
|
34
|
+
label: 'Cursor',
|
|
35
|
+
file: cursorFile,
|
|
36
|
+
format: 'mcp-json',
|
|
37
|
+
detected: existsSync(cursorFile) || existsSync(join(home, '.cursor')),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'codex',
|
|
41
|
+
label: 'Codex',
|
|
42
|
+
file: codexFile,
|
|
43
|
+
format: 'codex-toml',
|
|
44
|
+
detected: existsSync(codexFile) || existsSync(join(home, '.codex')),
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
/** Decide which targets to act on given the flags. Exported for testing. */
|
|
49
|
+
export function selectTargets(targets, opts) {
|
|
50
|
+
let chosen = targets;
|
|
51
|
+
if (opts.only && opts.only.length > 0) {
|
|
52
|
+
const want = new Set(opts.only.map((s) => s.trim().toLowerCase()));
|
|
53
|
+
chosen = chosen.filter((t) => want.has(t.id));
|
|
54
|
+
}
|
|
55
|
+
else if (!opts.all) {
|
|
56
|
+
chosen = chosen.filter((t) => t.detected);
|
|
57
|
+
}
|
|
58
|
+
return chosen;
|
|
59
|
+
}
|
|
60
|
+
/** The server entry we want present, in the JSON agents' shape. */
|
|
61
|
+
function desiredJsonEntry() {
|
|
62
|
+
return { command: MCP_COMMAND, args: [...MCP_ARGS] };
|
|
63
|
+
}
|
|
64
|
+
/** Shallow structural equality for a server entry we previously wrote. Only
|
|
65
|
+
* compares the fields we manage (command + args) so a user who hand-added
|
|
66
|
+
* `env` on top of our entry isn't treated as "different" and re-clobbered. */
|
|
67
|
+
function entryMatches(existing) {
|
|
68
|
+
if (!existing || typeof existing !== 'object')
|
|
69
|
+
return false;
|
|
70
|
+
const e = existing;
|
|
71
|
+
if (e.command !== MCP_COMMAND)
|
|
72
|
+
return false;
|
|
73
|
+
if (!Array.isArray(e.args))
|
|
74
|
+
return false;
|
|
75
|
+
return e.args.length === MCP_ARGS.length && e.args.every((a, i) => a === MCP_ARGS[i]);
|
|
76
|
+
}
|
|
77
|
+
/** Read a file if present; undefined if missing. Any other error propagates. */
|
|
78
|
+
async function readIfExists(file) {
|
|
79
|
+
try {
|
|
80
|
+
return await readFile(file, 'utf8');
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
if (e.code === 'ENOENT')
|
|
84
|
+
return undefined;
|
|
85
|
+
throw e;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Compute + optionally apply the change for a single JSON-shaped agent
|
|
89
|
+
* (Claude Code / Cursor). Both use `{ mcpServers: { <name>: entry } }`. */
|
|
90
|
+
async function applyJsonTarget(t, opts) {
|
|
91
|
+
const raw = await readIfExists(t.file);
|
|
92
|
+
let doc = {};
|
|
93
|
+
if (raw && raw.trim()) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
97
|
+
doc = parsed;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return { ...base(t), action: 'error', detail: 'config is not a JSON object — left untouched' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { ...base(t), action: 'error', detail: 'config is not valid JSON — left untouched' };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const servers = doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
|
|
108
|
+
? doc.mcpServers
|
|
109
|
+
: undefined;
|
|
110
|
+
const existing = servers?.[MCP_SERVER_NAME];
|
|
111
|
+
if (existing !== undefined) {
|
|
112
|
+
if (entryMatches(existing))
|
|
113
|
+
return { ...base(t), action: 'unchanged', detail: 'already configured' };
|
|
114
|
+
if (!opts.force)
|
|
115
|
+
return {
|
|
116
|
+
...base(t),
|
|
117
|
+
action: 'skipped-exists',
|
|
118
|
+
detail: 'a different `shiply` MCP entry exists — re-run with --force to overwrite',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const nextServers = { ...(servers ?? {}), [MCP_SERVER_NAME]: desiredJsonEntry() };
|
|
122
|
+
const nextDoc = { ...doc, mcpServers: nextServers };
|
|
123
|
+
const action = existing !== undefined ? 'updated' : 'added';
|
|
124
|
+
if (!opts.dryRun) {
|
|
125
|
+
await mkdir(dirname(t.file), { recursive: true });
|
|
126
|
+
// Preserve 2-space indentation like the rest of the CLI's JSON writers.
|
|
127
|
+
await writeFile(t.file, `${JSON.stringify(nextDoc, null, 2)}\n`);
|
|
128
|
+
}
|
|
129
|
+
return { ...base(t), action };
|
|
130
|
+
}
|
|
131
|
+
/** Codex reads TOML: `[mcp_servers.shiply]\ncommand=…\nargs=[…]`. We parse the
|
|
132
|
+
* whole doc, merge our table, and re-stringify — smol-toml round-trips the
|
|
133
|
+
* rest of the user's config unharmed. */
|
|
134
|
+
async function applyCodexTarget(t, opts) {
|
|
135
|
+
const raw = await readIfExists(t.file);
|
|
136
|
+
let doc = {};
|
|
137
|
+
if (raw && raw.trim()) {
|
|
138
|
+
try {
|
|
139
|
+
doc = parseToml(raw);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { ...base(t), action: 'error', detail: 'config is not valid TOML — left untouched' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const servers = doc.mcp_servers && typeof doc.mcp_servers === 'object' && !Array.isArray(doc.mcp_servers)
|
|
146
|
+
? doc.mcp_servers
|
|
147
|
+
: undefined;
|
|
148
|
+
const existing = servers?.[MCP_SERVER_NAME];
|
|
149
|
+
if (existing !== undefined) {
|
|
150
|
+
if (entryMatches(existing))
|
|
151
|
+
return { ...base(t), action: 'unchanged', detail: 'already configured' };
|
|
152
|
+
if (!opts.force)
|
|
153
|
+
return {
|
|
154
|
+
...base(t),
|
|
155
|
+
action: 'skipped-exists',
|
|
156
|
+
detail: 'a different `shiply` MCP entry exists — re-run with --force to overwrite',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const nextServers = { ...(servers ?? {}), [MCP_SERVER_NAME]: desiredJsonEntry() };
|
|
160
|
+
const nextDoc = { ...doc, mcp_servers: nextServers };
|
|
161
|
+
const action = existing !== undefined ? 'updated' : 'added';
|
|
162
|
+
if (!opts.dryRun) {
|
|
163
|
+
await mkdir(dirname(t.file), { recursive: true });
|
|
164
|
+
await writeFile(t.file, stringifyToml(nextDoc));
|
|
165
|
+
}
|
|
166
|
+
return { ...base(t), action };
|
|
167
|
+
}
|
|
168
|
+
function base(t) {
|
|
169
|
+
return { id: t.id, label: t.label, file: t.file };
|
|
170
|
+
}
|
|
171
|
+
async function applyTarget(t, opts) {
|
|
172
|
+
try {
|
|
173
|
+
return t.format === 'codex-toml' ? await applyCodexTarget(t, opts) : await applyJsonTarget(t, opts);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
return { ...base(t), action: 'error', detail: e.message };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Install the shiply MCP entry into every selected agent config. Idempotent:
|
|
180
|
+
* an identical entry is left as-is; a foreign `shiply` entry is preserved
|
|
181
|
+
* unless --force. Returns one result per selected target (empty if none). */
|
|
182
|
+
export async function runConnect(opts = {}) {
|
|
183
|
+
const targets = knownTargets(opts.home);
|
|
184
|
+
const selected = selectTargets(targets, opts);
|
|
185
|
+
const results = [];
|
|
186
|
+
for (const t of selected)
|
|
187
|
+
results.push(await applyTarget(t, opts));
|
|
188
|
+
return { results, targets };
|
|
189
|
+
}
|
package/dist/data.js
CHANGED
|
@@ -49,10 +49,24 @@ export async function dataQuery(ctx, slug, collection, opts) {
|
|
|
49
49
|
console.log(`\n more — re-run with --cursor=${page.nextCursor}`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
export async function dataInsert(ctx, slug, collection, body, sitesDomain
|
|
52
|
+
export async function dataInsert(ctx, slug, collection, body, sitesDomain) {
|
|
53
53
|
// Inserts hit the PUBLIC visitor endpoint at the live site host. We don't
|
|
54
54
|
// send the Bearer key here — manifest's `access.insert` decides.
|
|
55
|
-
|
|
55
|
+
//
|
|
56
|
+
// The site host is NOT the API base, but it lives on the same domain as it:
|
|
57
|
+
// for the default base https://shiply.now the site host is
|
|
58
|
+
// <slug>.shiply.now. Derive scheme + host from the resolved base so that
|
|
59
|
+
// --base / SHIPLY_BASE_URL (staging, self-host, local) is honored, matching
|
|
60
|
+
// every other data subcommand. An explicit sitesDomain arg still wins.
|
|
61
|
+
let origin;
|
|
62
|
+
if (sitesDomain) {
|
|
63
|
+
origin = `https://${slug}.${sitesDomain}`;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const baseUrl = new URL(resolveBase(ctx.base));
|
|
67
|
+
origin = `${baseUrl.protocol}//${slug}.${baseUrl.host}`;
|
|
68
|
+
}
|
|
69
|
+
const url = `${origin}/.shiply/data/${encodeURIComponent(collection)}`;
|
|
56
70
|
const res = await fetch(url, {
|
|
57
71
|
method: 'POST',
|
|
58
72
|
headers: { 'content-type': 'application/json' },
|
|
@@ -60,7 +74,7 @@ export async function dataInsert(ctx, slug, collection, body, sitesDomain = 'shi
|
|
|
60
74
|
});
|
|
61
75
|
const json = (await res.json().catch(() => ({})));
|
|
62
76
|
if (!res.ok)
|
|
63
|
-
throw new Error(json.message ?? `HTTP ${res.status}`);
|
|
77
|
+
throw new Error(json.error?.message ?? json.message ?? `HTTP ${res.status}`);
|
|
64
78
|
console.log(`✔ inserted ${json.id ?? ''} at ${json.createdAt ?? ''}`);
|
|
65
79
|
}
|
|
66
80
|
export async function dataExport(ctx, slug, collection, opts) {
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { loadApiKey, saveApiKey } from './config.js';
|
|
|
27
27
|
import { loginViaDeviceFlow } from './login.js';
|
|
28
28
|
import { api, ApiError, DEFAULT_BASE, publish, resolveBase } from './publish.js';
|
|
29
29
|
import { installSkill } from './skill.js';
|
|
30
|
+
import { runConnect } from './connect.js';
|
|
30
31
|
import { readState, writeState } from './state.js';
|
|
31
32
|
import { readPreview, writePreview } from './previews.js';
|
|
32
33
|
import { checkReadiness, targetToHostname } from './status.js';
|
|
@@ -140,6 +141,9 @@ Usage:
|
|
|
140
141
|
shiply skill [--project] [--force] Install the shiply skill for your AI agent
|
|
141
142
|
(global ~/.claude/skills, or ./.claude/skills with --project)
|
|
142
143
|
(--force overwrites an existing skill — recommended weekly)
|
|
144
|
+
shiply connect [--all] [--only <ids>] Wire the shiply MCP server into your agent(s)
|
|
145
|
+
(auto-detects Claude Code, Cursor, Codex; idempotent)
|
|
146
|
+
(--all writes even undetected agents; --dry-run previews)
|
|
143
147
|
shiply login [--name <label>] Open a browser, click Allow → key saved
|
|
144
148
|
(--email <addr> falls back to legacy 6-digit code)
|
|
145
149
|
shiply help
|
|
@@ -203,11 +207,13 @@ async function main() {
|
|
|
203
207
|
// token so the parser doesn't complain about a missing string value.
|
|
204
208
|
const rawArgv = process.argv.slice(2);
|
|
205
209
|
// `--json` is a STRING option (data-insert body). For commands where it's a
|
|
206
|
-
// boolean flag (status/publish/update),
|
|
207
|
-
//
|
|
210
|
+
// boolean flag (status/publish/update/logs), `--json` NEVER takes a value, so
|
|
211
|
+
// treat it as a bare flag regardless of what follows and strip it before
|
|
212
|
+
// parseArgs sees it. (A prior lookahead that cancelled the flag when a
|
|
213
|
+
// positional followed made `shiply publish --json ./dist` swallow the dir as
|
|
214
|
+
// the --json value, leaving no <dir>.)
|
|
208
215
|
const BARE_JSON_CMDS = new Set(['status', 'publish', 'update', 'logs']);
|
|
209
|
-
const bareJsonFlag = BARE_JSON_CMDS.has(rawArgv[0]) && rawArgv.includes('--json')
|
|
210
|
-
!rawArgv.some((a, i) => a === '--json' && typeof rawArgv[i + 1] === 'string' && !rawArgv[i + 1].startsWith('-'));
|
|
216
|
+
const bareJsonFlag = BARE_JSON_CMDS.has(rawArgv[0]) && rawArgv.includes('--json');
|
|
211
217
|
if (bareJsonFlag) {
|
|
212
218
|
const idx = rawArgv.indexOf('--json');
|
|
213
219
|
rawArgv.splice(idx, 1);
|
|
@@ -231,6 +237,9 @@ async function main() {
|
|
|
231
237
|
'new-site': { type: 'boolean' },
|
|
232
238
|
project: { type: 'boolean' },
|
|
233
239
|
force: { type: 'boolean' },
|
|
240
|
+
all: { type: 'boolean' },
|
|
241
|
+
only: { type: 'string' },
|
|
242
|
+
'dry-run': { type: 'boolean' },
|
|
234
243
|
timeout: { type: 'string' },
|
|
235
244
|
out: { type: 'string', short: 'o' },
|
|
236
245
|
site: { type: 'string' },
|
|
@@ -477,7 +486,7 @@ async function main() {
|
|
|
477
486
|
if (!apiKey)
|
|
478
487
|
throw new Error('duplicate needs an API key — run `shiply login` first');
|
|
479
488
|
const base = resolveBase(values.base);
|
|
480
|
-
const out = await api(`${base}/api/v1/publishes/${dir}/duplicate`, {
|
|
489
|
+
const out = await api(`${base}/api/v1/publishes/${encodeURIComponent(dir)}/duplicate`, {
|
|
481
490
|
method: 'POST',
|
|
482
491
|
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
483
492
|
body: JSON.stringify({}),
|
|
@@ -916,6 +925,70 @@ async function main() {
|
|
|
916
925
|
console.log('\nThe skill instructs your agent how to publish, manage sites, query databases, run demand tests, drive customer-intake projects, list/sell sites in the marketplace, manage inbox + sending domains, and more.');
|
|
917
926
|
break;
|
|
918
927
|
}
|
|
928
|
+
case 'connect': {
|
|
929
|
+
const only = values.only
|
|
930
|
+
? String(values.only)
|
|
931
|
+
.split(',')
|
|
932
|
+
.map((s) => s.trim())
|
|
933
|
+
.filter(Boolean)
|
|
934
|
+
: undefined;
|
|
935
|
+
const dryRun = Boolean(values['dry-run']);
|
|
936
|
+
const { results, targets } = await runConnect({
|
|
937
|
+
all: Boolean(values.all),
|
|
938
|
+
only,
|
|
939
|
+
force: Boolean(values.force),
|
|
940
|
+
dryRun,
|
|
941
|
+
});
|
|
942
|
+
if (results.length === 0) {
|
|
943
|
+
// Nothing selected: either no agents detected, or --only matched none.
|
|
944
|
+
if (only && only.length > 0) {
|
|
945
|
+
console.error(`✖ no known agent matched --only ${only.join(',')}`);
|
|
946
|
+
console.error(` known ids: ${targets.map((t) => t.id).join(', ')}`);
|
|
947
|
+
process.exit(2);
|
|
948
|
+
}
|
|
949
|
+
console.log('No agent config detected (looked for Claude Code, Cursor, Codex).');
|
|
950
|
+
console.log(' Re-run with --all to write configs for all of them anyway,');
|
|
951
|
+
console.log(' or --only claude,cursor,codex to target specific ones.');
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
let wrote = false;
|
|
955
|
+
let blocked = false;
|
|
956
|
+
for (const r of results) {
|
|
957
|
+
switch (r.action) {
|
|
958
|
+
case 'added':
|
|
959
|
+
case 'updated':
|
|
960
|
+
wrote = true;
|
|
961
|
+
console.log(`✔ ${r.label} — shiply MCP ${dryRun ? 'would be ' + r.action : r.action} (${r.file})`);
|
|
962
|
+
break;
|
|
963
|
+
case 'unchanged':
|
|
964
|
+
console.log(`• ${r.label} — already configured (${r.file})`);
|
|
965
|
+
break;
|
|
966
|
+
case 'skipped-exists':
|
|
967
|
+
blocked = true;
|
|
968
|
+
console.log(`• ${r.label} — ${r.detail} (${r.file})`);
|
|
969
|
+
break;
|
|
970
|
+
case 'error':
|
|
971
|
+
blocked = true;
|
|
972
|
+
console.error(`✖ ${r.label} — ${r.detail} (${r.file})`);
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (wrote && !dryRun) {
|
|
977
|
+
console.log('');
|
|
978
|
+
console.log('The shiply MCP is now wired in. Next steps:');
|
|
979
|
+
console.log(' 1. Restart your agent (or reload its MCP servers) to pick up the change.');
|
|
980
|
+
console.log(' 2. Ask it to "publish this to shiply" — it can now deploy, check status,');
|
|
981
|
+
console.log(' roll back, and manage custom domains directly.');
|
|
982
|
+
console.log(' 3. Optional: run `shiply login` so owned-site tools work (anonymous publish');
|
|
983
|
+
console.log(' already works with no key).');
|
|
984
|
+
}
|
|
985
|
+
else if (dryRun && wrote) {
|
|
986
|
+
console.log('\n(dry run — nothing was written; re-run without --dry-run to apply)');
|
|
987
|
+
}
|
|
988
|
+
if (blocked)
|
|
989
|
+
process.exitCode = 1;
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
919
992
|
case 'login': {
|
|
920
993
|
const base = resolveBase(values.base);
|
|
921
994
|
// Default path: device flow. One click in the browser, no email round-trip.
|
package/dist/skill.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { mkdir, readFile
|
|
1
|
+
import { cp, mkdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
/** The
|
|
6
|
-
|
|
5
|
+
/** The skill bundle ships inside the npm package (skill/ next to dist/):
|
|
6
|
+
* SKILL.md (core) + references/*.md (per-topic) + CHANGELOG.md. */
|
|
7
|
+
function bundledSkillDir() {
|
|
7
8
|
const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/dist
|
|
8
|
-
return
|
|
9
|
+
return join(here, '..', 'skill');
|
|
10
|
+
}
|
|
11
|
+
export async function bundledSkill() {
|
|
12
|
+
return readFile(join(bundledSkillDir(), 'SKILL.md'), 'utf8');
|
|
9
13
|
}
|
|
10
14
|
/** Where agents look for skills. Claude Code is the reference layout
|
|
11
|
-
* (~/.claude/skills/<name>/SKILL.md
|
|
12
|
-
* format from their own folders. */
|
|
15
|
+
* (~/.claude/skills/<name>/SKILL.md + sibling reference files); several
|
|
16
|
+
* other agents read the same format from their own folders. */
|
|
13
17
|
export function installTargets(project) {
|
|
14
18
|
if (project) {
|
|
15
19
|
return [{ agent: 'this project (Claude Code & compatible)', dir: join(process.cwd(), '.claude', 'skills', 'shiply') }];
|
|
@@ -17,17 +21,16 @@ export function installTargets(project) {
|
|
|
17
21
|
return [{ agent: 'Claude Code (all projects)', dir: join(homedir(), '.claude', 'skills', 'shiply') }];
|
|
18
22
|
}
|
|
19
23
|
export async function installSkill(project, force = false) {
|
|
20
|
-
const
|
|
24
|
+
const src = bundledSkillDir();
|
|
21
25
|
const written = [];
|
|
22
26
|
for (const t of installTargets(project)) {
|
|
23
27
|
await mkdir(t.dir, { recursive: true });
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// still overwrite — but a future version may add a sanity check.
|
|
28
|
+
// cp always overwrites; --force is an explicit signal of intent (and
|
|
29
|
+
// matches what users type from the AGENT_PROMPT). A stale references/
|
|
30
|
+
// file that no longer ships is harmless — SKILL.md only links current ones.
|
|
28
31
|
void force;
|
|
29
|
-
await
|
|
30
|
-
written.push(`${t.agent}: ${
|
|
32
|
+
await cp(src, t.dir, { recursive: true });
|
|
33
|
+
written.push(`${t.agent}: ${join(t.dir, 'SKILL.md')} (+ references/ + CHANGELOG.md)`);
|
|
31
34
|
}
|
|
32
35
|
return written;
|
|
33
36
|
}
|
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.25.1",
|
|
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
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# shiply — what's new for agents
|
|
2
|
+
|
|
3
|
+
Newest first. If your installed skill's "last updated" date is older than an
|
|
4
|
+
entry here, that capability may be missing from your copy — re-install with
|
|
5
|
+
`npx -y shiply-cli@latest skill --force` or read https://shiply.now/skill.md.
|
|
6
|
+
(This is the agent-facing capability log; the CLI's own release notes live in
|
|
7
|
+
the npm package changelog.)
|
|
8
|
+
|
|
9
|
+
## 2026-07-04
|
|
10
|
+
|
|
11
|
+
- **Update** — the site-relative endpoints (`POST /.shiply/data/<collection>`,
|
|
12
|
+
`POST /.shiply/email`) now return the same nested error envelope as
|
|
13
|
+
`/api/v1/*`: `{"error":{"code","message"}}` (previously a flat
|
|
14
|
+
`{"error":"<code>","message":"..."}`). A top-level `message` mirror is kept
|
|
15
|
+
for older inline JS — read `body.error.code` / `body.error.message` going
|
|
16
|
+
forward. Codes `data_requires_account` / `email_requires_account` (403 on
|
|
17
|
+
unclaimed sites) are now in the documented error list.
|
|
18
|
+
- **Update** — the skill was restructured for progressive disclosure: a slim
|
|
19
|
+
core `SKILL.md` (publish → authorize → update → claim → verify) plus
|
|
20
|
+
per-topic files under `references/` (publishing, ssr-frameworks,
|
|
21
|
+
custom-domains, databases, functions, email, site-features, client-work).
|
|
22
|
+
`shiply skill` installs the whole bundle. Hosted copies:
|
|
23
|
+
`https://shiply.now/skill/references/<file>`. `https://shiply.now/llms.txt`
|
|
24
|
+
is now an index; the single-file everything version moved to
|
|
25
|
+
`https://shiply.now/llms-full.txt`. This log is served at
|
|
26
|
+
`https://shiply.now/changelog.md`.
|
|
27
|
+
|
|
28
|
+
## 2026-06-28
|
|
29
|
+
|
|
30
|
+
- **New** — group work by client: optional `client` (name or email) on
|
|
31
|
+
`publish_site` / `create_drive` / `create_project`, `--client` on the CLI,
|
|
32
|
+
`shiply client ls|show`, and `clientId` filters on list calls. The client
|
|
33
|
+
record self-assembles — no CRM step. (CLI 0.21.0)
|
|
34
|
+
|
|
35
|
+
## 2026-06-27
|
|
36
|
+
|
|
37
|
+
- **Update** — one-Allow-forever guidance: persist the device-flow API key to
|
|
38
|
+
`~/.shiply/credentials` (`{"apiKey":"shp_..."}`) so every future publish in
|
|
39
|
+
any session is owned automatically.
|
|
40
|
+
|
|
41
|
+
## 2026-06-26
|
|
42
|
+
|
|
43
|
+
- **New** — `shiply logs <slug>` + `GET /api/v1/sites/<slug>/logs`: real
|
|
44
|
+
per-site Worker runtime logs (7-day retention, newest-first). The MCP
|
|
45
|
+
`get_function_logs` tool returns the same real logs. (CLI 0.20.0)
|
|
46
|
+
|
|
47
|
+
## 2026-06-25
|
|
48
|
+
|
|
49
|
+
- **New** — the SSR lane is complete: SvelteKit, Astro, Qwik City, the Nitro
|
|
50
|
+
family (Nuxt, SolidStart, Analog, TanStack Start), React Router v7, and
|
|
51
|
+
Next.js (via OpenNext) all deploy server-side rendered with
|
|
52
|
+
`shiply publish .` after a build. (CLI 0.18.x–0.19.0)
|
|
53
|
+
|
|
54
|
+
## 2026-06-23
|
|
55
|
+
|
|
56
|
+
- **New** — site management batch: `shiply ls` / `rm` / `rollback` /
|
|
57
|
+
`verify` (stable `VERIFY status=…` marker) / `promote <preview> --to
|
|
58
|
+
<prod>` / `publish --as <name>` stable previews / `--json` machine output /
|
|
59
|
+
`.shiplyignore`. (CLI 0.17.0)
|
|
60
|
+
|
|
61
|
+
## 2026-06-22
|
|
62
|
+
|
|
63
|
+
- **New** — Agent Email: every owned site captures signups
|
|
64
|
+
(`POST /.shiply/email`), sends (`/api/v1/email/send`), reads its inbox, and
|
|
65
|
+
broadcasts to double-opt-in audiences.
|
|
66
|
+
- **Update** — raw HTTP publish + finalize responses now include the
|
|
67
|
+
`toUpdate` string (the exact next-update call), matching MCP.
|
|
68
|
+
|
|
69
|
+
## 2026-06-21
|
|
70
|
+
|
|
71
|
+
- **New** — Contracts: draft from a project brief, customer e-signs in the
|
|
72
|
+
portal, amend after signing. MCP `contract_draft/send/amend/status/pdf`.
|
|
73
|
+
|
|
74
|
+
## 2026-06-20
|
|
75
|
+
|
|
76
|
+
- **New** — Functions (Workers Lite): `worker.js`/`worker.ts` at the publish
|
|
77
|
+
root deploys as a per-site Worker — webhooks, crons, secrets.
|
|
78
|
+
- **New** — Projects (customer intake + AI brief), Marketplace (sell sites via
|
|
79
|
+
Stripe Connect), and Sending domains (BYO verified outbound domain) CLI +
|
|
80
|
+
MCP + REST surfaces.
|
|
81
|
+
- **New** — custom domains: primary-subdomain selection with sibling 301s
|
|
82
|
+
(SEO); bring-your-own-auth pattern documented (Clerk/Auth.js/Supabase).
|
|
83
|
+
|
|
84
|
+
## Earlier
|
|
85
|
+
|
|
86
|
+
- Databases (free D1 + Neon Postgres with branching), Site Data
|
|
87
|
+
(zero-backend forms), proxy routes (server-side secret injection), access
|
|
88
|
+
control (password / invite-only), Drives (private storage), email demand
|
|
89
|
+
tests, device-flow auth (`agentName` on anonymous publish), anonymous
|
|
90
|
+
claim / pairing codes.
|