@homespunapps/cli 1.6.37 → 1.6.39
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 +61 -29
- package/dist/commands/data.js +50 -2
- package/dist/commands/template.js +28 -8
- package/dist/help-catalog.js +32 -3
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -16,18 +16,22 @@ npx @homespunapps/cli <command>
|
|
|
16
16
|
|
|
17
17
|
The binary is `homespun`.
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Quickstart
|
|
20
20
|
|
|
21
|
-
Register once, then
|
|
22
|
-
relay, opens it in your browser, and prints the structured event back in your
|
|
23
|
-
terminal the moment you interact (the demo app is cleaned up on exit):
|
|
21
|
+
Register once, then deploy:
|
|
24
22
|
|
|
25
23
|
```sh
|
|
26
24
|
npx @homespunapps/cli agent register --name "my-agent" # one-time, hosted relay
|
|
27
|
-
npx @homespunapps/cli
|
|
25
|
+
npx @homespunapps/cli deploy ./my-app # Node 20+, reads ./my-app/index.html + manifest.json
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
`agent register` uses browser approval by default: it prints a link and a short
|
|
29
|
+
code, you approve on any device, and the agent comes out already bound to your
|
|
30
|
+
account and ready to deploy. See [Setup](#setup) for the direct-registration
|
|
31
|
+
path and the one-time `agent claim` it needs.
|
|
32
|
+
|
|
33
|
+
New apps are private by default: only you and the people you invite can open
|
|
34
|
+
them. Pass `--visibility link` or `--visibility public` to share wider.
|
|
31
35
|
|
|
32
36
|
## Setup
|
|
33
37
|
|
|
@@ -53,41 +57,69 @@ Override per-invocation with `--url <url>` and `--api-key <key>`.
|
|
|
53
57
|
|
|
54
58
|
## Commands
|
|
55
59
|
|
|
56
|
-
Uniform `homespun <noun> <verb> [options]
|
|
60
|
+
Uniform `homespun <noun> <verb> [options]` (`data` takes its collection
|
|
61
|
+
before the verb: `homespun data <app> <collection> <verb>`).
|
|
62
|
+
|
|
63
|
+
App commands, each operating on a deployed app:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
homespun deploy [dir|file] Create or redeploy an app (POST /v1/apps, or
|
|
67
|
+
/v1/apps/:id/versions with --app)
|
|
68
|
+
homespun apps <verb> App lifecycle: list, show, update, share-link,
|
|
69
|
+
delete, wake, domain, watch (the change feed as
|
|
70
|
+
JSON-lines)
|
|
71
|
+
homespun data <app> <collection> <verb>
|
|
72
|
+
Collection row CRUD: list, get, upsert, update,
|
|
73
|
+
delete, purge, import, retention
|
|
74
|
+
homespun members <verb> App membership: add, list, set-role, remove, roles
|
|
75
|
+
homespun grants <verb> Grant-link management: mint, list, revoke
|
|
76
|
+
homespun ingest <verb> Inbound catch-hook management: list, rotate,
|
|
77
|
+
signing-secret, backfill
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Other command groups:
|
|
57
81
|
|
|
58
82
|
```
|
|
59
|
-
homespun
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
homespun
|
|
65
|
-
|
|
66
|
-
homespun
|
|
67
|
-
homespun
|
|
68
|
-
homespun
|
|
69
|
-
homespun
|
|
70
|
-
|
|
71
|
-
homespun
|
|
72
|
-
|
|
73
|
-
homespun config
|
|
74
|
-
|
|
83
|
+
homespun publisher <verb> Your community publisher identity: claim, show,
|
|
84
|
+
update, set-trust
|
|
85
|
+
homespun template <verb> Community marketplace templates: publish,
|
|
86
|
+
unpublish, config-contract, install, list-pending,
|
|
87
|
+
show, approve, reject
|
|
88
|
+
homespun review <verb> Community template reviews: create, respond,
|
|
89
|
+
report, remove, unhold
|
|
90
|
+
homespun key <verb> Your agent's own API key: list, mint, revoke
|
|
91
|
+
homespun taste <verb> Your agent's UI taste notes: get, set, clear
|
|
92
|
+
homespun feedback <verb> One-shot feedback to the relay operator: create, list
|
|
93
|
+
homespun attachment <verb> Binary attachments: upload, download, show, list,
|
|
94
|
+
delete, token mint | revoke | list
|
|
95
|
+
homespun agent <verb> This agent's identity on the relay: register,
|
|
96
|
+
claim, set-key, logout
|
|
97
|
+
homespun config <verb> CLI config and profile management: show, list,
|
|
98
|
+
use, add, rm
|
|
99
|
+
homespun skill <verb> The relay's SKILL.md: show, version
|
|
75
100
|
```
|
|
76
101
|
|
|
77
102
|
Run `homespun <noun> --help` for that noun's verbs, and
|
|
78
|
-
`homespun <noun> <verb> --help` for verb-specific options.
|
|
103
|
+
`homespun <noun> <verb> --help` for verb-specific options. The same table
|
|
104
|
+
also drives the generated [CLI reference](https://docs.homespun.dev/agents/cli-reference/).
|
|
79
105
|
|
|
80
106
|
## Output
|
|
81
107
|
|
|
82
|
-
stdout is
|
|
83
|
-
|
|
108
|
+
stdout is JSON. Errors go to stderr as `{"error":{"code","message"}}` with a
|
|
109
|
+
non-zero exit. A deploy, then a read of the data it wrote back:
|
|
84
110
|
|
|
85
111
|
```sh
|
|
86
|
-
|
|
87
|
-
|
|
112
|
+
homespun deploy ./grocery-list
|
|
113
|
+
# -> { app_id, slug, url, version, visibility: "private", created: true }
|
|
114
|
+
|
|
115
|
+
homespun data grocery-list items upsert --data '{"name":"Milk","checked":false}'
|
|
116
|
+
# -> { row: { key, data: { name: "Milk", checked: false }, version: 1, author, created_at, updated_at } }
|
|
117
|
+
|
|
118
|
+
homespun data grocery-list items list
|
|
119
|
+
# -> { rows: [...], next_cursor, has_more }
|
|
88
120
|
```
|
|
89
121
|
|
|
90
122
|
## Links
|
|
91
123
|
|
|
92
124
|
- Docs: <https://docs.homespun.dev>
|
|
93
|
-
- License: MIT
|
|
125
|
+
- License: [MIT](LICENSE)
|
package/dist/commands/data.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function runData(args) {
|
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
if (!appArg || !collection || !verb) {
|
|
21
|
-
fail("usage: homespun data <app> <collection> <list|get|upsert|update|delete|purge|import|retention>", "invalid_args");
|
|
21
|
+
fail("usage: homespun data <app> <collection> <list|get|upsert|update|delete|restore|deleted|purge|import|retention>", "invalid_args");
|
|
22
22
|
}
|
|
23
23
|
const sub = {
|
|
24
24
|
positionals: args.positionals.slice(3),
|
|
@@ -39,6 +39,10 @@ export async function runData(args) {
|
|
|
39
39
|
return runUpdate(appArg, collection, sub);
|
|
40
40
|
case "delete":
|
|
41
41
|
return runDelete(appArg, collection, sub);
|
|
42
|
+
case "restore":
|
|
43
|
+
return runRestore(appArg, collection, sub);
|
|
44
|
+
case "deleted":
|
|
45
|
+
return runListDeleted(appArg, collection, sub);
|
|
42
46
|
case "purge":
|
|
43
47
|
return runPurge(appArg, collection, sub);
|
|
44
48
|
case "import":
|
|
@@ -46,7 +50,7 @@ export async function runData(args) {
|
|
|
46
50
|
case "retention":
|
|
47
51
|
return runRetention(appArg, collection, sub);
|
|
48
52
|
default:
|
|
49
|
-
fail(`unknown verb '${verb}': homespun data <app> <collection> <list|get|upsert|update|delete|purge|import|retention>`, "invalid_args");
|
|
53
|
+
fail(`unknown verb '${verb}': homespun data <app> <collection> <list|get|upsert|update|delete|restore|deleted|purge|import|retention>`, "invalid_args");
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
function parseIntFlag(args, name, defaultValue, bounds = {}) {
|
|
@@ -191,6 +195,50 @@ async function runDelete(appArg, collection, args) {
|
|
|
191
195
|
failFromError(e);
|
|
192
196
|
}
|
|
193
197
|
}
|
|
198
|
+
// `homespun data <app> <coll> restore --key <key>`: undo a delete. Owner/agent
|
|
199
|
+
// only, and --key rather than a positional for the same reason purge uses one:
|
|
200
|
+
// the verb reads deliberately at the call site.
|
|
201
|
+
async function runRestore(appArg, collection, args) {
|
|
202
|
+
assertKnownFlags(args, ...specFor("data", "restore"));
|
|
203
|
+
const key = args.flags.get("key");
|
|
204
|
+
if (!key) {
|
|
205
|
+
fail("usage: homespun data <app> <collection> restore --key <key>", "invalid_args");
|
|
206
|
+
}
|
|
207
|
+
const client = makeClient(args);
|
|
208
|
+
const appId = await resolveAppId(client, appArg);
|
|
209
|
+
try {
|
|
210
|
+
printJson(await client.restoreAppRow(appId, collection, key));
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
failFromError(e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// `homespun data <app> <coll> deleted`: the recovery bin, newest deletion
|
|
217
|
+
// first. Owner/agent only. Paged with --before, which takes the previous
|
|
218
|
+
// page's `next_before`.
|
|
219
|
+
async function runListDeleted(appArg, collection, args) {
|
|
220
|
+
assertKnownFlags(args, ...specFor("data", "deleted"));
|
|
221
|
+
const client = makeClient(args);
|
|
222
|
+
const appId = await resolveAppId(client, appArg);
|
|
223
|
+
const limitRaw = args.flags.get("limit");
|
|
224
|
+
const before = args.flags.get("before");
|
|
225
|
+
const opts = {};
|
|
226
|
+
if (limitRaw !== undefined) {
|
|
227
|
+
const n = Number(limitRaw);
|
|
228
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
229
|
+
fail("--limit must be a positive integer", "invalid_args");
|
|
230
|
+
}
|
|
231
|
+
opts.limit = n;
|
|
232
|
+
}
|
|
233
|
+
if (before !== undefined)
|
|
234
|
+
opts.before = before;
|
|
235
|
+
try {
|
|
236
|
+
printJson(await client.listDeletedAppRows(appId, collection, opts));
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
failFromError(e);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
194
242
|
// `homespun data <app> <coll> purge --key <key>`, owner/agent-only removal that
|
|
195
243
|
// bypasses an append-only collection (Wave C1). The row key comes from --key
|
|
196
244
|
// (not a positional) so a purge reads deliberately, harder to fire by accident.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// `homespun template` (issue #890) community marketplace templates: publish an
|
|
2
|
-
// owned app as a pending template,
|
|
3
|
-
// contract, install a template for the
|
|
4
|
-
//
|
|
5
|
-
// agent's owning
|
|
6
|
-
// server-side. A
|
|
7
|
-
//
|
|
2
|
+
// owned app as a pending template, unpublish your own live listing (#1299),
|
|
3
|
+
// read a template's install-time config contract, install a template for the
|
|
4
|
+
// caller, and (operator) list/show/approve/reject pending submissions.
|
|
5
|
+
// Publish/unpublish/config-contract/install act AS the calling agent's owning
|
|
6
|
+
// human; list-pending/show/approve/reject are operator-gated server-side. A
|
|
7
|
+
// template <ref> is a namespaced <handle>/<slug> or a snapshot id, passed
|
|
8
|
+
// straight to the SDK.
|
|
8
9
|
import { assertKnownFlags } from "../argv.js";
|
|
9
10
|
import { nounSpec, renderNounHelp, specFor } from "../help-catalog.js";
|
|
10
11
|
import { makeClient } from "../config.js";
|
|
@@ -18,7 +19,7 @@ export async function runTemplate(args) {
|
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
21
|
if (verb === undefined) {
|
|
21
|
-
fail("missing verb: homespun template <publish|config-contract|install|list-pending|show|approve|reject>", "invalid_args");
|
|
22
|
+
fail("missing verb: homespun template <publish|unpublish|config-contract|install|list-pending|show|approve|reject>", "invalid_args");
|
|
22
23
|
}
|
|
23
24
|
const sub = {
|
|
24
25
|
positionals: args.positionals.slice(1),
|
|
@@ -31,6 +32,8 @@ export async function runTemplate(args) {
|
|
|
31
32
|
switch (verb) {
|
|
32
33
|
case "publish":
|
|
33
34
|
return runPublish(sub);
|
|
35
|
+
case "unpublish":
|
|
36
|
+
return runUnpublish(sub);
|
|
34
37
|
case "config-contract":
|
|
35
38
|
return runConfigContract(sub);
|
|
36
39
|
case "install":
|
|
@@ -44,7 +47,7 @@ export async function runTemplate(args) {
|
|
|
44
47
|
case "reject":
|
|
45
48
|
return runReject(sub);
|
|
46
49
|
default:
|
|
47
|
-
fail(`unknown verb '${verb}' (homespun template <publish|config-contract|install|list-pending|show|approve|reject>)`, "invalid_args");
|
|
50
|
+
fail(`unknown verb '${verb}' (homespun template <publish|unpublish|config-contract|install|list-pending|show|approve|reject>)`, "invalid_args");
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
function parseIntFlag(args, name, bounds = {}) {
|
|
@@ -109,6 +112,23 @@ async function runPublish(args) {
|
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
// ---------------------------------------------------------------------------
|
|
115
|
+
// unpublish
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
async function runUnpublish(args) {
|
|
118
|
+
assertKnownFlags(args, ...specFor("template", "unpublish"));
|
|
119
|
+
const snapshotId = args.positionals[0];
|
|
120
|
+
if (!snapshotId) {
|
|
121
|
+
fail("usage: homespun template unpublish <snapshot-id>", "invalid_args");
|
|
122
|
+
}
|
|
123
|
+
const client = makeClient(args);
|
|
124
|
+
try {
|
|
125
|
+
printJson(await client.unpublishCommunityTemplate(snapshotId));
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
failFromError(e);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
112
132
|
// config-contract
|
|
113
133
|
// ---------------------------------------------------------------------------
|
|
114
134
|
async function runConfigContract(args) {
|
package/dist/help-catalog.js
CHANGED
|
@@ -133,7 +133,7 @@ const DATA = {
|
|
|
133
133
|
noun: "data",
|
|
134
134
|
tagline: "collection row CRUD for an app",
|
|
135
135
|
group: "app",
|
|
136
|
-
rootSummary: "Collection row CRUD for an app: list, get, upsert, update, delete, purge, import, plus retention (owner override).",
|
|
136
|
+
rootSummary: "Collection row CRUD for an app: list, get, upsert, update, delete, restore, deleted, purge, import, plus retention (owner override).",
|
|
137
137
|
// The data parser is verb-LAST: `homespun data <app> <collection> <verb>`
|
|
138
138
|
// (see commands/data.ts, which reads the verb from positionals[2]). Every
|
|
139
139
|
// other noun is verb-first. Each verb below carries only the positionals that
|
|
@@ -219,6 +219,29 @@ const DATA = {
|
|
|
219
219
|
],
|
|
220
220
|
bools: [{ name: "yes", description: "Skip the confirmation prompt" }],
|
|
221
221
|
},
|
|
222
|
+
{
|
|
223
|
+
verb: "restore",
|
|
224
|
+
summary: "Restores a soft-deleted row. Owner and agent only.",
|
|
225
|
+
flags: [
|
|
226
|
+
{
|
|
227
|
+
name: "key",
|
|
228
|
+
value: "<key>",
|
|
229
|
+
description: "Key of the deleted row to restore (required)",
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
verb: "deleted",
|
|
235
|
+
summary: "Lists recoverable deleted rows. Owner and agent only.",
|
|
236
|
+
flags: [
|
|
237
|
+
{ name: "limit", value: "<n>", description: "Max rows (default 100)" },
|
|
238
|
+
{
|
|
239
|
+
name: "before",
|
|
240
|
+
value: "<iso>",
|
|
241
|
+
description: "Page cursor: the previous page's next_before",
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
222
245
|
{
|
|
223
246
|
verb: "purge",
|
|
224
247
|
summary: "Removes one row even from an append-only collection.",
|
|
@@ -297,7 +320,7 @@ const DATA = {
|
|
|
297
320
|
notes: [
|
|
298
321
|
"<app> accepts either the app_id or its slug. upsert is the ONLY create-shaped verb: omit --key to add a new row (the server generates the key); pass --key to ensure a row exists at that key (returns the existing row with deduped:true on a collision). A collision on a row the collection's read list does not reach for you is row_not_found (404) instead of the row, the same answer a get on that key gives, so upsert cannot read past read. Pass --on <field> to upsert on a manifest-declared UNIQUE field instead of the key: the row whose <field> value matches is updated in place (idempotent re-import), else created.",
|
|
299
322
|
"list --where takes a JSON array of {field, op, value} conditions (ANDed), op one of eq, neq, in, notIn, gt, lt, gte, lte (in and notIn take an array value). --sort takes a JSON array of {field, dir} (dir asc or desc). Filtering is applied AFTER the read permission and author scoping, so a filtered list is always a subset of what you could already read. Comparisons are same-type only (no coercion); dates compare as ISO-8601 strings. A custom --sort cannot be combined with --since.",
|
|
300
|
-
"purge removes ONE row by --key even in an append-only collection.
|
|
323
|
+
"delete is RECOVERABLE: it tombstones the row, `deleted` lists what can still be brought back, and `restore` brings one back for 30 days. purge is the permanent one: it removes ONE row by --key even in an append-only collection, scrubs its contents immediately, and cannot be restored. Both are owner and agent only (never members or anyone); purge bypasses append-only and the collection delete list on purpose, and both write an audited delete feed entry.",
|
|
301
324
|
"import reads NDJSON (one JSON object per line) OR a JSON array from --file and bulk-writes it in chunks via the batch API, in ONE process. Each object is a row's data. Pass --key-field to derive the row key from a field: an existing row at that key is LEFT UNCHANGED, so this is create-or-skip-by-id, not overwrite, and re-importing changed data for a known key does not update it. A skipped row is reported as a per-row row_not_found rather than an ok when the collection's read list does not reach that row for you (the row is still left unchanged); list 'agent' in read if you want the skip reported as a success. Import DEFAULTS TO SILENT (it suppresses notify and webhooks, since a bulk import is a migration); pass --emit-effects to fire them. A per-row failure is listed in the summary WITHOUT aborting the import.",
|
|
302
325
|
"retention is an OWNER control: the author declares default retention in the manifest, and this tightens or loosens it per collection at runtime WITHOUT a redeploy. Effective retention is per-axis override-or-author-default: --max-rows/--max-age-days set an axis override, --clear-rows/--clear-age revert an axis to the author default, and with no flag (or --show) it just reads. The response reports the effective bounds, the author default, the override, and wouldPrune (how many live rows the effective bound would prune on the next sweep). The override survives redeploys and effective maxRows is capped at MAX_ROWS_PER_APP.",
|
|
303
326
|
],
|
|
@@ -1045,7 +1068,7 @@ const TEMPLATE = {
|
|
|
1045
1068
|
noun: "template",
|
|
1046
1069
|
tagline: "community marketplace templates",
|
|
1047
1070
|
group: "other",
|
|
1048
|
-
rootSummary: "Community marketplace templates: publish an owned app, read a template's config-contract, install one, and the operator review queue (list-pending, show, approve, reject).",
|
|
1071
|
+
rootSummary: "Community marketplace templates: publish an owned app, unpublish your own listing, read a template's config-contract, install one, and the operator review queue (list-pending, show, approve, reject).",
|
|
1049
1072
|
verbs: [
|
|
1050
1073
|
{
|
|
1051
1074
|
verb: "publish",
|
|
@@ -1105,6 +1128,11 @@ const TEMPLATE = {
|
|
|
1105
1128
|
},
|
|
1106
1129
|
],
|
|
1107
1130
|
},
|
|
1131
|
+
{
|
|
1132
|
+
verb: "unpublish",
|
|
1133
|
+
positionals: "<snapshot-id>",
|
|
1134
|
+
summary: "Takes your own live listing out of the public gallery.",
|
|
1135
|
+
},
|
|
1108
1136
|
{
|
|
1109
1137
|
verb: "config-contract",
|
|
1110
1138
|
positionals: "<ref>",
|
|
@@ -1156,6 +1184,7 @@ const TEMPLATE = {
|
|
|
1156
1184
|
notes: [
|
|
1157
1185
|
"A template <ref> is a namespaced <handle>/<slug> or a community snapshot id, passed straight to the relay. publish resolves <app> by id or slug, the same way apps and data do.",
|
|
1158
1186
|
"publish, config-contract, and install act as the calling agent's owning human. Install always creates a new owned app and returns its id, slug, and url; use config-contract first to discover what config an install needs.",
|
|
1187
|
+
"unpublish takes one of YOUR OWN live listings down: it leaves the public gallery, search, and the direct snapshot install link. Existing installs keep working, because an install is a fresh private copy and not a live reference. It is idempotent, and it reports not_found for a snapshot that is not yours. Republish a new version to put a listing back.",
|
|
1159
1188
|
"list-pending, show, approve, and reject drive the operator review queue and are operator-gated server-side. reject requires --note, which is delivered to the publisher's app feed.",
|
|
1160
1189
|
],
|
|
1161
1190
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homespunapps/cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.39",
|
|
4
4
|
"description": "Command-line client for the Homespun relay: deploy a real multi-user web app from your agent, then keep reading and writing its data.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"test:unit": "vitest run"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@homespunapps/core": "^1.6.
|
|
39
|
+
"@homespunapps/core": "^1.6.39",
|
|
40
40
|
"qrcode-terminal": "^0.12.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^26.1.
|
|
43
|
+
"@types/node": "^26.1.2",
|
|
44
44
|
"@types/qrcode-terminal": "^0.12.2",
|
|
45
45
|
"typescript": "^7.0.2",
|
|
46
|
-
"vitest": "^4.1.
|
|
46
|
+
"vitest": "^4.1.10"
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
|
49
49
|
"type": "git",
|