rvlib-shipkit 0.23.0 → 0.24.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.24.0
4
+
5
+ - **Custom panels lead the studio drawer, and the prompt folds.** A repo's `customPanel` tabs (the tint-mask painter, a legs editor) are the working surfaces, yet they rendered LAST in the drawer tab row, behind two json diagnostics. They come first now, `asset` then `roll meta` after, and a kind with exactly one custom panel gets it as the drawer's default tab until the user picks another. The prompt textarea folds to one ellipsized line by default (click to expand, a chevron folds it back, state holds for the session), so the stage and the rolls get the height back. The drawer toggle now reads `drawer ▾`, because with a custom panel as default content a button labelled JSON opened a paint surface.
6
+
7
+ - **`shipkit start` on a brand-new dir bootstraps a WORKING repo, end to end.** It used to stop at git + the github repo and hand back two `next:` steps, because init needs a package.json. Now the dir being created THIS RUN is the signal to scaffold: a minimal package.json (private, type module), a first `src/index.ts` so the ci gate is green from commit one instead of failing an imports scan over zero files, the baseline .gitignore before anything installs, `bun add -d rvlib-shipkit` so the wired scripts actually run, then init, first commit, push. The guards stay strict: a pre-existing folder without a project gets git only, `--github` on it forces the github half but never writes a project into someone's files, a subdirectory another work tree owns gets nothing, a temp dir or `SHIPKIT_NO_INSTALL=1` skips the install, and an install that cannot run (offline, no bun) is a WARN, never a stop.
8
+
9
+ - **SK011 checks the FLOOR, not just resolution.** `^0.1.1` resolves 0.1.2, so a range left behind by a bump passed the rule while a consumer tree already holding 0.1.1 kept it and never saw the release. A sibling range must now name exactly the version that sibling carries, which is what `shipkit bump` has always written. It found one real stale floor the moment it tightened.
10
+
3
11
  ## 0.23.0
4
12
 
5
13
  - **Release order is a TOPOSORT of the units' own dependency graph.** It was "sub-units first, root last", so a `--all` run published in declaration order and a dependent could ship naming a version the registry did not have yet. Kahn's algorithm now, stable (ties keep declaration order, root always last), and a cycle degrades to declaration order rather than dropping a unit: a release that silently skips a package is worse than one in a bad order.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvlib-shipkit",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,9 +9,9 @@ export class StartCommand extends Command {
9
9
 
