@localpulse/cli 0.0.2 → 0.0.3
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/package.json +1 -1
- package/src/index.ts +65 -35
- package/src/lib/research-schema.ts +32 -7
- package/src/lib/upload-client.ts +2 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getCredentialsPath,
|
|
14
14
|
getDefaultApiUrl,
|
|
15
15
|
resolveApiUrl,
|
|
16
|
+
resolveToken,
|
|
16
17
|
} from "./lib/credentials";
|
|
17
18
|
import { loginWithToken } from "./lib/login";
|
|
18
19
|
import { exitCodeForError, printError } from "./lib/output";
|
|
@@ -28,8 +29,7 @@ import {
|
|
|
28
29
|
type DraftResult,
|
|
29
30
|
uploadPoster,
|
|
30
31
|
createDraft,
|
|
31
|
-
|
|
32
|
-
detectMediaType,
|
|
32
|
+
verifyCliToken,
|
|
33
33
|
} from "./lib/upload-client";
|
|
34
34
|
import packageJson from "../package.json";
|
|
35
35
|
|
|
@@ -81,12 +81,14 @@ async function runAuth(parsed: ReturnType<typeof parseArgv>): Promise<void> {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const subcommand = parsed.positionals[0];
|
|
84
|
-
if (!subcommand ||
|
|
85
|
-
throw new Error("Usage: localpulse auth <login|logout>. Run `localpulse auth --help` for details.");
|
|
84
|
+
if (!subcommand || !["login", "logout", "status"].includes(subcommand)) {
|
|
85
|
+
throw new Error("Usage: localpulse auth <login|logout|status>. Run `localpulse auth --help` for details.");
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (subcommand === "login") {
|
|
89
89
|
await runAuthLogin(parsed);
|
|
90
|
+
} else if (subcommand === "status") {
|
|
91
|
+
await runAuthStatus(parsed);
|
|
90
92
|
} else {
|
|
91
93
|
await runAuthLogout(parsed);
|
|
92
94
|
}
|
|
@@ -125,6 +127,45 @@ async function runAuthLogout(parsed: ReturnType<typeof parseArgv>): Promise<void
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
async function runAuthStatus(parsed: ReturnType<typeof parseArgv>): Promise<void> {
|
|
131
|
+
const jsonOutput = hasOption(parsed, "json");
|
|
132
|
+
const token = await resolveToken();
|
|
133
|
+
const apiUrl = await resolveApiUrl();
|
|
134
|
+
const credentialsPath = getCredentialsPath();
|
|
135
|
+
const source = process.env.LP_TOKEN?.trim() ? "LP_TOKEN" : credentialsPath;
|
|
136
|
+
|
|
137
|
+
if (!token) {
|
|
138
|
+
if (jsonOutput) {
|
|
139
|
+
stdout.write(`${JSON.stringify({ authenticated: false, source: null, reason: "no_token" })}\n`);
|
|
140
|
+
} else {
|
|
141
|
+
stdout.write("Not authenticated. Run `localpulse auth login` or set LP_TOKEN.\n");
|
|
142
|
+
}
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const result = await verifyCliToken(apiUrl, token);
|
|
149
|
+
if (jsonOutput) {
|
|
150
|
+
stdout.write(`${JSON.stringify({ authenticated: true, email: result.email, api_url: apiUrl, source })}\n`);
|
|
151
|
+
} else {
|
|
152
|
+
stdout.write(`Authenticated as ${result.email}\n`);
|
|
153
|
+
stdout.write(` API: ${apiUrl}\n`);
|
|
154
|
+
stdout.write(` Credentials: ${source}\n`);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const message = error instanceof Error ? error.message : "Token verification failed";
|
|
158
|
+
if (jsonOutput) {
|
|
159
|
+
stdout.write(`${JSON.stringify({ authenticated: false, api_url: apiUrl, source, error: message })}\n`);
|
|
160
|
+
} else {
|
|
161
|
+
stdout.write(`Authentication failed: ${message}\n`);
|
|
162
|
+
stdout.write(` API: ${apiUrl}\n`);
|
|
163
|
+
stdout.write(` Credentials: ${source}\n`);
|
|
164
|
+
}
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
128
169
|
async function runIngest(parsed: ReturnType<typeof parseArgv>): Promise<void> {
|
|
129
170
|
if (hasOption(parsed, "help")) {
|
|
130
171
|
stdout.write(ingestHelp());
|
|
@@ -197,13 +238,16 @@ async function runIngest(parsed: ReturnType<typeof parseArgv>): Promise<void> {
|
|
|
197
238
|
return;
|
|
198
239
|
}
|
|
199
240
|
|
|
200
|
-
// Default: draft flow
|
|
241
|
+
// Default: draft flow — create draft then submit to ingestion pipeline
|
|
201
242
|
const draft = await runDraftIngest(apiUrl, token, file, uploadOptions);
|
|
243
|
+
process.stderr.write("Submitting to ingestion pipeline...\n");
|
|
244
|
+
const result = await uploadPoster(apiUrl, token, { ...uploadOptions, draftId: draft.id }) as UploadPosterResult;
|
|
202
245
|
if (jsonOutput) {
|
|
203
|
-
|
|
204
|
-
stdout.write(`${JSON.stringify({ draft_id: draft.id, status: draft.status, edit_url: `${baseUrl}/publish/edit/${draft.id}` })}\n`);
|
|
246
|
+
stdout.write(`${JSON.stringify({ draft_id: draft.id, ...result })}\n`);
|
|
205
247
|
} else {
|
|
206
|
-
|
|
248
|
+
printIngestResult(result);
|
|
249
|
+
const baseUrl = toFrontendBaseUrl(apiUrl);
|
|
250
|
+
stdout.write(`Draft: ${baseUrl}/publish/edit/${draft.id}\n`);
|
|
207
251
|
}
|
|
208
252
|
}
|
|
209
253
|
|
|
@@ -234,36 +278,11 @@ async function runDraftIngest(
|
|
|
234
278
|
|
|
235
279
|
process.stderr.write("Creating draft...\n");
|
|
236
280
|
const draft = await createDraft(apiUrl, token, options);
|
|
237
|
-
|
|
238
281
|
process.stderr.write(`Draft created: ${draft.id}\n`);
|
|
239
|
-
process.stderr.write("Uploading primary media...\n");
|
|
240
|
-
await uploadDraftMedia(apiUrl, token, draft.id, primaryFile, {
|
|
241
|
-
mediaType: detectMediaType(primaryFile),
|
|
242
|
-
sortOrder: 0,
|
|
243
|
-
isPrimary: true,
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
if (options.extraMedia?.length) {
|
|
247
|
-
for (const [index, path] of options.extraMedia.entries()) {
|
|
248
|
-
process.stderr.write(`Uploading extra media ${index + 1}...\n`);
|
|
249
|
-
await uploadDraftMedia(apiUrl, token, draft.id, path, {
|
|
250
|
-
mediaType: detectMediaType(path),
|
|
251
|
-
sortOrder: index + 1,
|
|
252
|
-
isPrimary: false,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
282
|
|
|
257
283
|
return draft;
|
|
258
284
|
}
|
|
259
285
|
|
|
260
|
-
function printDraftResult(draft: DraftResult, apiUrl: string): void {
|
|
261
|
-
const baseUrl = toFrontendBaseUrl(apiUrl);
|
|
262
|
-
stdout.write(`\nDraft saved: ${draft.id}\n`);
|
|
263
|
-
stdout.write("Open the publish dashboard to review and submit:\n");
|
|
264
|
-
stdout.write(` ${baseUrl}/publish/edit/${draft.id}\n`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
286
|
function printIngestResult(result: UploadPosterResult): void {
|
|
268
287
|
stdout.write(`${result.message}\n`);
|
|
269
288
|
if (result.run_id) {
|
|
@@ -461,10 +480,12 @@ function authHelp(): string {
|
|
|
461
480
|
Commands:
|
|
462
481
|
login Authenticate with a Local Pulse CLI token
|
|
463
482
|
logout Remove stored credentials
|
|
483
|
+
status Check authentication status and verify token
|
|
464
484
|
|
|
465
485
|
Examples:
|
|
466
486
|
localpulse auth login --token lp_cli_...
|
|
467
|
-
|
|
487
|
+
localpulse auth status
|
|
488
|
+
localpulse auth status --json
|
|
468
489
|
localpulse auth logout
|
|
469
490
|
`;
|
|
470
491
|
}
|
|
@@ -502,7 +523,16 @@ Research payload:
|
|
|
502
523
|
--generate-skeleton Print an example research template and exit
|
|
503
524
|
|
|
504
525
|
The payload is JSON with five top-level sections. All are optional,
|
|
505
|
-
but
|
|
526
|
+
but richer research produces much better event listings.
|
|
527
|
+
|
|
528
|
+
Quality checklist (aim to fill as many as possible):
|
|
529
|
+
✓ Performer socials: Instagram, Spotify, Bandcamp, SoundCloud, RA, website
|
|
530
|
+
✓ Organizer socials: Instagram, website, RA promoter page
|
|
531
|
+
✓ Venue google_place_id (search Google Maps → share → extract place ID)
|
|
532
|
+
✓ Multiple event.urls: venue page, RA, Facebook event, artist page
|
|
533
|
+
✓ Embed URLs in context: Spotify album/track links, YouTube videos
|
|
534
|
+
✓ Performer context: recent releases, labels, residencies, tour info
|
|
535
|
+
✓ All support acts as separate performer entries
|
|
506
536
|
|
|
507
537
|
performers[] Who is performing or presenting
|
|
508
538
|
.name Full name (required per performer)
|
|
@@ -60,29 +60,54 @@ export function generateResearchSkeleton(): ResearchPayload {
|
|
|
60
60
|
name: "DJ Nobu",
|
|
61
61
|
type: "DJ",
|
|
62
62
|
genre: "techno",
|
|
63
|
-
socials: [
|
|
64
|
-
|
|
63
|
+
socials: [
|
|
64
|
+
"https://instagram.com/djnobu",
|
|
65
|
+
"https://ra.co/dj/djnobu",
|
|
66
|
+
"https://soundcloud.com/djnobu",
|
|
67
|
+
"https://djnobu.bandcamp.com",
|
|
68
|
+
"https://open.spotify.com/artist/0abc123",
|
|
69
|
+
],
|
|
70
|
+
context:
|
|
71
|
+
"Berlin-based Japanese DJ, known for long hypnotic sets. Resident at Future Terror (Tokyo). Recent release: 'Prism' on Bitta (2025).",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "Nene H",
|
|
75
|
+
type: "DJ",
|
|
76
|
+
genre: "experimental electronic",
|
|
77
|
+
socials: [
|
|
78
|
+
"https://instagram.com/nenehmusic",
|
|
79
|
+
"https://ra.co/dj/neneh",
|
|
80
|
+
],
|
|
81
|
+
context: "Support. Tehran-born, Berlin-based. Known for deconstructed club music.",
|
|
65
82
|
},
|
|
66
83
|
],
|
|
67
84
|
organizer: {
|
|
68
85
|
name: "Dekmantel",
|
|
69
|
-
socials: [
|
|
70
|
-
|
|
86
|
+
socials: [
|
|
87
|
+
"https://instagram.com/daboratorium",
|
|
88
|
+
"https://www.dekmantel.com",
|
|
89
|
+
"https://ra.co/promoters/16478",
|
|
90
|
+
],
|
|
91
|
+
context: "Amsterdam-based collective, running events since 2007. Annual festival + club nights.",
|
|
71
92
|
},
|
|
72
93
|
venue: {
|
|
73
94
|
name: "Shelter Amsterdam",
|
|
74
95
|
city: "Amsterdam",
|
|
75
96
|
google_place_id: "",
|
|
76
|
-
context: "Underground club beneath A'DAM Tower, 350 capacity, one room",
|
|
97
|
+
context: "Underground club beneath A'DAM Tower, 350 capacity, one room, Funktion-One sound.",
|
|
77
98
|
},
|
|
78
99
|
event: {
|
|
79
100
|
title: "Shelter Presents: DJ Nobu",
|
|
80
101
|
date: "2026-03-14T22:00:00+01:00",
|
|
81
102
|
type: "club night",
|
|
82
103
|
price: "€15-25",
|
|
83
|
-
urls: [
|
|
104
|
+
urls: [
|
|
105
|
+
"https://ra.co/events/1234567",
|
|
106
|
+
"https://www.dekmantel.com/events/shelter-nobu",
|
|
107
|
+
],
|
|
84
108
|
ticket_url: "https://tickets.example.com/shelter-nobu",
|
|
85
|
-
context:
|
|
109
|
+
context:
|
|
110
|
+
"Part of ADE week. Doors at 22:00. Cash only at door. DJ Nobu plays an extended 4-hour set.",
|
|
86
111
|
},
|
|
87
112
|
context: "",
|
|
88
113
|
};
|
package/src/lib/upload-client.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type IngestUploadOptions = {
|
|
|
23
23
|
venue?: string;
|
|
24
24
|
extraMedia?: string[];
|
|
25
25
|
dryRun?: boolean;
|
|
26
|
+
draftId?: string;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
export type UploadDryRunResult = {
|
|
@@ -88,6 +89,7 @@ export async function uploadPoster(
|
|
|
88
89
|
if (options.city) {
|
|
89
90
|
setOptionalField(form, "venue_city", options.city);
|
|
90
91
|
}
|
|
92
|
+
setOptionalField(form, "draft_id", options.draftId);
|
|
91
93
|
|
|
92
94
|
if (options.extraMedia?.length) {
|
|
93
95
|
const mediaTypes: string[] = [];
|