shiply-cli 0.7.1 → 0.12.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/README.md +104 -47
- package/dist/claim.js +32 -0
- package/dist/data-init.js +33 -0
- package/dist/data.js +99 -0
- package/dist/db.js +225 -0
- package/dist/detect.js +35 -0
- package/dist/domain.js +56 -0
- package/dist/framework.js +110 -0
- package/dist/index.js +327 -55
- package/dist/login.js +60 -0
- package/dist/publish.js +2 -0
- package/package.json +38 -23
- package/skill/SKILL.md +359 -129
package/dist/framework.js
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import { readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
const exists = async (p) => stat(p).then((s) => s.isFile() || s.isDirectory(), () => false);
|
|
4
|
+
const SSG_SIGNATURES = [
|
|
5
|
+
{ files: ['config.toml', 'hugo.toml', 'config.yaml', 'hugo.yaml'], framework: 'Hugo', outputDir: 'public', buildCommand: 'hugo' },
|
|
6
|
+
{ files: ['_config.yml', '_config.yaml'], framework: 'Jekyll', outputDir: '_site', buildCommand: 'bundle exec jekyll build' },
|
|
7
|
+
{ files: ['.eleventy.js', 'eleventy.config.js', 'eleventy.config.mjs', 'eleventy.config.cjs'], framework: 'Eleventy', outputDir: '_site', buildCommand: 'npx @11ty/eleventy' },
|
|
8
|
+
{ files: ['mkdocs.yml', 'mkdocs.yaml'], framework: 'MkDocs', outputDir: 'site', buildCommand: 'mkdocs build' },
|
|
9
|
+
];
|
|
10
|
+
async function detectStaticSiteGenerator(dir) {
|
|
11
|
+
for (const sig of SSG_SIGNATURES) {
|
|
12
|
+
for (const f of sig.files) {
|
|
13
|
+
if (await exists(join(dir, f))) {
|
|
14
|
+
return { framework: sig.framework, outputDir: sig.outputDir, spa: false, buildCommand: sig.buildCommand };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
4
20
|
/** Detect a framework *source* directory (package.json, no root index.html)
|
|
5
21
|
* so `shiply publish .` on a React/Vue/etc project can do the right thing:
|
|
6
22
|
* publish the build output with SPA mode instead of raw source files. */
|
|
7
23
|
export async function detectFramework(dir) {
|
|
24
|
+
// SSG configs win over package.json-based detection (a Hugo site might
|
|
25
|
+
// have a package.json for tooling but isn't a JS-framework build).
|
|
26
|
+
const ssg = await detectStaticSiteGenerator(dir);
|
|
27
|
+
if (ssg)
|
|
28
|
+
return ssg;
|
|
8
29
|
if (await exists(join(dir, 'index.html'))) {
|
|
9
30
|
// Vite keeps index.html at the root of the *source* too — only treat it as
|
|
10
31
|
// a source dir when a vite config is present alongside it.
|
|
@@ -31,6 +52,24 @@ export async function detectFramework(dir) {
|
|
|
31
52
|
if (has('next')) {
|
|
32
53
|
return { framework: 'Next.js', outputDir: 'out', spa: false, buildCommand: 'npm run build (needs `output: "export"` in next.config)' };
|
|
33
54
|
}
|
|
55
|
+
if (has('nuxt')) {
|
|
56
|
+
return { framework: 'Nuxt', outputDir: '.output/public', spa: false, buildCommand: 'npm run generate (or `npm run build` with nitro preset "static")' };
|
|
57
|
+
}
|
|
58
|
+
if (has('@remix-run/react')) {
|
|
59
|
+
return { framework: 'Remix', outputDir: 'build/client', spa: true, buildCommand: 'npm run build' };
|
|
60
|
+
}
|
|
61
|
+
if (has('@docusaurus/core')) {
|
|
62
|
+
return { framework: 'Docusaurus', outputDir: 'build', spa: false, buildCommand: 'npm run build' };
|
|
63
|
+
}
|
|
64
|
+
if (has('hexo')) {
|
|
65
|
+
return { framework: 'Hexo', outputDir: 'public', spa: false, buildCommand: 'npx hexo generate' };
|
|
66
|
+
}
|
|
67
|
+
if (has('@solidjs/start')) {
|
|
68
|
+
return { framework: 'SolidStart', outputDir: 'dist/public', spa: false, buildCommand: 'npm run build (use static preset in app.config)' };
|
|
69
|
+
}
|
|
70
|
+
if (has('@builder.io/qwik-city')) {
|
|
71
|
+
return { framework: 'Qwik City', outputDir: 'dist', spa: false, buildCommand: 'npm run build.static (or use static adapter)' };
|
|
72
|
+
}
|
|
34
73
|
if (has('astro'))
|
|
35
74
|
return { framework: 'Astro', outputDir: 'dist', spa: false, buildCommand: 'npm run build' };
|
|
36
75
|
if (has('@sveltejs/kit')) {
|
|
@@ -50,3 +89,74 @@ export async function findBuildOutput(dir, hint) {
|
|
|
50
89
|
const out = join(dir, hint.outputDir);
|
|
51
90
|
return (await exists(join(out, 'index.html'))) ? out : null;
|
|
52
91
|
}
|
|
92
|
+
/** Common output directories, in priority order. First match wins. */
|
|
93
|
+
const BUILT_OUTPUT_DIRS = ['dist', 'build', 'out', '_site', 'public', '.output/public'];
|
|
94
|
+
/** Named framework hints for `--framework=<name>` override. Lowercase keys; aliases allowed. */
|
|
95
|
+
const NAMED_FRAMEWORKS = {
|
|
96
|
+
'next.js': { framework: 'Next.js', outputDir: 'out', spa: false, buildCommand: 'npm run build (needs `output: "export"` in next.config)' },
|
|
97
|
+
next: { framework: 'Next.js', outputDir: 'out', spa: false, buildCommand: 'npm run build (needs `output: "export"` in next.config)' },
|
|
98
|
+
nuxt: { framework: 'Nuxt', outputDir: '.output/public', spa: false, buildCommand: 'npm run generate' },
|
|
99
|
+
astro: { framework: 'Astro', outputDir: 'dist', spa: false, buildCommand: 'npm run build' },
|
|
100
|
+
sveltekit: { framework: 'SvelteKit', outputDir: 'build', spa: true, buildCommand: 'npm run build (use adapter-static)' },
|
|
101
|
+
vite: { framework: 'Vite', outputDir: 'dist', spa: true, buildCommand: 'npm run build' },
|
|
102
|
+
remix: { framework: 'Remix', outputDir: 'build/client', spa: true, buildCommand: 'npm run build' },
|
|
103
|
+
cra: { framework: 'Create React App', outputDir: 'build', spa: true, buildCommand: 'npm run build' },
|
|
104
|
+
docusaurus: { framework: 'Docusaurus', outputDir: 'build', spa: false, buildCommand: 'npm run build' },
|
|
105
|
+
hexo: { framework: 'Hexo', outputDir: 'public', spa: false, buildCommand: 'npx hexo generate' },
|
|
106
|
+
solidstart: { framework: 'SolidStart', outputDir: 'dist/public', spa: false, buildCommand: 'npm run build' },
|
|
107
|
+
qwik: { framework: 'Qwik City', outputDir: 'dist', spa: false, buildCommand: 'npm run build.static' },
|
|
108
|
+
hugo: { framework: 'Hugo', outputDir: 'public', spa: false, buildCommand: 'hugo' },
|
|
109
|
+
jekyll: { framework: 'Jekyll', outputDir: '_site', spa: false, buildCommand: 'bundle exec jekyll build' },
|
|
110
|
+
eleventy: { framework: 'Eleventy', outputDir: '_site', spa: false, buildCommand: 'npx @11ty/eleventy' },
|
|
111
|
+
'11ty': { framework: 'Eleventy', outputDir: '_site', spa: false, buildCommand: 'npx @11ty/eleventy' },
|
|
112
|
+
mkdocs: { framework: 'MkDocs', outputDir: 'site', spa: false, buildCommand: 'mkdocs build' },
|
|
113
|
+
};
|
|
114
|
+
/** Look up a framework hint by name (case-insensitive). Used by `--framework=<name>` override. */
|
|
115
|
+
export function frameworkByName(name) {
|
|
116
|
+
return NAMED_FRAMEWORKS[name.toLowerCase()] ?? null;
|
|
117
|
+
}
|
|
118
|
+
/** Detect a pre-built static output the user has already produced. Used when
|
|
119
|
+
* no framework signature matched the source dir, as a last resort before
|
|
120
|
+
* treating the dir itself as the static payload. */
|
|
121
|
+
export async function detectBuiltOutput(dir) {
|
|
122
|
+
for (const out of BUILT_OUTPUT_DIRS) {
|
|
123
|
+
if (await exists(join(dir, out, 'index.html'))) {
|
|
124
|
+
return { framework: 'pre-built', outputDir: out, spa: false, buildCommand: `(skipped — ${out}/ already present)` };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/** Decide which directory to publish:
|
|
130
|
+
* 0. If `override.framework` is set → trust it, build-or-throw.
|
|
131
|
+
* 1. If a framework matched and its outputDir has index.html → that.
|
|
132
|
+
* 2. Else if a framework matched but the outputDir is missing → throw (user needs to build).
|
|
133
|
+
* 3. Else if a pre-built output dir exists → that.
|
|
134
|
+
* 4. Else → the source dir as-is. */
|
|
135
|
+
export async function resolvePayloadDir(sourceDir, override) {
|
|
136
|
+
if (override?.framework) {
|
|
137
|
+
const hint = frameworkByName(override.framework);
|
|
138
|
+
if (!hint) {
|
|
139
|
+
throw new Error(`Unknown framework "${override.framework}". Try one of: ${Object.keys(NAMED_FRAMEWORKS).join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
const built = await findBuildOutput(sourceDir, hint);
|
|
142
|
+
if (!built) {
|
|
143
|
+
throw new Error(`--framework=${override.framework} expects ${hint.outputDir}/index.html — build first:\n` +
|
|
144
|
+
` ${hint.buildCommand}\n` +
|
|
145
|
+
` then: shiply publish ./${hint.outputDir}${hint.spa ? ' --spa' : ''}`);
|
|
146
|
+
}
|
|
147
|
+
return { dir: built, hint };
|
|
148
|
+
}
|
|
149
|
+
const framework = await detectFramework(sourceDir);
|
|
150
|
+
if (framework) {
|
|
151
|
+
const built = await findBuildOutput(sourceDir, framework);
|
|
152
|
+
if (built)
|
|
153
|
+
return { dir: built, hint: framework };
|
|
154
|
+
throw new Error(`Detected ${framework.framework}, but ${framework.outputDir}/index.html is missing — build first:\n` +
|
|
155
|
+
` ${framework.buildCommand}\n` +
|
|
156
|
+
` then: shiply publish ./${framework.outputDir}${framework.spa ? ' --spa' : ''}`);
|
|
157
|
+
}
|
|
158
|
+
const fallback = await detectBuiltOutput(sourceDir);
|
|
159
|
+
if (fallback)
|
|
160
|
+
return { dir: join(sourceDir, fallback.outputDir), hint: fallback };
|
|
161
|
+
return { dir: sourceDir, hint: null };
|
|
162
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,46 +1,86 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
3
|
import { parseArgs } from 'node:util';
|
|
5
4
|
import { confetti } from './confetti.js';
|
|
6
|
-
import {
|
|
5
|
+
import { runDetect } from './detect.js';
|
|
6
|
+
import { resolvePayloadDir } from './framework.js';
|
|
7
|
+
import { runClaimVerify } from './claim.js';
|
|
8
|
+
import { dbAttach, dbBranch, dbBranches, dbCreate, dbDelete, dbDeleteBranch, dbLs, dbMerge, dbMigrate, dbSql, } from './db.js';
|
|
7
9
|
import { driveGet, driveLs, drivePublish, drivePut, driveRm } from './drive.js';
|
|
10
|
+
import { domainAdd, domainConnect, domainLs, domainSubAdd, domainSync } from './domain.js';
|
|
8
11
|
import { loadApiKey, saveApiKey } from './config.js';
|
|
9
|
-
import {
|
|
12
|
+
import { loginViaDeviceFlow } from './login.js';
|
|
13
|
+
import { api, ApiError, DEFAULT_BASE, publish, resolveBase } from './publish.js';
|
|
10
14
|
import { installSkill } from './skill.js';
|
|
11
15
|
import { readState, writeState } from './state.js';
|
|
12
16
|
import { checkReadiness, targetToHostname } from './status.js';
|
|
13
|
-
const HELP = `shiply — instant static hosting for agents (https://shiply.now)
|
|
14
|
-
|
|
15
|
-
Usage:
|
|
16
|
-
shiply publish <dir> [options] Publish a directory, print the live URL.
|
|
17
|
-
Re-running UPDATES the same site (state in .shiply.json)
|
|
18
|
-
shiply update <dir> Same as publish when .shiply.json exists
|
|
19
|
-
shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
|
|
20
|
-
shiply duplicate <slug> Server-side copy of an owned site → new URL
|
|
21
|
-
shiply
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
shiply
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
17
|
+
const HELP = `shiply — instant static hosting for agents (https://shiply.now)
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
shiply publish <dir> [options] Publish a directory, print the live URL.
|
|
21
|
+
Re-running UPDATES the same site (state in .shiply.json)
|
|
22
|
+
shiply update <dir> Same as publish when .shiply.json exists
|
|
23
|
+
shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
|
|
24
|
+
shiply duplicate <slug> Server-side copy of an owned site → new URL
|
|
25
|
+
shiply detect [dir] Diagnose: print framework + dir that would upload
|
|
26
|
+
(no upload — use --framework=<name> to test an override)
|
|
27
|
+
shiply drive <ls|put|get|rm|publish> Private cloud storage (drive publish → live site)
|
|
28
|
+
shiply db <create|ls|sql|migrate|delete|attach>
|
|
29
|
+
Per-site SQL databases (Cloudflare D1 or Neon Postgres)
|
|
30
|
+
shiply db create <name> [--postgres] [--site <slug>]
|
|
31
|
+
D1 by default — pass --postgres for Neon
|
|
32
|
+
shiply db branch <db> <branch-name> Branch a Neon database (developer plan)
|
|
33
|
+
shiply db branches <db> List branches of a Neon database
|
|
34
|
+
shiply db delete-branch <branch-or-id> --yes Delete a Neon branch
|
|
35
|
+
shiply db merge <branch> No-op alias — prints the pg_dump migration tip
|
|
36
|
+
shiply domain ls List custom domains on your account
|
|
37
|
+
shiply domain add <domain> Register a custom domain
|
|
38
|
+
shiply domain connect <domain> One-click OAuth DNS setup (Cloudflare/Domain Connect)
|
|
39
|
+
shiply domain sub add <host> --site <slug> Map a subdomain (e.g. app.example.com) to a slug
|
|
40
|
+
shiply domain sync <domain> Re-sync DNS records for a connected domain
|
|
41
|
+
shiply data init [dir] Scaffold a starter .shiply/data.json in <dir> (default cwd)
|
|
42
|
+
shiply data list <slug> List collections on a site with counts
|
|
43
|
+
shiply data query <slug> <coll> [--limit N] [--cursor C] [--where '<json>']
|
|
44
|
+
Read records (default 50)
|
|
45
|
+
shiply data insert <slug> <coll> --json '<body>'
|
|
46
|
+
Insert one record via the public endpoint
|
|
47
|
+
shiply data export <slug> <coll> [--out file.ndjson]
|
|
48
|
+
Stream every record as NDJSON
|
|
49
|
+
shiply data wipe-collection <slug> <coll> --yes
|
|
50
|
+
Delete every record in a collection
|
|
51
|
+
shiply claim verify <code> Confirm a pairing code from /claim/<slug>?pair=1
|
|
52
|
+
(uses .shiply.json from CWD)
|
|
53
|
+
shiply skill [--project] Install the shiply skill for your AI agent
|
|
54
|
+
(global ~/.claude/skills, or ./.claude/skills with --project)
|
|
55
|
+
shiply login [--name <label>] Open a browser, click Allow → key saved
|
|
56
|
+
(--email <addr> falls back to legacy 6-digit code)
|
|
57
|
+
shiply help
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
--spa Single-page app mode (unknown paths fall back to index.html)
|
|
61
|
+
--framework <name> Force the detector (e.g. hugo, nuxt, vite) for non-standard layouts
|
|
62
|
+
--claim-token <tok> Update a specific anonymous site (overrides .shiply.json)
|
|
63
|
+
--new-site Ignore .shiply.json and create a fresh site
|
|
64
|
+
--preview-branch <id> (publish) bind this version to a Neon branch's DATABASE_URL
|
|
65
|
+
(id = the branch row's site_databases.id; off by default)
|
|
66
|
+
--key <key> API key (default: $SHIPLY_API_KEY, then ~/.shiply/credentials)
|
|
67
|
+
--anonymous Publish without an API key even if one is saved
|
|
68
|
+
--base <url> API origin (default: ${DEFAULT_BASE})
|
|
69
|
+
--wait (status) poll until the site is ready
|
|
70
|
+
--timeout <seconds> (status --wait) give up after this long (default 300)
|
|
71
|
+
--json <body> (data insert) JSON object to insert
|
|
72
|
+
--limit <N> (data query) max records to return (default 50)
|
|
73
|
+
--cursor <iso> (data query) pagination cursor from previous response
|
|
74
|
+
--where <json> (data query) client-side filter, e.g. '{"email":"a@b.co"}'
|
|
75
|
+
--yes (data wipe-collection) skip the safety prompt
|
|
76
|
+
--no-confetti Celebrate quietly
|
|
77
|
+
|
|
78
|
+
\`shiply status\` prints stable machine-readable markers for agents:
|
|
79
|
+
SSL_READY and SITE_READY on success (exit 0); ✖ lines and exit 1 otherwise.
|
|
80
|
+
|
|
81
|
+
With an API key the site is permanent and owned by your account. Without one
|
|
82
|
+
it expires in 24 hours — save the printed claimToken/claimUrl to update or
|
|
83
|
+
claim it later.
|
|
44
84
|
`;
|
|
45
85
|
async function reportReadiness(host, celebrate) {
|
|
46
86
|
const r = await checkReadiness(host);
|
|
@@ -70,17 +110,29 @@ async function main() {
|
|
|
70
110
|
allowPositionals: true,
|
|
71
111
|
options: {
|
|
72
112
|
spa: { type: 'boolean' },
|
|
113
|
+
framework: { type: 'string' },
|
|
73
114
|
'claim-token': { type: 'string' },
|
|
74
115
|
key: { type: 'string' },
|
|
75
116
|
anonymous: { type: 'boolean' },
|
|
76
117
|
base: { type: 'string' },
|
|
77
118
|
email: { type: 'string' },
|
|
119
|
+
name: { type: 'string' },
|
|
78
120
|
wait: { type: 'boolean' },
|
|
79
121
|
'new-site': { type: 'boolean' },
|
|
80
122
|
project: { type: 'boolean' },
|
|
81
123
|
timeout: { type: 'string' },
|
|
82
124
|
out: { type: 'string', short: 'o' },
|
|
125
|
+
site: { type: 'string' },
|
|
126
|
+
binding: { type: 'string' },
|
|
127
|
+
params: { type: 'string' },
|
|
128
|
+
postgres: { type: 'boolean' },
|
|
129
|
+
'preview-branch': { type: 'string' },
|
|
130
|
+
yes: { type: 'boolean' },
|
|
83
131
|
'no-confetti': { type: 'boolean' },
|
|
132
|
+
json: { type: 'string' },
|
|
133
|
+
limit: { type: 'string' },
|
|
134
|
+
cursor: { type: 'string' },
|
|
135
|
+
where: { type: 'string' },
|
|
84
136
|
help: { type: 'boolean', short: 'h' },
|
|
85
137
|
},
|
|
86
138
|
});
|
|
@@ -95,26 +147,14 @@ async function main() {
|
|
|
95
147
|
if (!dir)
|
|
96
148
|
throw new Error(`usage: shiply ${cmd} <dir>`);
|
|
97
149
|
const apiKey = values.anonymous ? undefined : (values.key ?? (await loadApiKey()));
|
|
98
|
-
//
|
|
99
|
-
// not the source.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const hint = await
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
publishDir = out;
|
|
107
|
-
if (hint.spa && spa === undefined)
|
|
108
|
-
spa = true;
|
|
109
|
-
console.log(`✔ ${hint.framework} project detected — publishing ${out}${hint.spa ? ' with SPA mode' : ''}`);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
throw new Error(`this looks like a ${hint.framework} project (source code, not a built site).\n` +
|
|
113
|
-
` Build it first, then publish the output:\n` +
|
|
114
|
-
` ${hint.buildCommand}\n` +
|
|
115
|
-
` shiply ${cmd} ${join(dir, hint.outputDir)}${hint.spa ? ' --spa' : ''}\n` +
|
|
116
|
-
` (or pass the built folder directly if it lives elsewhere)`);
|
|
117
|
-
}
|
|
150
|
+
// Any static site works — React/Vue/Astro/Hugo/Jekyll/etc — but you
|
|
151
|
+
// publish the BUILD OUTPUT, not the source. resolvePayloadDir picks the
|
|
152
|
+
// right dir (source as-is, a matched framework's build output, or a
|
|
153
|
+
// pre-built fallback like dist/).
|
|
154
|
+
const { dir: publishDir, hint } = await resolvePayloadDir(dir, { framework: values.framework });
|
|
155
|
+
const spa = values.spa ?? (hint?.spa ? true : undefined);
|
|
156
|
+
if (hint && publishDir !== dir) {
|
|
157
|
+
console.log(`✔ ${hint.framework} project detected — publishing ${publishDir}${hint.spa ? ' with SPA mode' : ''}`);
|
|
118
158
|
}
|
|
119
159
|
// same command, same URL: reuse this directory's site automatically
|
|
120
160
|
const state = await readState(publishDir);
|
|
@@ -128,18 +168,28 @@ async function main() {
|
|
|
128
168
|
throw new Error('nothing to update here — publish first (shiply remembers the site in .shiply.json), or pass --claim-token');
|
|
129
169
|
}
|
|
130
170
|
const updating = Boolean(claimToken || slug);
|
|
171
|
+
// Auto-attach a previously-created database on publish. Anonymous
|
|
172
|
+
// publishes can't attach (no auth subject) — silently skipped server-side.
|
|
173
|
+
const attachDatabaseId = state?.databaseId && apiKey ? state.databaseId : undefined;
|
|
174
|
+
// Opt-in: bind THIS publish version to a Neon branch's URI. Server
|
|
175
|
+
// validates ownership + that the branch lives under a project attached
|
|
176
|
+
// to this site; a bogus/foreign id silently falls back to the parent URI.
|
|
177
|
+
const previewBranchDbId = values['preview-branch'];
|
|
131
178
|
const res = await publish(publishDir, {
|
|
132
179
|
apiKey,
|
|
133
180
|
base: values.base,
|
|
134
181
|
spaMode: spa,
|
|
135
182
|
claimToken,
|
|
136
183
|
slug,
|
|
184
|
+
attachDatabaseId,
|
|
185
|
+
previewBranchDbId,
|
|
137
186
|
});
|
|
138
187
|
await writeState(publishDir, {
|
|
139
188
|
slug: res.slug,
|
|
140
189
|
siteUrl: res.siteUrl,
|
|
141
190
|
...(res.claimToken ? { claimToken: res.claimToken } : state?.claimToken && updating ? { claimToken: state.claimToken } : {}),
|
|
142
191
|
owned: !res.anonymous,
|
|
192
|
+
...(state?.databaseId ? { databaseId: state.databaseId } : {}),
|
|
143
193
|
});
|
|
144
194
|
const skipped = res.skipped > 0 ? ` (${res.skipped} unchanged, skipped)` : '';
|
|
145
195
|
console.log(`✔ ${updating ? `updated ${res.slug} in place` : 'published'} — ${res.uploaded} file${res.uploaded === 1 ? '' : 's'}${skipped}`);
|
|
@@ -212,6 +262,10 @@ async function main() {
|
|
|
212
262
|
console.log(`\n ${out.siteUrl}\n`);
|
|
213
263
|
return;
|
|
214
264
|
}
|
|
265
|
+
case 'detect': {
|
|
266
|
+
await runDetect(dir ?? '.', { framework: values.framework });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
215
269
|
case 'drive': {
|
|
216
270
|
const apiKey = values.key ?? (await loadApiKey());
|
|
217
271
|
if (!apiKey)
|
|
@@ -246,6 +300,200 @@ async function main() {
|
|
|
246
300
|
throw new Error('usage: shiply drive <ls|put|get|rm|publish>');
|
|
247
301
|
}
|
|
248
302
|
}
|
|
303
|
+
case 'data': {
|
|
304
|
+
const sub = dir; // positionals[1]
|
|
305
|
+
// `data init` is local-only (no network, no API key) — handle before the key check.
|
|
306
|
+
if (sub === 'init') {
|
|
307
|
+
const { dataInit } = await import('./data-init.js');
|
|
308
|
+
const target = await dataInit(positionals[2] ?? '.');
|
|
309
|
+
console.log(`✔ wrote ${target}`);
|
|
310
|
+
console.log(' edit collections, then `shiply publish` to enable site data');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
314
|
+
if (!apiKey)
|
|
315
|
+
throw new Error('data commands need an API key — run `shiply login` first');
|
|
316
|
+
const dctx = { base: values.base, apiKey };
|
|
317
|
+
const slug = positionals[2];
|
|
318
|
+
const collection = positionals[3];
|
|
319
|
+
switch (sub) {
|
|
320
|
+
case 'list': {
|
|
321
|
+
if (!slug)
|
|
322
|
+
throw new Error('usage: shiply data list <slug>');
|
|
323
|
+
const { dataList } = await import('./data.js');
|
|
324
|
+
await dataList(dctx, slug);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
case 'query': {
|
|
328
|
+
if (!slug || !collection)
|
|
329
|
+
throw new Error("usage: shiply data query <slug> <collection> [--limit N] [--cursor C] [--where '<json>']");
|
|
330
|
+
const { dataQuery } = await import('./data.js');
|
|
331
|
+
await dataQuery(dctx, slug, collection, {
|
|
332
|
+
limit: values.limit ? Number(values.limit) : undefined,
|
|
333
|
+
cursor: values.cursor,
|
|
334
|
+
where: values.where,
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
case 'insert': {
|
|
339
|
+
if (!slug || !collection)
|
|
340
|
+
throw new Error("usage: shiply data insert <slug> <collection> --json '<body>'");
|
|
341
|
+
if (!values.json)
|
|
342
|
+
throw new Error('--json <body> is required');
|
|
343
|
+
let body;
|
|
344
|
+
try {
|
|
345
|
+
body = JSON.parse(values.json);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
throw new Error('--json must be a valid JSON object');
|
|
349
|
+
}
|
|
350
|
+
const { dataInsert } = await import('./data.js');
|
|
351
|
+
await dataInsert(dctx, slug, collection, body);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
case 'export': {
|
|
355
|
+
if (!slug || !collection)
|
|
356
|
+
throw new Error('usage: shiply data export <slug> <collection> [--out file.ndjson] (omit --out to stream to stdout)');
|
|
357
|
+
const { dataExport } = await import('./data.js');
|
|
358
|
+
await dataExport(dctx, slug, collection, { out: values.out });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
case 'wipe-collection': {
|
|
362
|
+
if (!slug || !collection)
|
|
363
|
+
throw new Error('usage: shiply data wipe-collection <slug> <collection> --yes');
|
|
364
|
+
const { dataWipeCollection } = await import('./data.js');
|
|
365
|
+
await dataWipeCollection(dctx, slug, collection, { yes: Boolean(values.yes) });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
default:
|
|
369
|
+
throw new Error('usage: shiply data <init|list|query|insert|export|wipe-collection>');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
case 'domain': {
|
|
373
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
374
|
+
if (!apiKey)
|
|
375
|
+
throw new Error('domain commands need an API key — run `shiply login` first');
|
|
376
|
+
const dctx = { base: values.base, apiKey };
|
|
377
|
+
const sub = dir; // positionals[1]
|
|
378
|
+
const arg = positionals[2];
|
|
379
|
+
const arg2 = positionals[3];
|
|
380
|
+
switch (sub) {
|
|
381
|
+
case 'ls':
|
|
382
|
+
await domainLs(dctx);
|
|
383
|
+
return;
|
|
384
|
+
case 'add':
|
|
385
|
+
if (!arg)
|
|
386
|
+
throw new Error('usage: shiply domain add <domain>');
|
|
387
|
+
await domainAdd(dctx, arg);
|
|
388
|
+
return;
|
|
389
|
+
case 'connect':
|
|
390
|
+
if (!arg)
|
|
391
|
+
throw new Error('usage: shiply domain connect <domain>');
|
|
392
|
+
await domainConnect(dctx, arg);
|
|
393
|
+
return;
|
|
394
|
+
case 'sub': {
|
|
395
|
+
if (arg !== 'add')
|
|
396
|
+
throw new Error('usage: shiply domain sub add <host> --site <slug>');
|
|
397
|
+
const hostname = arg2;
|
|
398
|
+
const slug = values.site;
|
|
399
|
+
if (!hostname)
|
|
400
|
+
throw new Error('usage: shiply domain sub add <host> --site <slug>');
|
|
401
|
+
if (!slug)
|
|
402
|
+
throw new Error('--site <slug> is required for `shiply domain sub add`');
|
|
403
|
+
await domainSubAdd(dctx, hostname, slug);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
case 'sync':
|
|
407
|
+
if (!arg)
|
|
408
|
+
throw new Error('usage: shiply domain sync <domain>');
|
|
409
|
+
await domainSync(dctx, arg);
|
|
410
|
+
return;
|
|
411
|
+
default:
|
|
412
|
+
throw new Error('usage: shiply domain <ls|add|connect|sub|sync>');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
case 'db': {
|
|
416
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
417
|
+
if (!apiKey)
|
|
418
|
+
throw new Error('db commands need an API key — run `shiply login` first');
|
|
419
|
+
const dctx = { base: values.base, apiKey };
|
|
420
|
+
const sub = dir; // positionals[1]
|
|
421
|
+
const arg = positionals[2];
|
|
422
|
+
const arg2 = positionals[3];
|
|
423
|
+
switch (sub) {
|
|
424
|
+
case 'create':
|
|
425
|
+
if (!arg)
|
|
426
|
+
throw new Error('usage: shiply db create <name> [--postgres] [--site <slug>] [--binding <NAME>]');
|
|
427
|
+
await dbCreate(dctx, arg, {
|
|
428
|
+
site: values.site,
|
|
429
|
+
binding: values.binding,
|
|
430
|
+
cwd: process.cwd(),
|
|
431
|
+
postgres: Boolean(values.postgres),
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
case 'ls':
|
|
435
|
+
await dbLs(dctx);
|
|
436
|
+
return;
|
|
437
|
+
case 'sql':
|
|
438
|
+
if (!arg || !arg2)
|
|
439
|
+
throw new Error('usage: shiply db sql <name-or-id> "<query>" [--params <json>]');
|
|
440
|
+
await dbSql(dctx, arg, arg2, values.params);
|
|
441
|
+
return;
|
|
442
|
+
case 'migrate':
|
|
443
|
+
if (!arg || !arg2)
|
|
444
|
+
throw new Error('usage: shiply db migrate <name-or-id> <dir>');
|
|
445
|
+
await dbMigrate(dctx, arg, arg2);
|
|
446
|
+
return;
|
|
447
|
+
case 'delete':
|
|
448
|
+
if (!arg)
|
|
449
|
+
throw new Error('usage: shiply db delete <name-or-id> [--yes]');
|
|
450
|
+
await dbDelete(dctx, arg, { yes: Boolean(values.yes) });
|
|
451
|
+
return;
|
|
452
|
+
case 'attach':
|
|
453
|
+
if (!arg)
|
|
454
|
+
throw new Error('usage: shiply db attach <name-or-id> --site <slug>');
|
|
455
|
+
if (!values.site)
|
|
456
|
+
throw new Error('--site <slug> is required for `shiply db attach`');
|
|
457
|
+
await dbAttach(dctx, arg, values.site);
|
|
458
|
+
return;
|
|
459
|
+
case 'branch':
|
|
460
|
+
if (!arg || !arg2)
|
|
461
|
+
throw new Error('usage: shiply db branch <db-name-or-id> <branch-name>');
|
|
462
|
+
await dbBranch(dctx, arg, arg2);
|
|
463
|
+
return;
|
|
464
|
+
case 'branches':
|
|
465
|
+
if (!arg)
|
|
466
|
+
throw new Error('usage: shiply db branches <db-name-or-id>');
|
|
467
|
+
await dbBranches(dctx, arg);
|
|
468
|
+
return;
|
|
469
|
+
case 'delete-branch':
|
|
470
|
+
if (!arg)
|
|
471
|
+
throw new Error('usage: shiply db delete-branch <branch-name-or-id> [--yes]');
|
|
472
|
+
await dbDeleteBranch(dctx, arg, { yes: Boolean(values.yes) });
|
|
473
|
+
return;
|
|
474
|
+
case 'merge':
|
|
475
|
+
if (!arg)
|
|
476
|
+
throw new Error('usage: shiply db merge <branch-name>');
|
|
477
|
+
await dbMerge(dctx, arg);
|
|
478
|
+
return;
|
|
479
|
+
default:
|
|
480
|
+
throw new Error('usage: shiply db <create|ls|sql|migrate|delete|attach|branch|branches|delete-branch|merge>');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
case 'claim': {
|
|
484
|
+
if (dir !== 'verify') {
|
|
485
|
+
console.error('Usage: shiply claim verify <SHIPLY-XXXXXXXX>');
|
|
486
|
+
process.exit(2);
|
|
487
|
+
}
|
|
488
|
+
const code = positionals[2];
|
|
489
|
+
if (!code) {
|
|
490
|
+
console.error('Usage: shiply claim verify <SHIPLY-XXXXXXXX>');
|
|
491
|
+
process.exit(2);
|
|
492
|
+
}
|
|
493
|
+
const r = await runClaimVerify({ code });
|
|
494
|
+
console.log(r.message);
|
|
495
|
+
process.exit(r.ok ? 0 : 1);
|
|
496
|
+
}
|
|
249
497
|
case 'skill': {
|
|
250
498
|
const written = await installSkill(Boolean(values.project));
|
|
251
499
|
for (const w of written)
|
|
@@ -256,9 +504,25 @@ async function main() {
|
|
|
256
504
|
}
|
|
257
505
|
case 'login': {
|
|
258
506
|
const base = resolveBase(values.base);
|
|
507
|
+
// Default path: device flow. One click in the browser, no email round-trip.
|
|
508
|
+
// The --email flag opts into the legacy code-by-email path (useful for
|
|
509
|
+
// scripted/non-interactive runs that can't open a browser).
|
|
510
|
+
if (!values.email) {
|
|
511
|
+
const result = await loginViaDeviceFlow(base, values.name);
|
|
512
|
+
if (!result.ok) {
|
|
513
|
+
console.error(`✗ login failed: ${result.reason}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
const path = await saveApiKey(result.apiKey);
|
|
517
|
+
console.log(`✔ key saved to ${path}`);
|
|
518
|
+
if (result.slug)
|
|
519
|
+
console.log(` also: site ${result.slug} attached to your account`);
|
|
520
|
+
console.log(' publishes are now permanent and owned by your account');
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
259
523
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
260
524
|
try {
|
|
261
|
-
const email = values.email
|
|
525
|
+
const email = values.email;
|
|
262
526
|
await api(`${base}/api/auth/agent/request-code`, {
|
|
263
527
|
method: 'POST',
|
|
264
528
|
headers: { 'content-type': 'application/json' },
|
|
@@ -284,6 +548,14 @@ async function main() {
|
|
|
284
548
|
}
|
|
285
549
|
}
|
|
286
550
|
main().catch((e) => {
|
|
551
|
+
// Plan-gate refusals (server returns 402 + code 'payment_required'): surface
|
|
552
|
+
// the server's human message and ALWAYS append a no-confusion upgrade pointer,
|
|
553
|
+
// even if the server message already embeds the URL. Quiet exit (no stack).
|
|
554
|
+
if (e instanceof ApiError && e.code === 'payment_required') {
|
|
555
|
+
console.error(`✖ ${e.message}`);
|
|
556
|
+
console.error(` upgrade: https://shiply.now/dashboard/plan`);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
287
559
|
const msg = e instanceof Error ? e.message : String(e);
|
|
288
560
|
console.error(`✖ ${msg}`);
|
|
289
561
|
process.exit(1);
|