10
10
  static override usage = Command.Usage({
11
11
  category: 'Setup',
12
- description: 'create (or enter) ~/dev/<name> from ANY cwd: ensures the dir, seeds CLAUDE.md include lines, wires shipkit on the spot (repo.config.ts via init) when the project already has a package.json, then runs the whole bootstrap chain: git init, first commit, PRIVATE github repo, push. Prints the dir on stdout so a shell alias can cd there. cwd-only sibling: `shipkit init`',
12
+ description: 'create (or enter) ~/dev/<name> from ANY cwd, READY to work in: ensures the dir, seeds CLAUDE.md include lines, and on a BRAND-NEW dir scaffolds a minimal package.json + src/index.ts + the rvlib-shipkit dev dep, wires shipkit (repo.config.ts via init), then runs the whole bootstrap chain: git init, first commit, PRIVATE github repo, push. A pre-existing folder with no project gets git only, never a scaffold (--github forces only the github half for it). Prints the dir on stdout so a shell alias can cd there. cwd-only sibling: `shipkit init`',
13
13
  examples: [
14
- ['create or enter ~/dev/pong: dir, wiring, git, first commit, private github repo, push', 'shipkit start pong'],
14
+ ['create or enter ~/dev/pong: dir, package.json, shipkit wiring, git, first commit, private github repo, push', 'shipkit start pong'],
15
15
  ['seed CLAUDE.md includes', "shipkit start pong --include '@~/wiki/index.md' --include '@./agent/index.md'"],
16
16
  ['adopt a pre-existing repo from anywhere', 'shipkit start my-old-project'],
17
17
  ['force the private github repo for a folder that was already there', 'shipkit start old-notes --github'],
@@ -48,8 +48,10 @@ const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
48
48
  /** what a repo with NO .gitignore at all gets before its first commit. Not a
49
49
  * policy (rule SK004 owns that one), just the three things nobody ever meant
50
50
  * to publish. A repo that already has a .gitignore keeps its own answer, and
51
- * the commit refuses instead when that answer misses these */
52
- const IGNORE_BASELINE = ['node_modules/', '.env', '.env.*', '!.env.example', '.DS_Store']
51
+ * the commit refuses instead when that answer misses these. Two writers:
52
+ * firstCommit below, and the start scaffold (BEFORE its dep install, so
53
+ * node_modules never reaches the first commit) */
54
+ export const IGNORE_BASELINE = ['node_modules/', '.env', '.env.*', '!.env.example', '.DS_Store']
53
55
 
54
56
  /** the one line of gh or git output worth reporting — both merge stderr into
55
57
  * stdout, so the first non-empty line is the message and the rest is chatter */
@@ -68,7 +70,7 @@ export function carveGhJson<T>(out: string): T | null {
68
70
  }
69
71
  }
70
72
 
71
- const runner = (bin: string, root: string, timeoutMs: number): Run => async (cmd) => {
73
+ export const runner = (bin: string, root: string, timeoutMs: number): Run => async (cmd) => {
72
74
  // a credential prompt would hang a bootstrap forever: git fails instead
73
75
  const env = { ...process.env, GIT_TERMINAL_PROMPT: '0' }
74
76
  const r = await spawnWithTimeout([bin, ...cmd], { cwd: root, timeoutMs, env })
@@ -23,9 +23,20 @@ type Pkg = {
23
23
 
24
24
  const BLOCKS = ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies'] as const
25
25
 
26
- /** does `version` satisfy a caret/tilde/exact range? Deliberately small: the
27
- * ranges this repo writes are `^x.y.z`, `~x.y.z` or an exact pin, and a range
28
- * shape it cannot read is SKIPPED rather than guessed at. */
26
+ /** does this range name the sibling's CURRENT version as its floor? Not
27
+ * "does it resolve": `^0.1.1` happily resolves 0.1.2, and then a consumer
28
+ * whose tree already holds 0.1.1 keeps it and misses the release. The floor
29
+ * moves on every bump, so a stale one means a bump nobody propagated.
30
+ * Deliberately small: `^x.y.z`, `~x.y.z` or an exact pin, and a range shape
31
+ * it cannot read is SKIPPED rather than guessed at. */
32
+ export function floorMatches(version: string, range: string): boolean | null {
33
+ const r = range.trim().match(/^([\^~]?)(\d+\.\d+\.\d+)$/)
34
+ if (r == null || !/^\d+\.\d+\.\d+/.test(version)) return null
35
+ return r[2] === version
36
+ }
37
+
38
+ /** does `version` satisfy a caret/tilde/exact range? Kept for the resolution
39
+ * question, which is a different one from the floor question above. */
29
40
  export function satisfiesSimple(version: string, range: string): boolean | null {
30
41
  const v = version.match(/^(\d+)\.(\d+)\.(\d+)/)
31
42
  const r = range.trim().match(/^([\^~]?)(\d+)\.(\d+)\.(\d+)$/)
@@ -64,14 +75,17 @@ export const siblingUnitRanges: Rule = {
64
75
  const actual = version.get(dep)
65
76
  if (actual == null) continue
66
77
  if (/^(link|file|portal|workspace):/.test(range)) continue
67
- const ok = satisfiesSimple(actual, range)
78
+ const ok = floorMatches(actual, range)
68
79
  if (ok == null) continue
69
80
  checked++
70
- if (!ok) bad.push(`${u.id}: ${block}.${dep} wants ${range}, ${dep} is ${actual}`)
81
+ if (!ok)
82
+ bad.push(
83
+ `${u.id}: ${block}.${dep} floors at ${range}, ${dep} is ${actual}${satisfiesSimple(actual, range) === true ? ' (it resolves, but a tree already holding the older one keeps it)' : ''}`,
84
+ )
71
85
  }
72
86
  }
73
87
  if (bad.length > 0)
74
- t.fail(`${bad.join(' · ')} — a range naming a version its sibling does not carry breaks at INSTALL, for a consumer. \`shipkit bump\` moves these forward, so fix the bump rather than the range`)
75
- else t.ok(`${checked} sibling range(s) match the version each unit carries`)
88
+ t.fail(`${bad.join(' · ')} — a sibling range FLOORS at the version it names, so a stale floor lets a consumer keep an older copy and miss the release. \`shipkit bump\` moves every floor forward in the version commit, so a red here is a bump nobody propagated`)
89
+ else t.ok(`${checked} sibling range(s) floor at exactly the version each unit carries`)
76
90
  },
77
91
  }
@@ -60,12 +60,18 @@ aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto
60
60
 
61
61
  /* ── main column ── */
62
62
  main{display:flex;flex-direction:column;height:100vh;overflow:hidden}
63
- /* topbar: the prompt is its OWN full-width row (small mono, auto-grows to
64
- its content, scrolls only past ~14 lines), then ONE slim toolbar row */
63
+ /* topbar: the prompt is its OWN full-width row (folded by default to one
64
+ ellipsized line; expanded = small mono, auto-grows to its content,
65
+ scrolls only past ~14 lines), then ONE slim toolbar row */
65
66
  .bar{display:flex;flex-direction:column;gap:8px;padding:10px 16px;border-bottom:1px solid var(--line);background:var(--panel)}
66
67
  textarea{width:100%;min-height:64px;background:var(--bg);color:var(--ink);border:1px solid var(--line);
67
68
  border-radius:9px;padding:9px 11px;font:12px/1.5 ui-monospace,Menlo,monospace;resize:vertical}
68
- .bar .prompt{min-height:36px;max-height:252px;overflow-y:auto;resize:none;font:11px/1.5 ui-monospace,Menlo,monospace;padding:7px 10px}
69
+ .promptrow{display:flex;gap:6px;align-items:flex-start}
70
+ .promptrow .fold{flex:none;background:var(--panel2);color:var(--dim);border:1px solid var(--line);border-radius:9px;
71
+ padding:6px 9px;font-size:11px;line-height:1.5}
72
+ .bar .prompt{flex:1;min-height:36px;max-height:252px;overflow-y:auto;resize:none;font:11px/1.5 ui-monospace,Menlo,monospace;padding:7px 10px}
73
+ .bar .prompt.folded{min-height:0;max-height:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;cursor:pointer;
74
+ color:var(--dim);background:var(--bg);border:1px solid var(--line);border-radius:9px}
69
75
  .toolbar{display:flex;gap:8px;align-items:center;min-height:30px}
70
76
  .toolbar button{padding:5px 11px;font-size:12px}
71
77
  .toolbar .sp{flex:1}
@@ -58,6 +58,11 @@ export class MediaSvc {
58
58
  drawerOpen = false
59
59
  jsonText = ''
60
60
  jsonMsg = ''
61
+ /** prompt folded to one ellipsized line (session state, click toggles) */
62
+ promptOpen = false
63
+
64
+ /** latched by picking a NON-default tab, until then a sole custom panel is the default */
65
+ private tabPicked = false
61
66
 
62
67
  private hbTimer: ReturnType<typeof setInterval> | null = null
63
68
  private rectTimer: ReturnType<typeof setTimeout> | null = null
@@ -66,8 +71,8 @@ export class MediaSvc {
66
71
  private savedPrompt = ''
67
72
 
68
73
  constructor(private st: HubSt) {
69
- makeAutoObservable<this, 'st' | 'hbTimer' | 'rectTimer' | 'promptTimer' | 'savedPrompt'>(
70
- this, { st: false, hbTimer: false, rectTimer: false, promptTimer: false, savedPrompt: false })
74
+ makeAutoObservable<this, 'st' | 'hbTimer' | 'rectTimer' | 'promptTimer' | 'savedPrompt' | 'tabPicked'>(
75
+ this, { st: false, hbTimer: false, rectTimer: false, promptTimer: false, savedPrompt: false, tabPicked: false })
71
76
  }
72
77
 
73
78
  start(): void {
@@ -161,7 +166,11 @@ export class MediaSvc {
161
166
  this.savedPrompt = r.asset.prompt
162
167
  this.promptMsg = ''
163
168
  if (this.deterministic) this.n = '1'
164
- if (this.tab.startsWith('c:') && !r.asset.custom.some((c) => `c:${c.id}` === this.tab)) this.tab = 'asset'
169
+ // custom panels are the working surfaces, a kind with exactly one
170
+ // gets it as the default tab (setTab latches an explicit divergence)
171
+ const sole = r.asset.custom.length === 1 ? `c:${r.asset.custom[0]!.id}` : null
172
+ if (this.tab.startsWith('c:') && !r.asset.custom.some((c) => `c:${c.id}` === this.tab)) this.tab = sole ?? 'asset'
173
+ else if (!this.tabPicked && sole != null) this.tab = sole
165
174
  this.pickActive(r)
166
175
  })
167
176
  if (this.hasGeom) await this.loadRect()
@@ -279,6 +288,9 @@ export class MediaSvc {
279
288
  this.promptMsg = 'copied'
280
289
  setTimeout(() => runInAction(() => { this.promptMsg = '' }), 900)
281
290
  }
291
+ togglePrompt(): void {
292
+ this.promptOpen = !this.promptOpen
293
+ }
282
294
 
283
295
  toggleFold(cat: string): void {
284
296
  if (this.st.query.trim() !== '') return // folds yield to a live search
@@ -370,9 +382,12 @@ export class MediaSvc {
370
382
  await this.fillCat()
371
383
  }
372
384
 
373
- // ── drawer: asset json (read-only) · roll meta sidecar · custom panels ─
385
+ // ── drawer: custom panels · asset json (read-only) · roll meta sidecar ─
374
386
  setTab(tab: string): void {
375
387
  this.tab = tab
388
+ // clicking the sole panel IS the default, only a different pick latches
389
+ const c = this.detail?.asset.custom ?? []
390
+ if (tab !== (c.length === 1 ? `c:${c[0]!.id}` : null)) this.tabPicked = true
376
391
  this.drawerOpen = true
377
392
  void this.loadTab()
378
393
  }
@@ -1,6 +1,6 @@
1
1
  // Media views — topbar, info chips, stage (image/audio + geometry), roll
2
- // strip, category grid + size-true lineup, drawer (asset json · roll meta ·
3
- // repo-defined custom panels). Feature parity with the deleted vanilla
2
+ // strip, category grid + size-true lineup, drawer (repo-defined custom
3
+ // panels · asset json · roll meta). Feature parity with the deleted vanilla
4
4
  // studio SPA; state lives in MediaSvc (components read and call).
5
5
  import { runInAction } from 'mobx'
6
6
  import { observer } from 'mobx-react-lite'
@@ -27,12 +27,23 @@ export const MediaTopbar = observer(({ app }: { app: HubSt }) => {
27
27
  if (t == null) return
28
28
  t.style.height = 'auto'
29
29
  t.style.height = `${t.scrollHeight + 2}px`
30
- }, [m.prompt])
30
+ }, [m.prompt, m.promptOpen])
31
31
  return (
32
32
  <div className="bar">
33
- {/* the prompt AUTOSAVES (debounced) — no save button, no fear */}
34
- <textarea className="prompt" ref={taRef} value={m.prompt} placeholder="select an asset ←"
35
- onChange={(e) => m.setPrompt(e.target.value)} />
33
+ {/* the prompt AUTOSAVES (debounced), no save button, no fear.
34
+ folded (default) = one ellipsized line, click to expand */}
35
+ <div className="promptrow">
36
+ <button className="fold" title={m.promptOpen ? 'fold the prompt' : 'expand the prompt'}
37
+ onClick={() => m.togglePrompt()}>{m.promptOpen ? '▾' : '▸'}</button>
38
+ {m.promptOpen
39
+ ? <textarea className="prompt" ref={taRef} value={m.prompt} placeholder="select an asset ←"
40
+ onChange={(e) => m.setPrompt(e.target.value)} />
41
+ : <div className="prompt folded" onClick={() => m.togglePrompt()}>
42
+ {/* placeholder only when NOTHING is selected, a selected
43
+ asset with an empty prompt shows an empty line */}
44
+ {m.curId == null ? 'select an asset ←' : m.prompt === '' ? '\u00a0' : m.prompt}
45
+ </div>}
46
+ </div>
36
47
  <div className="toolbar">
37
48
  {!manual ? <button disabled={m.curId == null} onClick={() => void m.generate()}>Generate</button> : null}
38
49
  {!manual ? (
@@ -232,7 +243,7 @@ export const CatGrid = observer(({ app }: { app: HubSt }) => {
232
243
  )
233
244
  })
234
245
 
235
- // ── drawer: asset json (read-only) · roll meta · custom panels ───────────
246
+ // ── drawer: custom panels · asset json (read-only) · roll meta ───────────
236
247
  const PanelHost = observer(({ app }: { app: HubSt }) => {
237
248
  const m = app.media
238
249
  const ref = useRef<HTMLDivElement>(null)
@@ -273,11 +284,13 @@ export const Drawer = observer(({ app }: { app: HubSt }) => {
273
284
  return (
274
285
  <div className="drawer">
275
286
  <div className="tabs">
276
- {[{ id: 'asset', label: 'asset' }, { id: 'meta', label: 'roll meta' }, ...customs.map((c) => ({ id: `c:${c.id}`, label: c.id }))].map((t) => (
287
+ {/* customs lead: they are the working surfaces, json tabs are diagnostics */}
288
+ {[...customs.map((c) => ({ id: `c:${c.id}`, label: c.id })), { id: 'asset', label: 'asset' }, { id: 'meta', label: 'roll meta' }].map((t) => (
277
289
  <button key={t.id} className={m.tab === t.id ? 'on' : ''} onClick={() => m.setTab(t.id)}>{t.label}</button>
278
290
  ))}
279
291
  <span className="sp" />
280
- <button className="ghost" onClick={() => runInAction(() => { m.drawerOpen = !m.drawerOpen })}>JSON ▾</button>
292
+ {/* names the drawer, not the tab set (a sole custom panel is the default content) */}
293
+ <button className="ghost" onClick={() => runInAction(() => { m.drawerOpen = !m.drawerOpen })}>drawer ▾</button>
281
294
  </div>
282
295
  {m.drawerOpen ? (
283
296
  <div id="drawerbody" className="on">
package/src/start-repo.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
8
8
  import { homedir } from 'os'
9
9
  import { join } from 'path'
10
- import { ensureGitRepo, publishRepo, type Run } from './git-bootstrap.ts'
10
+ import { ensureGitRepo, firstGhLine, IGNORE_BASELINE, isScratch, publishRepo, runner, type Run } from './git-bootstrap.ts'
11
11
  import { initRepo } from './init-repo.ts'
12
12
 
13
13
  export type StartOpts = {
@@ -24,6 +24,9 @@ export type StartOpts = {
24
24
  github?: boolean
25
25
  /** how `gh` runs — injected by tests, which never touch a real account */
26
26
  gh?: Run | null
27
+ /** how `bun` runs for the dep install — injected by tests, which never
28
+ * touch the network. An explicit null means no bun on this machine */
29
+ bun?: Run | null
27
30
  }
28
31
 
29
32
  export async function startRepo(name: string, opts: StartOpts = {}): Promise<number> {
@@ -66,11 +69,15 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
66
69
  }
67
70
  }
68
71
  }
69
- // start is the from-anywhere verb: a dir that is already a real project
70
- // (package.json) gets wired on the spot, so a pre-existing repo goes
71
- // 0 → shipkit'd with the one command. init itself stays cwd-only, like
72
- // git init. A brand-new empty dir gets git + its remote all the same, and
73
- // is told what is next, because the shipkit scaffold needs a package.json.
72
+ // start is the from-anywhere verb, and a STARTED repo comes out READY:
73
+ // a brand-new dir gets a minimal package.json, a first src/index.ts, the
74
+ // rvlib-shipkit dev dep, repo.config.ts via init, git, first commit,
75
+ // private github repo, push. A dir that already holds a project
76
+ // (package.json) gets the same minus the scaffold. A PRE-EXISTING dir
77
+ // with NO project is the everyday cd into some old folder: git only,
78
+ // never a scaffold (--github forces only the github half, so it must
79
+ // never write a package.json into a folder that holds someone's files).
80
+ // init itself stays cwd-only, like git init.
74
81
  // --skip means shipkit NEVER TOUCHES a listed repo (a day-job checkout
75
82
  // belongs on that list), not just no CLAUDE.md. And the cd contract survives a
76
83
  // failed adoption: the dir always prints, wiring trouble is a WARN.
@@ -83,6 +90,41 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
83
90
  let wired = false
84
91
  try { wired = await ensureGitRepo(dir, { print }) }
85
92
  catch (e) { console.error(`WARN could not wire git in ${dir}: ${e instanceof Error ? e.message : String(e)} — the dir is still yours`) }
93
+ // the scaffold fires ONLY for a dir this run created (never --github:
94
+ // forcing the github half on an old folder must not write a project
95
+ // into it), and only where git actually wired (a subdirectory of
96
+ // another work tree gets nothing): the minimal package.json lands so
97
+ // init below has its floor and the first commit carries a working
98
+ // repo, not an empty husk
99
+ if (wired && justCreated && !existsSync(join(dir, 'package.json')) && !existsSync(join(dir, 'repo.config.ts'))) {
100
+ const pkg = { name, version: '0.0.0', private: true, type: 'module', scripts: {} }
101
+ writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg, null, 4) + '\n')
102
+ // the first source file, so the ci gate is green from commit one:
103
+ // an imports gate over zero files fails loud on purpose
104
+ mkdirSync(join(dir, 'src'), { recursive: true })
105
+ writeFileSync(join(dir, 'src', 'index.ts'), 'export {}\n')
106
+ console.error('ok: package.json + src/index.ts scaffolded (private, type module)')
107
+ // the install below creates node_modules/ BEFORE the first
108
+ // commit, so the baseline ignore must exist first (SK004's block,
109
+ // written by init, does not carry node_modules)
110
+ if (!existsSync(join(dir, '.gitignore'))) writeFileSync(join(dir, '.gitignore'), `${IGNORE_BASELINE.join('\n')}\n`)
111
+ // the scripts init wires call node_modules/rvlib-shipkit — a
112
+ // scaffold whose scripts exit 127 is not a started repo. Skips
113
+ // mirror the github half's guards: an injected runner wins, the
114
+ // env switch turns it off machine-wide, a temp dir is a rehearsal
115
+ // that must not pull the network. init owns the fix line for
116
+ // every not-installed outcome
117
+ let bun: Run | null
118
+ if (opts.bun !== undefined) bun = opts.bun
119
+ else if ((process.env.SHIPKIT_NO_INSTALL ?? '') !== '') { console.error('skip: rvlib-shipkit install — SHIPKIT_NO_INSTALL is set'); bun = null }
120
+ else if (isScratch(dir)) { console.error(`skip: rvlib-shipkit install — ${dir} is a temp dir`); bun = null }
121
+ else bun = Bun.which('bun') == null ? null : runner('bun', dir, 180_000)
122
+ if (bun != null) {
123
+ const r = await bun(['add', '-d', 'rvlib-shipkit'])
124
+ if (r.code === 0) console.error('ok: rvlib-shipkit installed (dev dep)')
125
+ else console.error(`WARN rvlib-shipkit install failed: ${firstGhLine(r.out)}`)
126
+ }
127
+ }
86
128
  let unscaffolded = false
87
129
  if (!existsSync(join(dir, 'repo.config.ts'))) {
88
130
  if (existsSync(join(dir, 'package.json'))) {