@shoplifter/cli 0.1.0 → 0.1.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/README.md +12 -4
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +19 -4
- package/dist/index.js +15 -4
- package/dist/publication.js +7 -5
- package/dist/release-manifest.json +5 -5
- package/dist/runtime.js +5 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Shoplifter CLI
|
|
2
2
|
|
|
3
|
-
Requires Node 24. Install with `npm install --global @shoplifter/cli@0.1.
|
|
4
|
-
|
|
3
|
+
Requires Node 24. Install with `npm install --global @shoplifter/cli@0.1.1`.
|
|
4
|
+
It continues using SDK and contract `0.1.0`; CLI patches do not upgrade runtime inputs.
|
|
5
5
|
|
|
6
6
|
## Authoring
|
|
7
7
|
|
|
@@ -38,14 +38,22 @@ Set `SHOPLIFTER_API_URL=https://<platform-api-host>` before production operation
|
|
|
38
38
|
runtime, verifies hashes and explicitly updates local archives and the lockfile.
|
|
39
39
|
It preserves authored files. Run `npm ci --ignore-scripts` after synchronization.
|
|
40
40
|
|
|
41
|
-
`shoplifter login` opens dashboard approval for
|
|
41
|
+
`shoplifter login combo-media` opens dashboard approval for that channel code.
|
|
42
|
+
With `shoplifter login` alone, explicitly choose a channel on the approval page.
|
|
43
|
+
Only channels accessible to your dashboard account can be selected; publishing must
|
|
44
|
+
be enabled for the channel. Switching channels clears the reviewed request.
|
|
45
|
+
One-hour credentials
|
|
42
46
|
are stored with private permissions outside the project. Use `shoplifter logout` to revoke.
|
|
43
|
-
`shoplifter publication upload <channel-
|
|
47
|
+
`shoplifter publication upload <channel-code>` uploads source; `stage`, `preview`,
|
|
44
48
|
`build`, `activate`, `publish`, `status`, `logs`, `retry`, `retry-deployment` and
|
|
45
49
|
`rollback` operate on channel-scoped revisions. `preview-link` requests protected
|
|
46
50
|
preview access. Source changes require exact-input preview and explicit activation.
|
|
47
51
|
Downloading the SDK never grants publication permission. The loopback bypass is
|
|
48
52
|
development-only and cannot be used remotely or in production.
|
|
53
|
+
Older credentials resolve their channel code through the authenticated session API.
|
|
54
|
+
Set `SHOPLIFTER_NO_BROWSER=1` to copy the approval link without opening a browser.
|
|
55
|
+
Deploy the matching backend/dashboard changes before releasing CLI `0.1.1`;
|
|
56
|
+
this patch requires the authenticated session endpoint and code-based operations.
|
|
49
57
|
|
|
50
58
|
Shoplifter-owned code is Apache-2.0; separately installed dependencies retain their
|
|
51
59
|
licenses. See `LICENSE`, `NOTICE` and `THIRD_PARTY_NOTICES`. This does not license
|
package/dist/auth.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ export declare function apiOrigin(): URL;
|
|
|
2
2
|
export declare function credentials(required?: boolean): Promise<{
|
|
3
3
|
token: string;
|
|
4
4
|
channelId: string;
|
|
5
|
+
channelCode?: string;
|
|
5
6
|
expiresAt: string;
|
|
6
7
|
} | undefined>;
|
|
7
|
-
export declare function
|
|
8
|
+
export declare function sessionChannel(token: string): Promise<string>;
|
|
9
|
+
export declare function login(channelCode?: string): Promise<void>;
|
|
8
10
|
export declare function logout(): Promise<void>;
|
|
9
11
|
export declare function syncDependencies(): Promise<void>;
|
package/dist/auth.js
CHANGED
|
@@ -33,15 +33,25 @@ export async function credentials(required = true) {
|
|
|
33
33
|
}
|
|
34
34
|
async function response(value) { if (!value.ok)
|
|
35
35
|
throw new Error(`CLI request failed (${value.status})`); return value.json(); }
|
|
36
|
-
export async function
|
|
36
|
+
export async function sessionChannel(token) {
|
|
37
|
+
const result = await response(await fetch(new URL('/shoplifter-cli/session', apiOrigin()), { headers: { authorization: 'Bearer ' + token }, signal: AbortSignal.timeout(15000) }));
|
|
38
|
+
if (typeof result.channelCode !== 'string' || !result.channelCode.trim())
|
|
39
|
+
throw new Error('Invalid CLI channel response');
|
|
40
|
+
return result.channelCode;
|
|
41
|
+
}
|
|
42
|
+
export async function login(channelCode) {
|
|
43
|
+
if (channelCode !== undefined && (!channelCode.trim() || channelCode !== channelCode.trim() || channelCode.length > 255))
|
|
44
|
+
throw new Error('A channel code is required');
|
|
37
45
|
const origin = apiOrigin();
|
|
38
46
|
const grant = await response(await fetch(new URL('/shoplifter-cli/device', origin), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ scopes: ['read', 'content', 'source'] }), signal: AbortSignal.timeout(15000) }));
|
|
39
47
|
const url = new URL(grant.verificationUri);
|
|
40
48
|
if (url.origin !== origin.origin || !/^[A-F0-9]{10}$/.test(grant.userCode))
|
|
41
49
|
throw new Error('Invalid CLI approval response');
|
|
42
50
|
url.searchParams.set('code', grant.userCode);
|
|
51
|
+
if (channelCode)
|
|
52
|
+
url.searchParams.set('channel', channelCode);
|
|
43
53
|
console.log(`Approve code ${grant.userCode} in your dashboard: ${url}`);
|
|
44
|
-
if (['darwin', 'linux'].includes(process.platform)) {
|
|
54
|
+
if (process.env.SHOPLIFTER_NO_BROWSER !== '1' && ['darwin', 'linux'].includes(process.platform)) {
|
|
45
55
|
const child = spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url.href], { stdio: 'ignore', detached: true });
|
|
46
56
|
child.on('error', () => undefined);
|
|
47
57
|
child.unref();
|
|
@@ -57,12 +67,17 @@ export async function login() {
|
|
|
57
67
|
|| !Number.isFinite(Date.parse(result.expiresAt)) || Date.parse(result.expiresAt) <= Date.now()
|
|
58
68
|
|| Date.parse(result.expiresAt) > Date.now() + 3600000)
|
|
59
69
|
throw new Error('Invalid CLI credential response');
|
|
70
|
+
const approvedCode = await sessionChannel(result.token);
|
|
71
|
+
if (channelCode && approvedCode !== channelCode) {
|
|
72
|
+
await response(await fetch(new URL('/shoplifter-cli/logout', origin), { method: 'POST', headers: { authorization: 'Bearer ' + result.token }, signal: AbortSignal.timeout(15000) }));
|
|
73
|
+
throw new Error('A different channel was approved; run shoplifter login with the intended channel code');
|
|
74
|
+
}
|
|
60
75
|
await mkdir(directory(), { recursive: true, mode: 0o700 });
|
|
61
76
|
await chmod(directory(), 0o700);
|
|
62
77
|
const temporary = filename() + '.' + randomUUID();
|
|
63
|
-
await writeFile(temporary, JSON.stringify({ token: result.token, channelId: result.channelId, expiresAt: result.expiresAt }), { mode: 0o600, flag: 'wx' });
|
|
78
|
+
await writeFile(temporary, JSON.stringify({ token: result.token, channelId: result.channelId, channelCode: approvedCode, expiresAt: result.expiresAt }), { mode: 0o600, flag: 'wx' });
|
|
64
79
|
await rename(temporary, filename());
|
|
65
|
-
console.log(`Authorized channel ${
|
|
80
|
+
console.log(`Authorized channel ${approvedCode} until ${result.expiresAt}`);
|
|
66
81
|
return;
|
|
67
82
|
}
|
|
68
83
|
throw new Error('CLI approval timed out');
|
package/dist/index.js
CHANGED
|
@@ -17,9 +17,19 @@ import { initialize, editProject, scaffold } from './author.js';
|
|
|
17
17
|
import { generate, validateImages } from './files.js';
|
|
18
18
|
import { publication, previewUpload } from './publication.js';
|
|
19
19
|
import { login, logout, syncDependencies } from './auth.js';
|
|
20
|
+
// commander-ts exits on async command failures without reporting the error.
|
|
21
|
+
async function reportFailure(work) {
|
|
22
|
+
try {
|
|
23
|
+
await work();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
20
30
|
let Shoplifter = class Shoplifter {
|
|
21
31
|
async files() { await previewUpload(); }
|
|
22
|
-
async login() { await login(); }
|
|
32
|
+
async login(channelCode) { await reportFailure(() => login(channelCode)); }
|
|
23
33
|
async logout() { await logout(); }
|
|
24
34
|
async dependencies(action) { if (action !== 'sync')
|
|
25
35
|
throw new Error('Choose dependencies sync'); await syncDependencies(); }
|
|
@@ -59,7 +69,7 @@ let Shoplifter = class Shoplifter {
|
|
|
59
69
|
throw new Error(`Astro exited with status ${code}`);
|
|
60
70
|
}
|
|
61
71
|
async publication(action, channel, revision, language) {
|
|
62
|
-
await publication(action, channel, revision, language);
|
|
72
|
+
await reportFailure(() => publication(action, channel, revision, language));
|
|
63
73
|
}
|
|
64
74
|
};
|
|
65
75
|
__decorate([
|
|
@@ -70,8 +80,9 @@ __decorate([
|
|
|
70
80
|
], Shoplifter.prototype, "files", null);
|
|
71
81
|
__decorate([
|
|
72
82
|
command(),
|
|
83
|
+
__param(0, optionalArg('channel-code')),
|
|
73
84
|
__metadata("design:type", Function),
|
|
74
|
-
__metadata("design:paramtypes", []),
|
|
85
|
+
__metadata("design:paramtypes", [String]),
|
|
75
86
|
__metadata("design:returntype", Promise)
|
|
76
87
|
], Shoplifter.prototype, "login", null);
|
|
77
88
|
__decorate([
|
|
@@ -196,7 +207,7 @@ __decorate([
|
|
|
196
207
|
], Shoplifter.prototype, "publication", null);
|
|
197
208
|
Shoplifter = __decorate([
|
|
198
209
|
program(),
|
|
199
|
-
version('0.1.
|
|
210
|
+
version('0.1.1'),
|
|
200
211
|
description('Author and publish independent Shoplifter presentations')
|
|
201
212
|
], Shoplifter);
|
|
202
213
|
if (Number(process.versions.node.split('.')[0]) !== 24)
|
package/dist/publication.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { apiOrigin, credentials } from './auth.js';
|
|
1
|
+
import { apiOrigin, credentials, sessionChannel } from './auth.js';
|
|
2
2
|
import { readProject } from './files.js';
|
|
3
3
|
import { collectUploadFiles } from './upload-files.js';
|
|
4
4
|
export async function previewUpload(directory = process.cwd()) {
|
|
@@ -11,13 +11,15 @@ export async function publication(action, channel, revision, languageCode) {
|
|
|
11
11
|
if (!['upload', 'stage', 'preview', 'preview-link', 'build', 'publish', 'status', 'logs', 'retry', 'retry-deployment', 'activate', 'rollback'].includes(action))
|
|
12
12
|
throw new Error('Unknown publication operation');
|
|
13
13
|
const base = apiOrigin(), session = await credentials(false);
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const channelCode = session ? await sessionChannel(session.token) : channel;
|
|
15
|
+
if (session && channel !== channelCode && channel !== session.channelId)
|
|
16
|
+
throw new Error('CLI session belongs to another channel; run shoplifter login with the intended channel code');
|
|
16
17
|
if (!session && !['localhost', '127.0.0.1', '[::1]'].includes(base.hostname))
|
|
17
18
|
throw new Error('Run shoplifter login before publishing remotely');
|
|
18
19
|
if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password)
|
|
19
20
|
throw new Error('Invalid API origin');
|
|
20
|
-
const
|
|
21
|
+
const selector = !session && /^\d+$/.test(channel) ? { channelId: channel } : { channelCode };
|
|
22
|
+
const body = { ...selector, revisionId: revision, ...(languageCode ? { languageCode } : {}) };
|
|
21
23
|
if (action === 'upload') {
|
|
22
24
|
body.project = await readProject();
|
|
23
25
|
body.files = (await collectUploadFiles(process.cwd(), body.project)).map(file => ({ path: file.path, bytes: file.bytes.toString('base64') }));
|
|
@@ -30,7 +32,7 @@ export async function publication(action, channel, revision, languageCode) {
|
|
|
30
32
|
const response = await fetch(new URL(session ? '/shoplifter-cli/operation' : '/shoplifter-development/' + operation, base), {
|
|
31
33
|
method: 'POST',
|
|
32
34
|
headers: { 'content-type': 'application/json', ...(session ? { authorization: 'Bearer ' + session.token } : {}) },
|
|
33
|
-
body: JSON.stringify(session ? {
|
|
35
|
+
body: JSON.stringify(session ? { channelCode, languageCode, action: operation, input: body } : body),
|
|
34
36
|
signal: AbortSignal.timeout(120000),
|
|
35
37
|
});
|
|
36
38
|
const result = await response.text();
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"path": "auth.d.ts",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "ff98d7cbeca5a9fc2985acf804d18fcfe18e54cdac63a4484367b3e1638d349a"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"path": "auth.js",
|
|
14
|
-
"sha256": "
|
|
14
|
+
"sha256": "282c5df5cc768f33e2f96052fb21758f74ca3875bc5c9c582c439a490ceed15e"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"path": "author.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"path": "index.js",
|
|
38
|
-
"sha256": "
|
|
38
|
+
"sha256": "4b85f13c321650f3a35e9cdee6df2eb8608d815750886b99a5850b224441e3c4"
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"path": "publication.d.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"path": "publication.js",
|
|
46
|
-
"sha256": "
|
|
46
|
+
"sha256": "c9d689a9cc90d61207c4f48c1489fafeda8d1016d7ec0e5a12fb885ddb4abac4"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"path": "runtime.d.ts",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"path": "runtime.js",
|
|
54
|
-
"sha256": "
|
|
54
|
+
"sha256": "f278ffc0d417812adbba6ac0dd742b0c5a3fc91c5089c310c9914dbb8a98f1d2"
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
"path": "upload-files.d.ts",
|
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
2
|
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
4
|
const maximumArchive = 12 * 1024 * 1024;
|
|
5
5
|
async function bytes(response, maximum) {
|
|
6
6
|
if (!response.ok || !response.body)
|
|
@@ -33,7 +33,10 @@ export async function downloadRuntime(origin) {
|
|
|
33
33
|
throw new Error('Invalid platform runtime archive');
|
|
34
34
|
return runtime;
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
// CLI-only patches do not require new platform runtime releases.
|
|
37
|
+
const { shoplifterRuntimeVersion: version } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
38
|
+
if (!/^\d+\.\d+\.\d+$/.test(version ?? ''))
|
|
39
|
+
throw new Error('Invalid platform runtime version');
|
|
37
40
|
const archive = async (kind) => {
|
|
38
41
|
const name = '@shoplifter/presentation-' + kind;
|
|
39
42
|
const metadata = JSON.parse((await bytes(await get(`https://registry.npmjs.org/${encodeURIComponent(name)}/${version}`), 512 * 1024)).toString());
|
package/package.json
CHANGED