shiply-cli 0.20.1 → 0.21.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/clients.js +48 -0
- package/dist/index.js +11 -0
- package/dist/publish.js +1 -0
- package/package.json +1 -1
- package/skill/SKILL.md +35 -8
package/dist/clients.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { api, resolveBase } from './publish.js';
|
|
2
|
+
import { loadApiKey } from './config.js';
|
|
3
|
+
const hdr = (apiKey) => ({
|
|
4
|
+
'content-type': 'application/json',
|
|
5
|
+
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
6
|
+
});
|
|
7
|
+
/** Turn a `--client "<name-or-email>"` flag into the API's client object.
|
|
8
|
+
* An '@' means email (create-or-find by it); otherwise a display name. */
|
|
9
|
+
export function parseClientArg(value) {
|
|
10
|
+
const v = value?.trim();
|
|
11
|
+
if (!v)
|
|
12
|
+
return undefined;
|
|
13
|
+
return v.includes('@') ? { clientEmail: v } : { clientName: v };
|
|
14
|
+
}
|
|
15
|
+
/** `shiply client ls | show <id>` — read-only view of your clients. */
|
|
16
|
+
export async function runClient(args, opts) {
|
|
17
|
+
const sub = args[0] ?? 'ls';
|
|
18
|
+
const base = resolveBase(opts.base);
|
|
19
|
+
const apiKey = opts.key ?? (await loadApiKey());
|
|
20
|
+
if (!apiKey)
|
|
21
|
+
throw new Error('`shiply client` needs an API key — run `shiply login` or pass --key');
|
|
22
|
+
if (sub === 'ls') {
|
|
23
|
+
const { clients } = await api(`${base}/api/v1/clients`, { headers: hdr(apiKey) });
|
|
24
|
+
if (clients.length === 0) {
|
|
25
|
+
console.log(' no clients yet — pass --client on publish or create an intake to add one');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const c of clients) {
|
|
29
|
+
const meta = [c.company, c.email].filter(Boolean).join(' · ');
|
|
30
|
+
console.log(` ${c.id} ${c.name}${meta ? ` (${meta})` : ''} [${c.status}]`);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (sub === 'show') {
|
|
35
|
+
const id = args[1];
|
|
36
|
+
if (!id)
|
|
37
|
+
throw new Error('usage: shiply client show <id>');
|
|
38
|
+
const r = await api(`${base}/api/v1/clients/${encodeURIComponent(id)}`, { headers: hdr(apiKey) });
|
|
39
|
+
console.log(`${r.client.name}${r.client.company ? ` · ${r.client.company}` : ''}`);
|
|
40
|
+
if (r.client.email)
|
|
41
|
+
console.log(` ${r.client.email}`);
|
|
42
|
+
console.log(` sites (${r.sites.length}): ${r.sites.map((s) => s.slug).join(', ') || '—'}`);
|
|
43
|
+
console.log(` folders (${r.drives.length}): ${r.drives.map((d) => d.name).join(', ') || '—'}`);
|
|
44
|
+
console.log(` projects (${r.projects.length}): ${r.projects.map((p) => `${p.label} [${p.status}]`).join(', ') || '—'}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`unknown client command '${sub}' — try: ls, show <id>`);
|
|
48
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { dbAttach, dbBranch, dbBranches, dbCreate, dbDelete, dbDeleteBranch, dbL
|
|
|
16
16
|
import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
|
|
17
17
|
import { domainAdd, domainConnect, domainLs, domainPrimary, domainSubAdd, domainSync } from './domain.js';
|
|
18
18
|
import { runProject } from './projects.js';
|
|
19
|
+
import { runClient, parseClientArg } from './clients.js';
|
|
19
20
|
import { runListing } from './listings.js';
|
|
20
21
|
import { runSendingDomain } from './sending-domains.js';
|
|
21
22
|
import { contractAmend, contractDraft, contractList, contractPdf, contractRetract, contractSend, contractShow, } from './contract.js';
|
|
@@ -82,6 +83,9 @@ Usage:
|
|
|
82
83
|
Customer-intake projects + AI brief generator
|
|
83
84
|
shiply project create <label> [--customer-email <e>] [--customer-name <n>]
|
|
84
85
|
Returns the intake URL to share with the customer
|
|
86
|
+
shiply client <ls|show <id>> Your clients (freelancer view): customer-360 roll-up
|
|
87
|
+
shiply publish <dir> --client "<name|email>"
|
|
88
|
+
Group the site under a client (create-or-finds by email)
|
|
85
89
|
shiply listing <ls|create|rm> Marketplace listings (sell built sites)
|
|
86
90
|
shiply listing create <site-slug> --price <cents> --jurisdiction "<region>"
|
|
87
91
|
Requires Stripe Connect onboarding (see MCP get_connect_status)
|
|
@@ -257,6 +261,7 @@ async function main() {
|
|
|
257
261
|
'no-notify': { type: 'boolean' },
|
|
258
262
|
'notify-to': { type: 'string' },
|
|
259
263
|
from: { type: 'string' },
|
|
264
|
+
client: { type: 'string' },
|
|
260
265
|
help: { type: 'boolean', short: 'h' },
|
|
261
266
|
version: { type: 'boolean', short: 'v' },
|
|
262
267
|
},
|
|
@@ -347,6 +352,8 @@ async function main() {
|
|
|
347
352
|
slug,
|
|
348
353
|
attachDatabaseId,
|
|
349
354
|
previewBranchDbId,
|
|
355
|
+
// Group under a client (owned publishes only). --client "<name|email>"
|
|
356
|
+
client: apiKey ? parseClientArg(values.client) : undefined,
|
|
350
357
|
});
|
|
351
358
|
await writeState(stateDir, {
|
|
352
359
|
slug: res.slug,
|
|
@@ -734,6 +741,10 @@ async function main() {
|
|
|
734
741
|
await runProject(positionals.slice(1));
|
|
735
742
|
break;
|
|
736
743
|
}
|
|
744
|
+
case 'client': {
|
|
745
|
+
await runClient(positionals.slice(1), { base: values.base, key: values.key });
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
737
748
|
case 'listing': {
|
|
738
749
|
await runListing(positionals.slice(1));
|
|
739
750
|
break;
|
package/dist/publish.js
CHANGED
|
@@ -42,6 +42,7 @@ export async function publish(dir, opts = {}) {
|
|
|
42
42
|
...(opts.slug ? { slug: opts.slug } : {}),
|
|
43
43
|
...(opts.attachDatabaseId ? { attachDatabaseId: opts.attachDatabaseId } : {}),
|
|
44
44
|
...(opts.previewBranchDbId ? { previewBranchDbId: opts.previewBranchDbId } : {}),
|
|
45
|
+
...(opts.client ? { client: opts.client } : {}),
|
|
45
46
|
}),
|
|
46
47
|
});
|
|
47
48
|
await uploadAll(dir, created.upload.uploads);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -18,7 +18,7 @@ description: "shiply is the production backend for anything an agent builds —
|
|
|
18
18
|
> fresh — your cached copy may be missing features like databases, projects,
|
|
19
19
|
> marketplace, or sending domains.
|
|
20
20
|
>
|
|
21
|
-
> **Last updated: 2026-06-
|
|
21
|
+
> **Last updated: 2026-06-28**
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -64,14 +64,25 @@ POST https://shiply.now/api/v1/auth/device/poll
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
The response is one of `{status:"pending"}` (keep polling at `interval`s),
|
|
67
|
-
`{status:"approved", api_key:"shp_...", slug_claimed:"..."}` (
|
|
68
|
-
|
|
69
|
-
click), `{status:"expired"|"denied"|"consumed"}` (stop). Cap your polling at
|
|
67
|
+
`{status:"approved", api_key:"shp_...", slug_claimed:"..."}` (PERSIST the key —
|
|
68
|
+
see "Save the key" below — the site is now permanent; Allow does both jobs in
|
|
69
|
+
one click), `{status:"expired"|"denied"|"consumed"}` (stop). Cap your polling at
|
|
70
70
|
the announced `expires_in` and respect `interval` — don't tight-loop.
|
|
71
71
|
|
|
72
72
|
After approval, every future publish uses `Authorization: Bearer <api_key>`
|
|
73
73
|
and the user's dashboard shows their sites without any further claiming.
|
|
74
74
|
|
|
75
|
+
### Save the key — so EVERY future site is seamless (do this once)
|
|
76
|
+
|
|
77
|
+
Write the key to **`~/.shiply/credentials`** as `{"apiKey":"shp_..."}` (chmod
|
|
78
|
+
600) — the exact file the shiply CLI reads. This is what makes "one Allow, ever"
|
|
79
|
+
work: from then on every publish (any site, any project, ANY FUTURE SESSION)
|
|
80
|
+
sends `Authorization: Bearer <key>` automatically and lands owned in the user's
|
|
81
|
+
dashboard — no re-authorizing, no claim step, zero clicks. An env var alone
|
|
82
|
+
(`SHIPLY_API_KEY`) works for the current shell but does NOT survive a new
|
|
83
|
+
session — the credentials file is what makes it permanent. `shiply login` writes
|
|
84
|
+
this file for you; if you publish over raw HTTP, write it yourself.
|
|
85
|
+
|
|
75
86
|
## When the user pastes you something shiply-shaped, do this
|
|
76
87
|
|
|
77
88
|
These are the patterns you will see. Match the input, run the action — do not
|
|
@@ -94,10 +105,11 @@ ask the user to repeat themselves or explain.
|
|
|
94
105
|
as `https://shiply.now/claim/<slug>?token=<claimToken>` from those fields.
|
|
95
106
|
Either way, also suggest `shiply login` so future publishes mint to their
|
|
96
107
|
account directly — that's the long-term fix and how the dashboard works.
|
|
97
|
-
- **User says "I have an API key now, use it from now on"** →
|
|
98
|
-
`
|
|
99
|
-
`Authorization: Bearer <key>` on every future publish.
|
|
100
|
-
are then permanent and appear in their dashboard
|
|
108
|
+
- **User says "I have an API key now, use it from now on"** → persist it to
|
|
109
|
+
`~/.shiply/credentials` (`{"apiKey":"shp_..."}`, chmod 600 — see "Save the
|
|
110
|
+
key" above), then send `Authorization: Bearer <key>` on every future publish.
|
|
111
|
+
All future sites are then permanent and appear in their dashboard
|
|
112
|
+
automatically — across sessions, with no re-authorizing.
|
|
101
113
|
- **User opens an `https://shiply.now/auth/XXXX-YYYY` URL you printed** → no
|
|
102
114
|
action from you; they're on the device-flow consent screen. Keep polling
|
|
103
115
|
`poll_url` (see "First publish on a new machine" above). The poll response
|
|
@@ -646,6 +658,21 @@ CLI: `shiply project ls · shiply project create <label> [--customer-email <e>]
|
|
|
646
658
|
|
|
647
659
|
Docs: https://shiply.now/docs/projects
|
|
648
660
|
|
|
661
|
+
**Group work by client (optional, for freelancers).** Building sites for
|
|
662
|
+
clients? Pass an optional `client` (a name or email) on any `publish_site`,
|
|
663
|
+
`create_drive`, or `create_project` call — shiply create-or-finds the client
|
|
664
|
+
by email and files everything you ship for them (sites, drop-box, intake,
|
|
665
|
+
contracts) under one customer view in the dashboard. A project's customer **is**
|
|
666
|
+
the site's client (same email-keyed identity), so `create_project` already
|
|
667
|
+
links automatically whenever you pass `customerEmail` — no extra step. There is
|
|
668
|
+
no `create_client` tool and never a CRM step: the client record self-assembles
|
|
669
|
+
from the calls you already make. `publish_site` echoes the `resolvedClientId` —
|
|
670
|
+
reuse it across the session so drive/intake/contract all land under the same
|
|
671
|
+
client. List calls take an optional `clientId` filter
|
|
672
|
+
(`list_sites`/`list_drives`/`list_projects`). Omit `client` for personal
|
|
673
|
+
projects; shiply works identically. CLI: `--client "<name-or-email>"` on
|
|
674
|
+
publish/drive/project, plus `shiply client ls|show`.
|
|
675
|
+
|
|
649
676
|
## Contracts — draft, sign, amend (extends Projects)
|
|
650
677
|
|
|
651
678
|
Once a project reaches `brief_ready`, the dev drafts a signed contract from
|