@nivaro/sdk 0.1.16 → 0.1.18
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 +93 -0
- package/bin/lib/ext-scaffold.mjs +220 -0
- package/bin/lib/graphql-codegen.mjs +737 -0
- package/bin/nivaro.mjs +150 -29
- package/dist/index.d.ts +290 -1
- package/dist/index.js +126 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -24,6 +24,14 @@ The `@nivaro/sdk` package is a fully-typed TypeScript client for the Nivaro REST
|
|
|
24
24
|
pnpm add @nivaro/sdk
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
#### MCP server (Claude Code, Claude Desktop, Cursor)
|
|
28
|
+
|
|
29
|
+
The companion package `@nivaro/mcp` wraps the SDK as a Model Context Protocol server, so an AI coding assistant can read and write an instance directly: collection schemas, record reads and aggregates, rehearsed creates, updates, deletes with an explicit confirm, pipeline state and transitions, and the instance's own ask-your-data assistant. It authenticates with an `nvk_` API key, so role permissions, row-level security and user scopes apply unchanged; a sandbox key is read-only by construction. Install and client config snippets live in the package README (`packages/mcp/README.md`).
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
claude mcp add nivaro -e NIVARO_URL=https://nivaro.example.com -e NIVARO_TOKEN=nvk_... -- npx -y @nivaro/mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
27
35
|
#### Create a client
|
|
28
36
|
|
|
29
37
|
```typescript
|
|
@@ -2446,6 +2454,91 @@ await nivaro.request(createHierarchyConfig({
|
|
|
2446
2454
|
|
|
2447
2455
|
---
|
|
2448
2456
|
|
|
2457
|
+
## SDK — Help Videos
|
|
2458
|
+
|
|
2459
|
+
Tutorial recordings: upload, edit, version, publish, choose where they show, require viewing and track progress. Authoring commands need an author (an administrator or a role in the author setting); reading and progress need only visibility.
|
|
2460
|
+
|
|
2461
|
+
```typescript
|
|
2462
|
+
import {
|
|
2463
|
+
openHelpVideoUpload, finalizeHelpVideoUpload, createHelpVideo,
|
|
2464
|
+
setHelpVideoContexts, publishHelpVideo, helpVideosFor,
|
|
2465
|
+
} from '@nivaro/sdk'
|
|
2466
|
+
|
|
2467
|
+
// 1. Open an upload, send the bytes, finalize, create the video
|
|
2468
|
+
const { data: up } = await nivaro.request(openHelpVideoUpload('video/webm'))
|
|
2469
|
+
let part = up.next_part
|
|
2470
|
+
for (const chunk of chunks) {
|
|
2471
|
+
// Raw PUT — not a Command, because Commands JSON-encode their bodies.
|
|
2472
|
+
await fetch(`${baseUrl}/api/help-videos/uploads/${up.id}/parts/${part++}`, {
|
|
2473
|
+
method: 'PUT',
|
|
2474
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream' },
|
|
2475
|
+
body: chunk, // at most 8 MB per part; the response is the upload session
|
|
2476
|
+
})
|
|
2477
|
+
}
|
|
2478
|
+
await nivaro.request(finalizeHelpVideoUpload(up.id, { duration_ms }))
|
|
2479
|
+
const { data: video } = await nivaro.request(createHelpVideo({ upload_id: up.id, title: 'Raise a PO' }))
|
|
2480
|
+
|
|
2481
|
+
// 2. Say where it shows, then publish
|
|
2482
|
+
await nivaro.request(setHelpVideoContexts(video.id, [{ kind: 'page', key: 'inbox', state_key: null }]))
|
|
2483
|
+
await nivaro.request(publishHelpVideo(video.id))
|
|
2484
|
+
|
|
2485
|
+
// 3. Show the right videos on a screen
|
|
2486
|
+
const { data } = await nivaro.request(helpVideosFor({ collection: 'purchase_orders', item: 42 }))
|
|
2487
|
+
```
|
|
2488
|
+
|
|
2489
|
+
#### Response notes
|
|
2490
|
+
|
|
2491
|
+
- `stream_url`, `captions_url` and `poster_url` are ticketed (`?st=`): hand them to `<video>`, `<track>` and `<img>` as-is. Authors also get `draft_stream_url` and `draft_captions_url`.
|
|
2492
|
+
- `published.playable` is `false` when a viewer’s stream would answer 409 (the video is still being prepared); authors always get `true`.
|
|
2493
|
+
- `required` is true when the video is required for the caller’s own role. Authors also get `required_role_ids`. `my_progress` is the caller’s own progress (or `null`).
|
|
2494
|
+
- `recordHelpVideoProgress` takes `buckets` as a 20-character `0`/`1` string (the 5% sections seen) and answers `{ data: { completed } }`, or no body (204) for a masquerade session.
|
|
2495
|
+
- `listHelpVideos` and `helpVideosFor` carry `can_author`, so a screen can offer "Add a video" without a second call.
|
|
2496
|
+
|
|
2497
|
+
#### Errors callers must handle
|
|
2498
|
+
|
|
2499
|
+
| Status | code | When |
|
|
2500
|
+
| --- | --- | --- |
|
|
2501
|
+
| 409 | `HELP_VIDEO_PROCESSING` | A viewer requested `stream_url` before a current render exists and the original cannot be shown. Check `published.playable`, retry later. |
|
|
2502
|
+
| 409 | `HELP_VIDEO_EDITS_CONFLICT` | `saveHelpVideoDraft` with a stale `base_hash`. The body carries `current_hash`; reload the draft and merge. |
|
|
2503
|
+
| 409 | `UPLOAD_CLOSED` | The upload is already finalized or finishing; or, on discard, already used by a video or discarded. |
|
|
2504
|
+
| 422 | `UPLOAD_NOT_VIDEO` | A part is not a WebM or MP4 recording. |
|
|
2505
|
+
| 422 | `UPLOAD_TOO_LONG` | The recording is longer than 30 minutes (on finalize). |
|
|
2506
|
+
| 422 | `HELP_VIDEO_NOT_READY` | `publishHelpVideo` before the checklist is done; the body lists `missing`. |
|
|
2507
|
+
| 409 | `HELP_VIDEO_NOTHING_TO_PUBLISH` | `publishHelpVideo` with no draft, or a draft that is exactly the published version (`draft_matches_published`). With `watch_again` on a video someone must watch, it re-asks them instead. |
|
|
2508
|
+
| 422 | `HELP_VIDEO_EDITS_INVALID` | `saveHelpVideoDraft` with edits that cannot be kept, such as cuts that leave less than a second. The message says why; the stored draft is unchanged. |
|
|
2509
|
+
| 409 | `HELP_VIDEO_NOTHING_TO_RENDER` | `rerenderHelpVideo` for a version that does not exist. |
|
|
2510
|
+
| 403 | `HELP_VIDEO_AUTHOR_ONLY` | An authoring command by a non-author. |
|
|
2511
|
+
| 404 | `HELP_VIDEO_NOT_FOUND` | Unknown id, or a video the caller may not see. |
|
|
2512
|
+
|
|
2513
|
+
| Command | Route | Auth |
|
|
2514
|
+
| --- | --- | --- |
|
|
2515
|
+
| listHelpVideos(params?) | GET /help-videos | Authenticated |
|
|
2516
|
+
| readHelpVideo(id) | GET /help-videos/:id | Authenticated |
|
|
2517
|
+
| helpVideosFor(params) | GET /help-videos/for | Authenticated |
|
|
2518
|
+
| createHelpVideo(body) | POST /help-videos | Author |
|
|
2519
|
+
| updateHelpVideo(id, body) | PATCH /help-videos/:id | Author |
|
|
2520
|
+
| setHelpVideoContexts(id, contexts) | PUT /help-videos/:id/contexts | Author |
|
|
2521
|
+
| setHelpVideoRequirements(id, role_ids) | PUT /help-videos/:id/requirements | Author |
|
|
2522
|
+
| archiveHelpVideo(id, opts?) | DELETE /help-videos/:id | Author (purge: admin, archived videos only) |
|
|
2523
|
+
| readHelpVideoDraft(id) | GET /help-videos/:id/draft/edits | Author |
|
|
2524
|
+
| saveHelpVideoDraft(id, edits, base_hash?) | PUT /help-videos/:id/draft/edits | Author |
|
|
2525
|
+
| publishHelpVideo(id, opts?) | POST /help-videos/:id/publish | Author |
|
|
2526
|
+
| rerecordHelpVideo(id, upload_id) | POST /help-videos/:id/rerecord | Author |
|
|
2527
|
+
| listHelpVideoVersions(id) | GET /help-videos/:id/versions | Author |
|
|
2528
|
+
| restoreHelpVideoVersion(id, versionId) | POST /help-videos/:id/versions/:vid/restore | Author |
|
|
2529
|
+
| rerenderHelpVideo(id, opts?) | POST /help-videos/:id/render | Author |
|
|
2530
|
+
| recordHelpVideoProgress(id, body) | POST /help-videos/:id/progress | Authenticated |
|
|
2531
|
+
| readRequiredHelpVideos() | GET /help-videos/required/mine | Authenticated |
|
|
2532
|
+
| readHelpVideoAnalytics(id) | GET /help-videos/:id/analytics | Author |
|
|
2533
|
+
| openHelpVideoUpload(mime) | POST /help-videos/uploads | Author |
|
|
2534
|
+
| finalizeHelpVideoUpload(id, meta?) | POST /help-videos/uploads/:id/finalize | Author |
|
|
2535
|
+
| listMyHelpVideoUploads() | GET /help-videos/uploads/mine | Author |
|
|
2536
|
+
| abandonHelpVideoUpload(id) | DELETE /help-videos/uploads/:id | Author |
|
|
2537
|
+
| listHelpVideoPages() | GET /help-videos/pages | Authenticated |
|
|
2538
|
+
| registerHelpVideoPage(body) | POST /help-videos/pages | Author |
|
|
2539
|
+
|
|
2540
|
+
---
|
|
2541
|
+
|
|
2449
2542
|
## SDK Coverage: 300+ Typed Commands
|
|
2450
2543
|
|
|
2451
2544
|
The @nivaro/sdk command surface now covers every feature area — roughly 175 typed `Command<T>` factories spanning items, files, workflows, pipelines, flows, comments, webhooks, rules, custom queries, trees and hierarchies, submission forms, field watches, notification subscriptions, imports, SLA, alerts, AI endpoints (generate, summarize, validate, check-duplicates), translations, drafts, scheduled changes, record templates, saved views, API keys, widget feeds, sync jobs, ERP submissions, PDF templates, pages, queues (worklists, items, claims, saved views, trends, workload), roles & policies (RBAC/RLS), user management + out-of-office delegation, file management (list/meta/presign/transform URLs), dashboard widgets, extension item/bulk actions, throughput reporting, and more. If a REST route exists, there is a typed command for it.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension scaffolder (#1302) — `nivaro ext init <id> [--dir api/extensions]`.
|
|
3
|
+
*
|
|
4
|
+
* Writes `<dir>/<id>/` with an index.ts written against @nivaro/extension-kit
|
|
5
|
+
* (one setting, one hook, one described cron, one route behind requireAuth),
|
|
6
|
+
* a passing test on the kit's createTestContext, and a README naming the
|
|
7
|
+
* context members the starter uses. Refuses an existing directory and an id
|
|
8
|
+
* that is not kebab-case.
|
|
9
|
+
*
|
|
10
|
+
* Plain ESM so the CLI imports it directly and `node --test` covers it.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { isAbsolute, join, relative, resolve } from 'node:path'
|
|
14
|
+
|
|
15
|
+
/** kebab-case: lowercase words joined by single hyphens, 2–64 characters. */
|
|
16
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
|
17
|
+
|
|
18
|
+
/** Null when the id is usable, otherwise the reason it is not. */
|
|
19
|
+
export function validateExtensionId(id) {
|
|
20
|
+
if (typeof id !== 'string' || id.length === 0) return 'An extension id is required'
|
|
21
|
+
if (id.length < 2 || id.length > 64) return 'The id must be 2–64 characters'
|
|
22
|
+
if (!ID.test(id))
|
|
23
|
+
return `"${id}" is not kebab-case — use lowercase letters, digits and single hyphens (my-extension)`
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The directory the extension goes in when --dir is not given. */
|
|
28
|
+
export function defaultExtensionsDir(cwd = process.cwd()) {
|
|
29
|
+
if (existsSync(join(cwd, 'api', 'extensions'))) return join(cwd, 'api', 'extensions')
|
|
30
|
+
if (existsSync(join(cwd, 'extensions', 'tsconfig.json'))) return join(cwd, 'extensions')
|
|
31
|
+
return join(cwd, 'api', 'extensions')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function camel(id) {
|
|
35
|
+
return id.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The files, by name, for an extension called `id`. */
|
|
39
|
+
export function scaffoldFiles(id) {
|
|
40
|
+
// The id is written into source text and a path: only a validated one.
|
|
41
|
+
const bad = validateExtensionId(id)
|
|
42
|
+
if (bad) throw new Error(bad)
|
|
43
|
+
const label = id
|
|
44
|
+
.split('-')
|
|
45
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
46
|
+
.join(' ')
|
|
47
|
+
const counter = `${camel(id)}Creates`
|
|
48
|
+
|
|
49
|
+
const index = `import { defineExtension } from '@nivaro/extension-kit'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* ${label} — scaffolded by \`nivaro ext init ${id}\`.
|
|
53
|
+
*
|
|
54
|
+
* One of each thing an extension usually starts with: a setting an admin edits
|
|
55
|
+
* on the Extensions page, a hook on record writes, a scheduled job, and a route.
|
|
56
|
+
* The Extensions registry sheet lists each of them once the API loads this file.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/** Creates seen since the last daily summary, per collection. In memory: a
|
|
60
|
+
* restart starts the count again, which is fine for a summary line. */
|
|
61
|
+
const ${counter} = new Map<string, number>()
|
|
62
|
+
|
|
63
|
+
export default defineExtension({
|
|
64
|
+
id: '${id}',
|
|
65
|
+
settings: [
|
|
66
|
+
{
|
|
67
|
+
key: 'greeting',
|
|
68
|
+
label: 'Greeting',
|
|
69
|
+
type: 'string',
|
|
70
|
+
default: 'Hello',
|
|
71
|
+
description: 'The word GET /api/${id}/hello answers with.'
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
async register(ctx) {
|
|
75
|
+
const { app, auth, hooks, cron, logger } = ctx
|
|
76
|
+
|
|
77
|
+
// Route. \`preHandler: auth.requireAuth\` is the gate: the loader records it,
|
|
78
|
+
// and a route with no gate is a warning on the readiness scorecard.
|
|
79
|
+
app.register(
|
|
80
|
+
async (f) => {
|
|
81
|
+
f.get('/${id}/hello', { preHandler: auth.requireAuth }, async () => {
|
|
82
|
+
const greeting = (await ctx.settings?.get('greeting')) ?? 'Hello'
|
|
83
|
+
return { data: { message: \`\${greeting} from ${id}\` } }
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
{ prefix: '/api' }
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// Hook. After-hooks run once the write has landed; keep them quick, the
|
|
90
|
+
// caller waits for them.
|
|
91
|
+
hooks.after('*', 'create', async ({ collection }) => {
|
|
92
|
+
${counter}.set(collection, (${counter}.get(collection) ?? 0) + 1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Scheduled job. The id is scoped to this extension; the description is
|
|
96
|
+
// what Background Jobs shows beside it.
|
|
97
|
+
cron.schedule(
|
|
98
|
+
'daily-summary',
|
|
99
|
+
'0 6 * * *',
|
|
100
|
+
async () => {
|
|
101
|
+
const total = [...${counter}.values()].reduce((n, v) => n + v, 0)
|
|
102
|
+
const busiest = [...${counter}.entries()].sort((a, b) => b[1] - a[1])[0]
|
|
103
|
+
${counter}.clear()
|
|
104
|
+
await ctx.logActivity({
|
|
105
|
+
action: 'daily-summary',
|
|
106
|
+
comment: busiest
|
|
107
|
+
? \`\${total} record(s) created; most in \${busiest[0]} (\${busiest[1]})\`
|
|
108
|
+
: 'No records created'
|
|
109
|
+
})
|
|
110
|
+
logger.info({ total }, '${id}: daily summary written')
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
description: 'One activity line a day counting the records created since the last.',
|
|
114
|
+
idempotent: 'safe'
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
`
|
|
120
|
+
|
|
121
|
+
const test = `import { createTestContext } from '@nivaro/extension-kit'
|
|
122
|
+
import { describe, expect, it } from 'vitest'
|
|
123
|
+
import extension from './index.js'
|
|
124
|
+
|
|
125
|
+
describe('${id}', () => {
|
|
126
|
+
it('answers the hello route with the configured greeting', async () => {
|
|
127
|
+
const ctx = createTestContext({ settings: { greeting: 'Hi' } })
|
|
128
|
+
await extension.register(ctx)
|
|
129
|
+
const res = await ctx.invoke('GET', '/api/${id}/hello')
|
|
130
|
+
expect(res).toEqual({ status: 200, body: { data: { message: 'Hi from ${id}' } } })
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('counts creates and reports them from the daily job', async () => {
|
|
134
|
+
const ctx = createTestContext()
|
|
135
|
+
await extension.register(ctx)
|
|
136
|
+
await ctx.runHooks('orders', 'create', 'after', { keys: [1] })
|
|
137
|
+
await ctx.runHooks('orders', 'create', 'after', { keys: [2] })
|
|
138
|
+
await ctx.runCron('daily-summary')
|
|
139
|
+
expect(ctx.calls.activity).toEqual([
|
|
140
|
+
{ action: 'daily-summary', comment: '2 record(s) created; most in orders (2)' }
|
|
141
|
+
])
|
|
142
|
+
expect(ctx.registered.crons.get('daily-summary')?.opts?.description).toBeTruthy()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('uses no deprecated kit member', async () => {
|
|
146
|
+
const ctx = createTestContext()
|
|
147
|
+
await extension.register(ctx)
|
|
148
|
+
expect(ctx.calls.deprecations).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
`
|
|
152
|
+
|
|
153
|
+
const readme = `# ${label}
|
|
154
|
+
|
|
155
|
+
Scaffolded by \`nivaro ext init ${id}\`. The API loads it from \`api/extensions/${id}/\` at boot
|
|
156
|
+
(restart the API after adding it); the Extensions page lists it with a registry sheet of
|
|
157
|
+
everything it registered.
|
|
158
|
+
|
|
159
|
+
## What it registers
|
|
160
|
+
|
|
161
|
+
| Thing | Where | Context member |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| Setting \`greeting\` | Extensions page → ${id} → Settings | \`settings\` on the export, read with \`ctx.settings.get('greeting')\` |
|
|
164
|
+
| Route \`GET /api/${id}/hello\` | behind \`requireAuth\` | \`ctx.app.register\`, gated with \`ctx.auth.requireAuth\` |
|
|
165
|
+
| Hook after every create | all collections | \`ctx.hooks.after('*', 'create', fn)\` |
|
|
166
|
+
| Daily job \`daily-summary\` | Background Jobs | \`ctx.cron.schedule(id, expression, fn, { description })\` |
|
|
167
|
+
| Activity line | Activity log | \`ctx.logActivity({ action, comment })\` |
|
|
168
|
+
|
|
169
|
+
## Context members you will reach for next
|
|
170
|
+
|
|
171
|
+
- \`ctx.database\` — knex on the instance database (raw writes skip hooks, revisions and permissions; prefer the items API for record writes).
|
|
172
|
+
- \`ctx.notifyUser(userId, { subject, message })\` — the only way to notify a person.
|
|
173
|
+
- \`ctx.callExternalApi(name, { method, path, body })\` — configured external APIs; credentials never reach the extension.
|
|
174
|
+
- \`ctx.flows.registerOperation\` / \`registerTrigger\` / \`emit\` — custom flow steps and triggers.
|
|
175
|
+
- \`ctx.readiness.registerCheck\`, \`ctx.integrity.registerCheck\` — scored checks and Data Integrity checks.
|
|
176
|
+
- \`ctx.events.publish\` / \`on\` — a durable outbox for this extension's own events.
|
|
177
|
+
- \`ctx.sql.runLong(sql)\` — one long statement outside knex's request timeout.
|
|
178
|
+
|
|
179
|
+
The full contract is \`ExtensionContext\` in \`@nivaro/extension-kit\`.
|
|
180
|
+
|
|
181
|
+
## Test and type-check
|
|
182
|
+
|
|
183
|
+
\`\`\`bash
|
|
184
|
+
cd api
|
|
185
|
+
npx vitest run extensions/${id}
|
|
186
|
+
npx tsc -p extensions/tsconfig.json
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
The test runs the extension against the kit's \`createTestContext()\`: every side effect lands in
|
|
190
|
+
\`ctx.calls\`, every registration in \`ctx.registered\`, and \`ctx.calls.deprecations\` lists any
|
|
191
|
+
deprecated kit member the extension read.
|
|
192
|
+
`
|
|
193
|
+
|
|
194
|
+
return { 'index.ts': index, 'index.test.ts': test, 'README.md': readme }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Writes the extension. Throws (with the reason) on a bad id or an existing
|
|
199
|
+
* directory; answers the paths written.
|
|
200
|
+
*/
|
|
201
|
+
export function writeScaffold(id, dir) {
|
|
202
|
+
const bad = validateExtensionId(id)
|
|
203
|
+
if (bad) throw new Error(bad)
|
|
204
|
+
if (typeof dir !== 'string' || dir.length === 0 || dir.includes('\0'))
|
|
205
|
+
throw new Error('--dir must be a directory path')
|
|
206
|
+
const base = resolve(dir)
|
|
207
|
+
const target = join(base, id)
|
|
208
|
+
// The extension lands exactly one level under --dir, never beside or above it.
|
|
209
|
+
const rel = relative(base, target)
|
|
210
|
+
if (rel !== id || isAbsolute(rel)) throw new Error(`Refusing to write outside ${base}`)
|
|
211
|
+
if (existsSync(target)) throw new Error(`${target} already exists — pick another id or remove it`)
|
|
212
|
+
mkdirSync(target, { recursive: true })
|
|
213
|
+
const written = []
|
|
214
|
+
for (const [name, text] of Object.entries(scaffoldFiles(id))) {
|
|
215
|
+
const file = join(target, name)
|
|
216
|
+
writeFileSync(file, text, 'utf8')
|
|
217
|
+
written.push(file)
|
|
218
|
+
}
|
|
219
|
+
return { target, written, hasTsconfig: existsSync(join(base, 'tsconfig.json')) }
|
|
220
|
+
}
|