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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shiply-cli",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "description": "Publish static sites to shiply.now from the command line — instant web hosting for agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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-27**
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:"..."}` (save the key as
68
- `SHIPLY_API_KEY`, the site is now permanent Allow does both jobs in one
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"** → save it as
98
- `SHIPLY_API_KEY` in your environment / config and send
99
- `Authorization: Bearer <key>` on every future publish. All future sites
100
- are then permanent and appear in their dashboard automatically.
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