@evcraddock/slug-cli 0.7.0 → 0.9.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 +27 -0
- package/dist/commands.js +267 -20
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -75,6 +75,33 @@ The imported body.
|
|
|
75
75
|
|
|
76
76
|
A relative `banner` path is resolved from the Markdown file, uploaded through the Slugkit media API under `posts/<slug>/banner.<extension>`, and inserted as the first Markdown image in the post body so standard Slugkit article previews render it. The command looks up the post by slug: it creates a missing post and updates an existing one, replacing the imported metadata, body, tags, and publication date. Re-importing a banner uploads the current file again to refresh the deterministic media object key.
|
|
77
77
|
|
|
78
|
+
## Source imports
|
|
79
|
+
|
|
80
|
+
Import link sources and their optional contacts and accounts from YAML with `slug --site <name> sources import <file.yaml>`. The file has one required top-level `sources` array. Each source requires `name` and may include `url`, `description`, `imageUrl`, `faviconUrl`, `contacts`, and `accounts`. Contacts require `name` and may include `url` and `accounts`. Accounts require `label` and `url` and may include `avatarUrl`, `kind`, `protocol`, `default`, and `sortOrder`.
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
sources:
|
|
84
|
+
- name: Example Blog
|
|
85
|
+
url: https://example.com
|
|
86
|
+
description: Example writing
|
|
87
|
+
accounts:
|
|
88
|
+
- label: Feed
|
|
89
|
+
url: https://example.com/feed.xml
|
|
90
|
+
kind: feed
|
|
91
|
+
protocol: rss
|
|
92
|
+
default: true
|
|
93
|
+
contacts:
|
|
94
|
+
- name: Jane Example
|
|
95
|
+
url: https://example.com/jane
|
|
96
|
+
accounts:
|
|
97
|
+
- label: Mastodon
|
|
98
|
+
url: https://social.example/@jane
|
|
99
|
+
kind: social
|
|
100
|
+
protocol: activitypub
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The CLI validates the complete file before sending requests. It creates contacts first, associates their returned IDs when creating each source, and then creates accounts with the returned contact or source owner ID. Sources may omit both `contacts` and `accounts`. An API failure stops the import; because the API does not provide a bulk transaction, records created by earlier successful requests remain.
|
|
104
|
+
|
|
78
105
|
Use `slug tag list` to list tags and post usage counts through the Slugkit tags API.
|
|
79
106
|
|
|
80
107
|
Example config:
|
package/dist/commands.js
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, basename, extname, join, resolve } from "node:path";
|
|
4
4
|
import AdmZip from "adm-zip";
|
|
5
|
+
import { parse as parseYaml } from "yaml";
|
|
5
6
|
import { SLUGKIT_API_MAJOR_VERSION, SLUGKIT_API_NAME, compareSlugkitApiVersions, normalizeApiBaseUrl, readSlugkitApiMajorVersion, } from "@evcraddock/slug-core";
|
|
6
7
|
import { isValidSiteName, readConfig, removeConfigSite, setConfigSiteApiBaseUrl, setConfigSiteApiKey, toDisplayConfig, writeConfig, } from "./config.js";
|
|
7
8
|
import { CliError, createInvalidUsageError, ExitCode } from "./errors.js";
|
|
@@ -36,6 +37,7 @@ Usage:
|
|
|
36
37
|
slug [--config <file>] sources list [--json]
|
|
37
38
|
slug [--config <file>] sources show <id> [--json]
|
|
38
39
|
slug [--config <file>] sources create --name <name> [--url <url>] [--description <text>] [--image-url <url>] [--favicon-url <url>] [--contact-id <id>]... [--json]
|
|
40
|
+
slug [--config <file>] sources import <file.yaml> [--json]
|
|
39
41
|
slug [--config <file>] sources edit <id> [--name <name>] [--url <url>] [--description <text>] [--image-url <url>] [--favicon-url <url>] [--contact-id <id>]... [--json]
|
|
40
42
|
slug [--config <file>] sources delete <id> [--json]
|
|
41
43
|
slug [--config <file>] accounts list [--owner-type contact|source|site] [--owner-id <id>] [--json]
|
|
@@ -344,7 +346,7 @@ function shouldExcludeTemplateEntry(relativePath, name) {
|
|
|
344
346
|
parts.includes("data") ||
|
|
345
347
|
parts.includes("coverage") ||
|
|
346
348
|
relativePath === "src/__tests__/dev-environment.test.ts" ||
|
|
347
|
-
relativePath === "src/federation/__tests__/follow.test.ts" ||
|
|
349
|
+
relativePath === "src/federation/__tests__/follow.integration.test.ts" ||
|
|
348
350
|
relativePath === ".env" ||
|
|
349
351
|
name.endsWith(".log") ||
|
|
350
352
|
name.endsWith(".tsbuildinfo"));
|
|
@@ -363,25 +365,24 @@ async function replaceGeneratedSitePlaceholders(directory, options) {
|
|
|
363
365
|
`intro: ${JSON.stringify(`${options.siteTitle} is ready for posts, feeds, and federation.`)}`,
|
|
364
366
|
],
|
|
365
367
|
]);
|
|
366
|
-
await
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
[
|
|
373
|
-
|
|
374
|
-
"Configure these values for your own local or production S3-compatible service before uploading media.",
|
|
375
|
-
],
|
|
376
|
-
[
|
|
377
|
-
"## In-repository development\n\nWhen working inside the Slugkit toolkit repository, start the full local dev environment from the repository root:\n\n```bash\nmake dev\n```\n\nFrom `template/site`, you can also run the app and Tailwind watcher directly in separate terminals:\n\n```bash\nnpm run dev\nnpm run css:watch\n```\n\nRepository-root workspace commands are useful while developing the template itself:\n\n```bash\nnpm run db:migrate --workspace @slugkit/template-site\nnpm run db:status --workspace @slugkit/template-site\nnpm run test --workspace @slugkit/template-site\n```\n\nStandalone initialized sites should use the non-workspace commands shown in the quick start section.",
|
|
378
|
-
"## Local development\n\nInstall Overmind if it is not already available, then run the app and Tailwind watcher together from this site directory:\n\n```bash\nmake dev\n```\n\nThe generated `Procfile.dev` runs `npm run dev` for the Hono app and `npm run css:watch` for Tailwind. Use the non-workspace commands shown in the quick start section for migrations and local checks.",
|
|
379
|
-
],
|
|
380
|
-
[
|
|
381
|
-
"## Related specs\n\n- [Web Spec 08: Template Setup Documentation](../../docs/web-specs/08-template-setup-documentation.md)\n- [CLI Spec 12: Site Init Command](../../docs/cli-specs/12-site-init-command.md)\n- [CLI Specs](../../docs/cli-specs/README.md)\n- [API Route Specs](../../docs/specs/README.md)",
|
|
382
|
-
"## Related Slugkit references\n\n- Slugkit API documentation is available from a running site at `/api/v1/docs`.\n- The CLI compatibility check is `slug doctor`.\n- The generated site marker is `.slugkit-site.json`.",
|
|
383
|
-
],
|
|
368
|
+
await writeGeneratedSiteReadme(directory, options);
|
|
369
|
+
}
|
|
370
|
+
async function writeGeneratedSiteReadme(directory, options) {
|
|
371
|
+
const sourcePath = join(directory, "README.generated.md");
|
|
372
|
+
const source = await readFile(sourcePath, "utf8");
|
|
373
|
+
const replacements = new Map([
|
|
374
|
+
["{{SITE_NAME}}", options.name],
|
|
375
|
+
["{{SITE_TITLE}}", options.siteTitle],
|
|
384
376
|
]);
|
|
377
|
+
let content = source;
|
|
378
|
+
for (const [placeholder, value] of replacements) {
|
|
379
|
+
if (!content.includes(placeholder)) {
|
|
380
|
+
throw createInvalidUsageError(`Generated README is missing placeholder: ${placeholder}`);
|
|
381
|
+
}
|
|
382
|
+
content = content.replaceAll(placeholder, value);
|
|
383
|
+
}
|
|
384
|
+
await writeFile(join(directory, "README.md"), content, "utf8");
|
|
385
|
+
await rm(sourcePath);
|
|
385
386
|
}
|
|
386
387
|
function replaceGeneratedSiteWorkspaceDependencies(value) {
|
|
387
388
|
if (!isRecord(value))
|
|
@@ -1668,12 +1669,14 @@ async function runSourcesCommand(context, args) {
|
|
|
1668
1669
|
return runSourcesShowCommand(context, args.slice(1));
|
|
1669
1670
|
case "create":
|
|
1670
1671
|
return runSourcesCreateCommand(context, args.slice(1));
|
|
1672
|
+
case "import":
|
|
1673
|
+
return runSourcesImportCommand(context, args.slice(1));
|
|
1671
1674
|
case "edit":
|
|
1672
1675
|
return runSourcesEditCommand(context, args.slice(1));
|
|
1673
1676
|
case "delete":
|
|
1674
1677
|
return runSourcesDeleteCommand(context, args.slice(1));
|
|
1675
1678
|
default:
|
|
1676
|
-
throw createInvalidUsageError("Usage: slug sources <list|show|create|edit|delete>");
|
|
1679
|
+
throw createInvalidUsageError("Usage: slug sources <list|show|create|import|edit|delete>");
|
|
1677
1680
|
}
|
|
1678
1681
|
}
|
|
1679
1682
|
async function runSourcesListCommand(context, args) {
|
|
@@ -1715,6 +1718,250 @@ async function runSourcesCreateCommand(context, args) {
|
|
|
1715
1718
|
writeSourceMutationOutput(context.writer, response, parsed.json);
|
|
1716
1719
|
return { exitCode: ExitCode.Ok };
|
|
1717
1720
|
}
|
|
1721
|
+
async function runSourcesImportCommand(context, args) {
|
|
1722
|
+
const { filePath, json } = readSourceImportArgs(args);
|
|
1723
|
+
const sources = await readSourceImportFile(filePath);
|
|
1724
|
+
const api = await createConfiguredApiContext(context);
|
|
1725
|
+
const result = {
|
|
1726
|
+
sources: [],
|
|
1727
|
+
totals: { sources: 0, contacts: 0, accounts: 0 },
|
|
1728
|
+
};
|
|
1729
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
1730
|
+
for (const [sourceIndex, source] of sources.entries()) {
|
|
1731
|
+
result.sources.push(await importSource(api.client, source, sourceIndex, result.totals));
|
|
1732
|
+
}
|
|
1733
|
+
if (json) {
|
|
1734
|
+
writeJson(context.writer, { data: result });
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
writeSourceImportSummary(context.writer, result.totals);
|
|
1738
|
+
}
|
|
1739
|
+
return { exitCode: ExitCode.Ok };
|
|
1740
|
+
}
|
|
1741
|
+
function readSourceImportArgs(args) {
|
|
1742
|
+
const json = args.includes("--json");
|
|
1743
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
1744
|
+
if (positional.length !== 1 || positional[0] === undefined || positional[0].startsWith("--")) {
|
|
1745
|
+
throw createInvalidUsageError("Usage: slug sources import <file.yaml> [--json]");
|
|
1746
|
+
}
|
|
1747
|
+
return { filePath: positional[0], json };
|
|
1748
|
+
}
|
|
1749
|
+
async function readSourceImportFile(filePath) {
|
|
1750
|
+
let content;
|
|
1751
|
+
try {
|
|
1752
|
+
content = await readFile(filePath, "utf8");
|
|
1753
|
+
}
|
|
1754
|
+
catch {
|
|
1755
|
+
throw createInvalidUsageError(`Cannot read source import file: ${filePath}`);
|
|
1756
|
+
}
|
|
1757
|
+
let document;
|
|
1758
|
+
try {
|
|
1759
|
+
document = parseYaml(content);
|
|
1760
|
+
}
|
|
1761
|
+
catch (error) {
|
|
1762
|
+
const message = error instanceof Error ? error.message : "Unable to parse YAML";
|
|
1763
|
+
throw createInvalidUsageError(`Invalid source import YAML: ${message}`);
|
|
1764
|
+
}
|
|
1765
|
+
return parseSourceImportDocument(document);
|
|
1766
|
+
}
|
|
1767
|
+
function parseSourceImportDocument(value) {
|
|
1768
|
+
const root = readSourceImportRecord(value, "document", ["sources"]);
|
|
1769
|
+
if (!Array.isArray(root.sources) || root.sources.length === 0) {
|
|
1770
|
+
throw createInvalidUsageError("sources must be a non-empty array");
|
|
1771
|
+
}
|
|
1772
|
+
return root.sources.map((source, index) => parseSourceImportSource(source, `sources[${index}]`));
|
|
1773
|
+
}
|
|
1774
|
+
function parseSourceImportSource(value, path) {
|
|
1775
|
+
const record = readSourceImportRecord(value, path, [
|
|
1776
|
+
"name",
|
|
1777
|
+
"url",
|
|
1778
|
+
"description",
|
|
1779
|
+
"imageUrl",
|
|
1780
|
+
"faviconUrl",
|
|
1781
|
+
"contacts",
|
|
1782
|
+
"accounts",
|
|
1783
|
+
]);
|
|
1784
|
+
return {
|
|
1785
|
+
name: readSourceImportRequiredString(record, "name", path),
|
|
1786
|
+
...readSourceImportOptionalUrl(record, "url", path, ["http:", "https:"]),
|
|
1787
|
+
...readSourceImportOptionalString(record, "description", path),
|
|
1788
|
+
...readSourceImportOptionalUrl(record, "imageUrl", path, ["http:", "https:"]),
|
|
1789
|
+
...readSourceImportOptionalUrl(record, "faviconUrl", path, ["http:", "https:"]),
|
|
1790
|
+
contacts: readSourceImportArray(record, "contacts", path, parseSourceImportContact),
|
|
1791
|
+
accounts: readSourceImportArray(record, "accounts", path, parseSourceImportAccount),
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function parseSourceImportContact(value, path) {
|
|
1795
|
+
const record = readSourceImportRecord(value, path, ["name", "url", "accounts"]);
|
|
1796
|
+
return {
|
|
1797
|
+
name: readSourceImportRequiredString(record, "name", path),
|
|
1798
|
+
...readSourceImportOptionalUrl(record, "url", path, ["http:", "https:"]),
|
|
1799
|
+
accounts: readSourceImportArray(record, "accounts", path, parseSourceImportAccount),
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
function parseSourceImportAccount(value, path) {
|
|
1803
|
+
const record = readSourceImportRecord(value, path, [
|
|
1804
|
+
"label",
|
|
1805
|
+
"url",
|
|
1806
|
+
"avatarUrl",
|
|
1807
|
+
"kind",
|
|
1808
|
+
"protocol",
|
|
1809
|
+
"default",
|
|
1810
|
+
"sortOrder",
|
|
1811
|
+
]);
|
|
1812
|
+
const account = {
|
|
1813
|
+
label: readSourceImportRequiredString(record, "label", path),
|
|
1814
|
+
url: readSourceImportRequiredUrl(record, "url", path, ["http:", "https:", "mailto:"]),
|
|
1815
|
+
...readSourceImportOptionalUrl(record, "avatarUrl", path, ["http:", "https:"]),
|
|
1816
|
+
...readSourceImportOptionalString(record, "kind", path),
|
|
1817
|
+
...readSourceImportOptionalString(record, "protocol", path),
|
|
1818
|
+
};
|
|
1819
|
+
if ("default" in record)
|
|
1820
|
+
account.default = readSourceImportBoolean(record, "default", path);
|
|
1821
|
+
if ("sortOrder" in record)
|
|
1822
|
+
account.sortOrder = readSourceImportInteger(record, "sortOrder", path);
|
|
1823
|
+
return account;
|
|
1824
|
+
}
|
|
1825
|
+
function readSourceImportRecord(value, path, allowedFields) {
|
|
1826
|
+
if (!isRecord(value))
|
|
1827
|
+
throw createInvalidUsageError(`${path} must be an object`);
|
|
1828
|
+
for (const key of Object.keys(value)) {
|
|
1829
|
+
if (!allowedFields.includes(key)) {
|
|
1830
|
+
throw createInvalidUsageError(`${path}.${key} is not supported`);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return value;
|
|
1834
|
+
}
|
|
1835
|
+
function readSourceImportRequiredString(record, key, path) {
|
|
1836
|
+
const value = record[key];
|
|
1837
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1838
|
+
throw createInvalidUsageError(`${path}.${key} must be a non-empty string`);
|
|
1839
|
+
}
|
|
1840
|
+
return value;
|
|
1841
|
+
}
|
|
1842
|
+
function readSourceImportOptionalString(record, key, path) {
|
|
1843
|
+
if (!(key in record))
|
|
1844
|
+
return {};
|
|
1845
|
+
if (typeof record[key] !== "string") {
|
|
1846
|
+
throw createInvalidUsageError(`${path}.${key} must be a string`);
|
|
1847
|
+
}
|
|
1848
|
+
return { [key]: record[key] };
|
|
1849
|
+
}
|
|
1850
|
+
function readSourceImportRequiredUrl(record, key, path, protocols) {
|
|
1851
|
+
const value = readSourceImportRequiredString(record, key, path);
|
|
1852
|
+
assertSourceImportUrl(value, `${path}.${key}`, protocols);
|
|
1853
|
+
return value;
|
|
1854
|
+
}
|
|
1855
|
+
function readSourceImportOptionalUrl(record, key, path, protocols) {
|
|
1856
|
+
if (!(key in record))
|
|
1857
|
+
return {};
|
|
1858
|
+
const value = readSourceImportRequiredString(record, key, path);
|
|
1859
|
+
assertSourceImportUrl(value, `${path}.${key}`, protocols);
|
|
1860
|
+
return { [key]: value };
|
|
1861
|
+
}
|
|
1862
|
+
function assertSourceImportUrl(value, path, protocols) {
|
|
1863
|
+
try {
|
|
1864
|
+
const url = new URL(value);
|
|
1865
|
+
if (!protocols.includes(url.protocol))
|
|
1866
|
+
throw new Error("Unsupported protocol");
|
|
1867
|
+
}
|
|
1868
|
+
catch {
|
|
1869
|
+
throw createInvalidUsageError(`${path} must be a valid ${protocols.join(" or ")} URL`);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
function readSourceImportArray(record, key, path, parseItem) {
|
|
1873
|
+
if (!(key in record))
|
|
1874
|
+
return [];
|
|
1875
|
+
const value = record[key];
|
|
1876
|
+
if (!Array.isArray(value))
|
|
1877
|
+
throw createInvalidUsageError(`${path}.${key} must be an array`);
|
|
1878
|
+
return value.map((item, index) => parseItem(item, `${path}.${key}[${index}]`));
|
|
1879
|
+
}
|
|
1880
|
+
function readSourceImportBoolean(record, key, path) {
|
|
1881
|
+
if (typeof record[key] !== "boolean") {
|
|
1882
|
+
throw createInvalidUsageError(`${path}.${key} must be a boolean`);
|
|
1883
|
+
}
|
|
1884
|
+
return record[key];
|
|
1885
|
+
}
|
|
1886
|
+
function readSourceImportInteger(record, key, path) {
|
|
1887
|
+
if (typeof record[key] !== "number" || !Number.isInteger(record[key])) {
|
|
1888
|
+
throw createInvalidUsageError(`${path}.${key} must be an integer`);
|
|
1889
|
+
}
|
|
1890
|
+
return record[key];
|
|
1891
|
+
}
|
|
1892
|
+
async function importSource(client, source, sourceIndex, totals) {
|
|
1893
|
+
const contacts = [];
|
|
1894
|
+
for (const [contactIndex, contact] of source.contacts.entries()) {
|
|
1895
|
+
const response = await requestSourceImport(client, "/contacts", { name: contact.name, ...(contact.url === undefined ? {} : { url: contact.url }) }, `sources[${sourceIndex}].contacts[${contactIndex}]`);
|
|
1896
|
+
contacts.push({ input: contact, document: response.data, accountIds: [] });
|
|
1897
|
+
totals.contacts += 1;
|
|
1898
|
+
}
|
|
1899
|
+
const sourceResponse = await requestSourceImport(client, "/sources", createImportedSourceBody(source, contacts.map((contact) => contact.document.id)), `sources[${sourceIndex}]`);
|
|
1900
|
+
totals.sources += 1;
|
|
1901
|
+
for (const [contactIndex, contact] of contacts.entries()) {
|
|
1902
|
+
contact.accountIds.push(...(await importAccounts(client, contact.input.accounts, "contact", contact.document.id, `sources[${sourceIndex}].contacts[${contactIndex}].accounts`, totals)));
|
|
1903
|
+
}
|
|
1904
|
+
const accountIds = await importAccounts(client, source.accounts, "source", sourceResponse.data.id, `sources[${sourceIndex}].accounts`, totals);
|
|
1905
|
+
return {
|
|
1906
|
+
id: sourceResponse.data.id,
|
|
1907
|
+
name: sourceResponse.data.name,
|
|
1908
|
+
contacts: contacts.map((contact) => ({
|
|
1909
|
+
id: contact.document.id,
|
|
1910
|
+
name: contact.document.name,
|
|
1911
|
+
accountIds: contact.accountIds,
|
|
1912
|
+
})),
|
|
1913
|
+
accountIds,
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
function createImportedSourceBody(source, contactIds) {
|
|
1917
|
+
return {
|
|
1918
|
+
name: source.name,
|
|
1919
|
+
...(source.url === undefined ? {} : { url: source.url }),
|
|
1920
|
+
...(source.description === undefined ? {} : { description: source.description }),
|
|
1921
|
+
...(source.imageUrl === undefined ? {} : { imageUrl: source.imageUrl }),
|
|
1922
|
+
...(source.faviconUrl === undefined ? {} : { faviconUrl: source.faviconUrl }),
|
|
1923
|
+
...(contactIds.length === 0 ? {} : { contactIds }),
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
async function importAccounts(client, accounts, ownerType, ownerId, path, totals) {
|
|
1927
|
+
const ids = [];
|
|
1928
|
+
for (const [index, account] of accounts.entries()) {
|
|
1929
|
+
const response = await requestSourceImport(client, "/accounts", createImportedAccountBody(account, ownerType, ownerId), `${path}[${index}]`);
|
|
1930
|
+
ids.push(response.data.id);
|
|
1931
|
+
totals.accounts += 1;
|
|
1932
|
+
}
|
|
1933
|
+
return ids;
|
|
1934
|
+
}
|
|
1935
|
+
function createImportedAccountBody(account, ownerType, ownerId) {
|
|
1936
|
+
return {
|
|
1937
|
+
ownerType,
|
|
1938
|
+
ownerId,
|
|
1939
|
+
label: account.label,
|
|
1940
|
+
url: account.url,
|
|
1941
|
+
...(account.avatarUrl === undefined ? {} : { avatarUrl: account.avatarUrl }),
|
|
1942
|
+
...(account.kind === undefined ? {} : { kind: account.kind }),
|
|
1943
|
+
...(account.protocol === undefined ? {} : { protocol: account.protocol }),
|
|
1944
|
+
...(account.default === undefined ? {} : { isDefault: account.default }),
|
|
1945
|
+
...(account.sortOrder === undefined ? {} : { sortOrder: account.sortOrder }),
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
async function requestSourceImport(client, path, body, importPath) {
|
|
1949
|
+
try {
|
|
1950
|
+
return await client.requestJson({ method: "POST", path, body });
|
|
1951
|
+
}
|
|
1952
|
+
catch (error) {
|
|
1953
|
+
if (error instanceof CliError) {
|
|
1954
|
+
throw new CliError(`Failed to import ${importPath}: ${error.message}`, error.exitCode, error.status);
|
|
1955
|
+
}
|
|
1956
|
+
throw error;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function writeSourceImportSummary(writer, totals) {
|
|
1960
|
+
writer.stdout(`Imported ${formatImportCount(totals.sources, "source")}, ${formatImportCount(totals.contacts, "contact")}, and ${formatImportCount(totals.accounts, "account")}.`);
|
|
1961
|
+
}
|
|
1962
|
+
function formatImportCount(count, label) {
|
|
1963
|
+
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
1964
|
+
}
|
|
1718
1965
|
async function runSourcesEditCommand(context, args) {
|
|
1719
1966
|
const idArg = args[0];
|
|
1720
1967
|
if (idArg === undefined || idArg.startsWith("--")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evcraddock/slug-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Command-line tool for Slugkit sites.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@evcraddock/slug-core": "0.1.1",
|
|
30
|
-
"adm-zip": "^0.5.18"
|
|
30
|
+
"adm-zip": "^0.5.18",
|
|
31
|
+
"yaml": "^2.9.0"
|
|
31
32
|
}
|
|
32
33
|
}
|