@micacms/runtime 0.4.1 → 0.5.0-rc.2
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 +2 -2
- package/dist/app.d.ts +8 -37
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +7 -42
- package/dist/app.js.map +1 -1
- package/dist/config.d.ts +12 -82
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -82
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +37 -66
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -75
- package/dist/context.js.map +1 -1
- package/dist/http.d.ts +8 -16
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +6 -13
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +13 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/file-type.js.map +1 -1
- package/dist/lib/id.js.map +1 -1
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/ports.js.map +1 -1
- package/dist/lib/rate-limit.js.map +1 -1
- package/dist/lib/schema-stats.js.map +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/routes/admin-content.d.ts +4 -17
- package/dist/routes/admin-content.d.ts.map +1 -1
- package/dist/routes/admin-content.js +11 -51
- package/dist/routes/admin-content.js.map +1 -1
- package/dist/routes/admin-media.d.ts +3 -14
- package/dist/routes/admin-media.d.ts.map +1 -1
- package/dist/routes/admin-media.js +7 -29
- package/dist/routes/admin-media.js.map +1 -1
- package/dist/routes/admin-spa.js.map +1 -1
- package/dist/routes/admin-staging.d.ts +4 -9
- package/dist/routes/admin-staging.d.ts.map +1 -1
- package/dist/routes/admin-staging.js +31 -18
- package/dist/routes/admin-staging.js.map +1 -1
- package/dist/routes/admin-system.d.ts +2 -8
- package/dist/routes/admin-system.d.ts.map +1 -1
- package/dist/routes/admin-system.js +9 -44
- package/dist/routes/admin-system.js.map +1 -1
- package/dist/routes/health.d.ts +3 -10
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +9 -26
- package/dist/routes/health.js.map +1 -1
- package/dist/schema.d.ts +10 -38
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +14 -65
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +2 -10
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -9
- package/dist/server.js.map +1 -1
- package/dist/services/changes.d.ts +150 -0
- package/dist/services/changes.d.ts.map +1 -0
- package/dist/services/changes.js +339 -0
- package/dist/services/changes.js.map +1 -0
- package/dist/services/content-store.d.ts +61 -0
- package/dist/services/content-store.d.ts.map +1 -0
- package/dist/services/content-store.js +330 -0
- package/dist/services/content-store.js.map +1 -0
- package/dist/services/media.d.ts +9 -29
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/media.js +17 -57
- package/dist/services/media.js.map +1 -1
- package/dist/services/staging.d.ts +35 -167
- package/dist/services/staging.d.ts.map +1 -1
- package/dist/services/staging.js +195 -458
- package/dist/services/staging.js.map +1 -1
- package/dist/workspace/developer.d.ts +14 -0
- package/dist/workspace/developer.d.ts.map +1 -0
- package/dist/workspace/developer.js +31 -0
- package/dist/workspace/developer.js.map +1 -0
- package/dist/workspace/files.d.ts +70 -0
- package/dist/workspace/files.d.ts.map +1 -0
- package/dist/workspace/files.js +176 -0
- package/dist/workspace/files.js.map +1 -0
- package/dist/workspace/git.d.ts +45 -0
- package/dist/workspace/git.d.ts.map +1 -0
- package/dist/workspace/git.js +68 -0
- package/dist/workspace/git.js.map +1 -0
- package/dist/workspace/locks.d.ts +33 -0
- package/dist/workspace/locks.d.ts.map +1 -0
- package/dist/workspace/locks.js +76 -0
- package/dist/workspace/locks.js.map +1 -0
- package/dist/workspace/policy.d.ts +92 -0
- package/dist/workspace/policy.d.ts.map +1 -0
- package/dist/workspace/policy.js +7 -0
- package/dist/workspace/policy.js.map +1 -0
- package/dist/workspace/tree.d.ts +73 -0
- package/dist/workspace/tree.d.ts.map +1 -0
- package/dist/workspace/tree.js +229 -0
- package/dist/workspace/tree.js.map +1 -0
- package/package.json +2 -3
- package/dist/db/driver.d.ts +0 -35
- package/dist/db/driver.d.ts.map +0 -1
- package/dist/db/driver.js +0 -95
- package/dist/db/driver.js.map +0 -1
- package/dist/lib/git.d.ts +0 -29
- package/dist/lib/git.d.ts.map +0 -1
- package/dist/lib/git.js +0 -62
- package/dist/lib/git.js.map +0 -1
- package/dist/services/staging-local.d.ts +0 -58
- package/dist/services/staging-local.d.ts.map +0 -1
- package/dist/services/staging-local.js +0 -391
- package/dist/services/staging-local.js.map +0 -1
package/dist/db/driver.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQLite driver.
|
|
3
|
-
*
|
|
4
|
-
* Everything above this file talks to `SqlDatabase`, never to `node:sqlite`
|
|
5
|
-
* directly, so the driver can be swapped (better-sqlite3, libsql, a Postgres
|
|
6
|
-
* adapter) without touching the repositories.
|
|
7
|
-
*
|
|
8
|
-
* `node:sqlite` is the default because it ships with Node — no native build
|
|
9
|
-
* step, no prebuilt-binary mismatch inside the container.
|
|
10
|
-
*/
|
|
11
|
-
import { DatabaseSync } from "node:sqlite";
|
|
12
|
-
export function openSqlite(filename, options = {}) {
|
|
13
|
-
const db = new DatabaseSync(filename);
|
|
14
|
-
const cache = new Map();
|
|
15
|
-
let depth = 0;
|
|
16
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
17
|
-
db.exec(`PRAGMA busy_timeout = ${options.busyTimeoutMs ?? 5000}`);
|
|
18
|
-
if (options.wal !== false) {
|
|
19
|
-
// WAL lets the Astro build read while an editor writes.
|
|
20
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
21
|
-
// NORMAL is durable across process crashes; only an OS-level crash can lose
|
|
22
|
-
// the last transactions. The right trade-off for a content database.
|
|
23
|
-
db.exec("PRAGMA synchronous = NORMAL");
|
|
24
|
-
}
|
|
25
|
-
const prepare = (sql) => {
|
|
26
|
-
let statement = cache.get(sql);
|
|
27
|
-
if (!statement) {
|
|
28
|
-
statement = db.prepare(sql);
|
|
29
|
-
cache.set(sql, statement);
|
|
30
|
-
}
|
|
31
|
-
return statement;
|
|
32
|
-
};
|
|
33
|
-
// Rows arrive as null-prototype objects; callers expect ordinary ones.
|
|
34
|
-
const plain = (row) => ({ ...row });
|
|
35
|
-
return {
|
|
36
|
-
all(sql, params = []) {
|
|
37
|
-
return prepare(sql)
|
|
38
|
-
.all(...params)
|
|
39
|
-
.map((row) => plain(row));
|
|
40
|
-
},
|
|
41
|
-
get(sql, params = []) {
|
|
42
|
-
const row = prepare(sql).get(...params);
|
|
43
|
-
return row === undefined ? undefined : plain(row);
|
|
44
|
-
},
|
|
45
|
-
run(sql, params = []) {
|
|
46
|
-
const result = prepare(sql).run(...params);
|
|
47
|
-
return {
|
|
48
|
-
changes: Number(result.changes),
|
|
49
|
-
lastInsertRowid: result.lastInsertRowid,
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
exec(sql) {
|
|
53
|
-
db.exec(sql);
|
|
54
|
-
},
|
|
55
|
-
transaction(fn) {
|
|
56
|
-
// Nested calls use savepoints so an inner rollback does not discard the
|
|
57
|
-
// outer transaction's work.
|
|
58
|
-
const name = `sp_${depth}`;
|
|
59
|
-
const begin = depth === 0 ? "BEGIN IMMEDIATE" : `SAVEPOINT ${name}`;
|
|
60
|
-
const commit = depth === 0 ? "COMMIT" : `RELEASE ${name}`;
|
|
61
|
-
const rollback = depth === 0 ? "ROLLBACK" : `ROLLBACK TO ${name}`;
|
|
62
|
-
db.exec(begin);
|
|
63
|
-
depth++;
|
|
64
|
-
try {
|
|
65
|
-
const result = fn();
|
|
66
|
-
depth--;
|
|
67
|
-
db.exec(commit);
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
depth--;
|
|
72
|
-
try {
|
|
73
|
-
db.exec(rollback);
|
|
74
|
-
if (depth > 0)
|
|
75
|
-
db.exec(`RELEASE ${name}`);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// The rollback itself failing means the connection is unusable;
|
|
79
|
-
// surface the original error rather than masking it.
|
|
80
|
-
}
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
snapshot(destination) {
|
|
85
|
-
// VACUUM INTO takes a read lock only, so editors are not blocked and the
|
|
86
|
-
// resulting file is a defragmented, fully consistent database.
|
|
87
|
-
db.exec(`VACUUM INTO '${destination.replace(/'/g, "''")}'`);
|
|
88
|
-
},
|
|
89
|
-
close() {
|
|
90
|
-
cache.clear();
|
|
91
|
-
db.close();
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
//# sourceMappingURL=driver.js.map
|
package/dist/db/driver.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"driver.js","sourceRoot":"","sources":["../../src/db/driver.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/git.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spawning the developer's own `git`.
|
|
3
|
-
*
|
|
4
|
-
* Deliberately the binary, not a JavaScript Git implementation. The reason is
|
|
5
|
-
* credentials: a developer's push already works because of their SSH agent,
|
|
6
|
-
* credential helper and remote configuration, and the spawned binary inherits
|
|
7
|
-
* all three. A library would have to reimplement them, and would do it worse
|
|
8
|
-
* — see decisions.md, "Local publishing spawns the `git` binary".
|
|
9
|
-
*
|
|
10
|
-
* `GIT_TERMINAL_PROMPT=0` because there is no terminal to answer: a push that
|
|
11
|
-
* would ask for credentials must fail with Git's own message rather than hang
|
|
12
|
-
* the request that triggered it.
|
|
13
|
-
*/
|
|
14
|
-
export declare class GitError extends Error {
|
|
15
|
-
readonly stderr: string;
|
|
16
|
-
readonly exitCode: number | null;
|
|
17
|
-
name: string;
|
|
18
|
-
constructor(message: string, stderr: string, exitCode: number | null);
|
|
19
|
-
}
|
|
20
|
-
export interface GitRunner {
|
|
21
|
-
/** Runs `git` in the runner's directory. Trimmed stdout, or a `GitError`. */
|
|
22
|
-
run(args: string[]): Promise<string>;
|
|
23
|
-
/** Like `run`, but null instead of a throw — for questions Git may say no to. */
|
|
24
|
-
tryRun(args: string[]): Promise<string | null>;
|
|
25
|
-
}
|
|
26
|
-
export declare function gitRunner(cwd: string): GitRunner;
|
|
27
|
-
/** The line a person should see first — Git puts its verdict there. */
|
|
28
|
-
export declare function firstLine(text: string): string;
|
|
29
|
-
//# sourceMappingURL=git.d.ts.map
|
package/dist/lib/git.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/lib/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,qBAAa,QAAS,SAAQ,KAAK;IAI/B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAJzB,IAAI,SAAc;gBAEzB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GAAG,IAAI;CAInC;AAED,MAAM,WAAW,SAAS;IACxB,6EAA6E;IAC7E,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,iFAAiF;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAChD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAyChD;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO9C"}
|
package/dist/lib/git.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spawning the developer's own `git`.
|
|
3
|
-
*
|
|
4
|
-
* Deliberately the binary, not a JavaScript Git implementation. The reason is
|
|
5
|
-
* credentials: a developer's push already works because of their SSH agent,
|
|
6
|
-
* credential helper and remote configuration, and the spawned binary inherits
|
|
7
|
-
* all three. A library would have to reimplement them, and would do it worse
|
|
8
|
-
* — see decisions.md, "Local publishing spawns the `git` binary".
|
|
9
|
-
*
|
|
10
|
-
* `GIT_TERMINAL_PROMPT=0` because there is no terminal to answer: a push that
|
|
11
|
-
* would ask for credentials must fail with Git's own message rather than hang
|
|
12
|
-
* the request that triggered it.
|
|
13
|
-
*/
|
|
14
|
-
import { execFile } from "node:child_process";
|
|
15
|
-
export class GitError extends Error {
|
|
16
|
-
stderr;
|
|
17
|
-
exitCode;
|
|
18
|
-
name = "GitError";
|
|
19
|
-
constructor(message, stderr, exitCode) {
|
|
20
|
-
super(message);
|
|
21
|
-
this.stderr = stderr;
|
|
22
|
-
this.exitCode = exitCode;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
export function gitRunner(cwd) {
|
|
26
|
-
const run = (args) => new Promise((resolve, reject) => {
|
|
27
|
-
execFile("git", args, {
|
|
28
|
-
cwd,
|
|
29
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
30
|
-
timeout: 60_000,
|
|
31
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
32
|
-
}, (error, stdout, stderr) => {
|
|
33
|
-
if (error) {
|
|
34
|
-
const missing = error.code === "ENOENT";
|
|
35
|
-
reject(new GitError(missing
|
|
36
|
-
? "Git is not installed, or not on the PATH."
|
|
37
|
-
: firstLine(stderr) || error.message, stderr, missing ? null : (error.code ?? null)));
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
resolve(stdout.replace(/\n$/, ""));
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
return {
|
|
44
|
-
run,
|
|
45
|
-
async tryRun(args) {
|
|
46
|
-
try {
|
|
47
|
-
return await run(args);
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
/** The line a person should see first — Git puts its verdict there. */
|
|
56
|
-
export function firstLine(text) {
|
|
57
|
-
return (text
|
|
58
|
-
.split("\n")
|
|
59
|
-
.map((line) => line.trim())
|
|
60
|
-
.find((line) => line !== "") ?? "");
|
|
61
|
-
}
|
|
62
|
-
//# sourceMappingURL=git.js.map
|
package/dist/lib/git.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/lib/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,MAAM,OAAO,QAAS,SAAQ,KAAK;IAItB;IACA;IAJF,IAAI,GAAG,UAAU,CAAC;IAC3B,YACE,OAAe,EACN,MAAc,EACd,QAAuB;QAEhC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAe;IAGlC,CAAC;CACF;AASD,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,GAAG,GAAG,CAAC,IAAc,EAAmB,EAAE,CAC9C,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC9B,QAAQ,CACN,KAAK,EACL,IAAI,EACJ;YACE,GAAG;YACH,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,mBAAmB,EAAE,GAAG,EAAE;YACjD,OAAO,EAAE,MAAM;YACf,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;SAC5B,EACD,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,OAAO,GAAI,KAA+B,CAAC,IAAI,KAAK,QAAQ,CAAC;gBACnE,MAAM,CACJ,IAAI,QAAQ,CACV,OAAO;oBACL,CAAC,CAAC,2CAA2C;oBAC7C,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,EACtC,MAAM,EACN,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,KAA2B,CAAC,IAAI,IAAI,IAAI,CAAC,CAC7D,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QACrC,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEL,OAAO;QACL,GAAG;QACH,KAAK,CAAC,MAAM,CAAC,IAAI;YACf,IAAI,CAAC;gBACH,OAAO,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,CACL,IAAI;SACD,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,EAAE,CACrC,CAAC;AACJ,CAAC"}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Working-tree staging: local mode's half of the staging service.
|
|
3
|
-
*
|
|
4
|
-
* The checked-out working tree *is* the draft workspace. Autosave has already
|
|
5
|
-
* written the files — that is what lets the Astro dev server hot-reload them,
|
|
6
|
-
* which is the whole value of local mode — so this service's job is reading
|
|
7
|
-
* what Git thinks of the tree and, on Publish, turning exactly the reviewed
|
|
8
|
-
* paths into one commit and one push.
|
|
9
|
-
*
|
|
10
|
-
* Three rules keep it safe in a repository that belongs to a developer:
|
|
11
|
-
*
|
|
12
|
-
* - Only Mica-managed paths. Every Git operation is limited to an explicit
|
|
13
|
-
* path set derived from the change list — never `git add -A` over the
|
|
14
|
-
* repository, so unrelated work in the same checkout is never swept in.
|
|
15
|
-
* - HEAD is verified against the base the editor reviewed. A repository
|
|
16
|
-
* that moved refuses, like a provider branch that moved.
|
|
17
|
-
* - A commit and a push are two operations. When the push fails the commit
|
|
18
|
-
* is retained and reported as `committed-pending-push`; a retry pushes
|
|
19
|
-
* again and never creates a second commit.
|
|
20
|
-
*/
|
|
21
|
-
import type { RepositoryConnector } from "@micacms/connectors";
|
|
22
|
-
import { type ContentChange, type Site } from "@micacms/core";
|
|
23
|
-
import type { Logger } from "../lib/logger.js";
|
|
24
|
-
import { type ContentStagingService } from "./staging.js";
|
|
25
|
-
export interface WorkingTreeStagingDeps {
|
|
26
|
-
model: Site;
|
|
27
|
-
/** Reads the working tree — the same local connector autosave writes through. */
|
|
28
|
-
connector: RepositoryConnector;
|
|
29
|
-
/** Absolute path of the content root. */
|
|
30
|
-
contentRoot: string;
|
|
31
|
-
/** Revision of `.mica/schema.json` when the model was loaded. */
|
|
32
|
-
schemaRevision: string | null;
|
|
33
|
-
/** `MICA_LOCAL_PUBLISH`. False reduces this to a read-only changes view. */
|
|
34
|
-
publishEnabled: boolean;
|
|
35
|
-
logger: Logger;
|
|
36
|
-
}
|
|
37
|
-
/** A changed file plus what only Git status knows about it. */
|
|
38
|
-
export interface TreeChange extends ContentChange {
|
|
39
|
-
/** Not yet known to Git — discarding means deleting, not restoring. */
|
|
40
|
-
untracked: boolean;
|
|
41
|
-
/** Mid-merge conflict markers. Unpublishable until resolved with Git tools. */
|
|
42
|
-
conflicted: boolean;
|
|
43
|
-
}
|
|
44
|
-
export declare function createWorkingTreeStagingService(deps: WorkingTreeStagingDeps): ContentStagingService;
|
|
45
|
-
/**
|
|
46
|
-
* Parses `git status --porcelain=v1 -z` into content-root-relative changes.
|
|
47
|
-
*
|
|
48
|
-
* The `-z` format is the one that never munges a path: entries are
|
|
49
|
-
* NUL-terminated, and a rename is two fields — the new path, then the one it
|
|
50
|
-
* came from. A rename becomes a create and a delete, which is how the rest of
|
|
51
|
-
* Mica already thinks about moves.
|
|
52
|
-
*
|
|
53
|
-
* `.mica/` is excluded. The manifest there is build output a developer
|
|
54
|
-
* commits with their own tools; the editor neither shows nor publishes it,
|
|
55
|
-
* and model changes are guarded separately, by the drift check.
|
|
56
|
-
*/
|
|
57
|
-
export declare function parseStatus(raw: string, prefix: string): TreeChange[];
|
|
58
|
-
//# sourceMappingURL=staging-local.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"staging-local.d.ts","sourceRoot":"","sources":["../../src/services/staging-local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAGL,KAAK,aAAa,EAIlB,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AAKvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAOL,KAAK,qBAAqB,EAI3B,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,IAAI,CAAC;IACZ,iFAAiF;IACjF,SAAS,EAAE,mBAAmB,CAAC;IAC/B,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,4EAA4E;IAC5E,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAW,SAAQ,aAAa;IAC/C,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,UAAU,EAAE,OAAO,CAAC;CACrB;AAQD,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,sBAAsB,GAC3B,qBAAqB,CAgRvB;AAmED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,CAmDrE"}
|
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Working-tree staging: local mode's half of the staging service.
|
|
3
|
-
*
|
|
4
|
-
* The checked-out working tree *is* the draft workspace. Autosave has already
|
|
5
|
-
* written the files — that is what lets the Astro dev server hot-reload them,
|
|
6
|
-
* which is the whole value of local mode — so this service's job is reading
|
|
7
|
-
* what Git thinks of the tree and, on Publish, turning exactly the reviewed
|
|
8
|
-
* paths into one commit and one push.
|
|
9
|
-
*
|
|
10
|
-
* Three rules keep it safe in a repository that belongs to a developer:
|
|
11
|
-
*
|
|
12
|
-
* - Only Mica-managed paths. Every Git operation is limited to an explicit
|
|
13
|
-
* path set derived from the change list — never `git add -A` over the
|
|
14
|
-
* repository, so unrelated work in the same checkout is never swept in.
|
|
15
|
-
* - HEAD is verified against the base the editor reviewed. A repository
|
|
16
|
-
* that moved refuses, like a provider branch that moved.
|
|
17
|
-
* - A commit and a push are two operations. When the push fails the commit
|
|
18
|
-
* is retained and reported as `committed-pending-push`; a retry pushes
|
|
19
|
-
* again and never creates a second commit.
|
|
20
|
-
*/
|
|
21
|
-
import { realpath, rm } from "node:fs/promises";
|
|
22
|
-
import { join, relative, sep } from "node:path";
|
|
23
|
-
import { MANIFEST_PATH, memorySource, } from "@micacms/core";
|
|
24
|
-
import { within } from "@micacms/core/node";
|
|
25
|
-
import { ApiError, badRequest } from "../lib/errors.js";
|
|
26
|
-
import { firstLine, gitRunner, GitError } from "../lib/git.js";
|
|
27
|
-
import { analyseChanges, compareSources, describeProblems, flattenProblems, } from "./staging.js";
|
|
28
|
-
export function createWorkingTreeStagingService(deps) {
|
|
29
|
-
const { model, connector, contentRoot, logger } = deps;
|
|
30
|
-
// One Git mutation at a time. Editing is untouched — autosave writes files,
|
|
31
|
-
// not Git — this only refuses to interleave two commits or pushes.
|
|
32
|
-
let running = false;
|
|
33
|
-
/**
|
|
34
|
-
* Finds the repository the content root sits in, freshly each time: a
|
|
35
|
-
* `git init` after startup should start working, not require a restart.
|
|
36
|
-
*/
|
|
37
|
-
const locate = async () => {
|
|
38
|
-
const probe = gitRunner(contentRoot);
|
|
39
|
-
try {
|
|
40
|
-
const repoRoot = await probe.run(["rev-parse", "--show-toplevel"]);
|
|
41
|
-
// Git reports the toplevel with symlinks resolved (`/private/var` where
|
|
42
|
-
// the caller said `/var`, on macOS). Resolve our side the same way, or
|
|
43
|
-
// the relative prefix walks out of the repository.
|
|
44
|
-
const resolved = await realpath(contentRoot);
|
|
45
|
-
return { git: gitRunner(repoRoot), prefix: toPosix(relative(repoRoot, resolved)) };
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
return {
|
|
49
|
-
reason: error instanceof GitError && error.exitCode === null
|
|
50
|
-
? error.message
|
|
51
|
-
: "The content directory is not inside a Git repository.",
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
const listChanges = async (workspace) => {
|
|
56
|
-
// `-uall` because Git collapses a fully-untracked directory to one line,
|
|
57
|
-
// and a brand-new entry is exactly that. The change list must name files.
|
|
58
|
-
const raw = await workspace.git.run([
|
|
59
|
-
"status",
|
|
60
|
-
"--porcelain=v1",
|
|
61
|
-
"-z",
|
|
62
|
-
"-uall",
|
|
63
|
-
"--",
|
|
64
|
-
workspace.prefix === "" ? "." : workspace.prefix,
|
|
65
|
-
]);
|
|
66
|
-
return parseStatus(raw, workspace.prefix);
|
|
67
|
-
};
|
|
68
|
-
const analyse = async (workspace, treeChanges) => {
|
|
69
|
-
const { changes, validation } = await analyseChanges(model, connector, treeChanges, headSource(workspace));
|
|
70
|
-
// A merge conflict is a validity problem the field validator cannot see:
|
|
71
|
-
// the file on disk is Git's conflict format, not content.
|
|
72
|
-
for (const change of treeChanges) {
|
|
73
|
-
if (!change.conflicted)
|
|
74
|
-
continue;
|
|
75
|
-
validation.problems.push({
|
|
76
|
-
path: change.path,
|
|
77
|
-
label: change.path,
|
|
78
|
-
issues: [
|
|
79
|
-
{ path: "", message: "This file has a Git merge conflict. Resolve it with your Git tools." },
|
|
80
|
-
],
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
validation.ok = validation.problems.length === 0;
|
|
84
|
-
return { changes, validation };
|
|
85
|
-
};
|
|
86
|
-
const guardPublishable = async () => {
|
|
87
|
-
if (!deps.publishEnabled) {
|
|
88
|
-
throw badRequest("Publishing from local mode is disabled (MICA_LOCAL_PUBLISH=false).");
|
|
89
|
-
}
|
|
90
|
-
const located = await locate();
|
|
91
|
-
if ("reason" in located)
|
|
92
|
-
throw badRequest(located.reason);
|
|
93
|
-
return located;
|
|
94
|
-
};
|
|
95
|
-
/** Runs `fn` holding the one-mutation-at-a-time flag. */
|
|
96
|
-
const exclusive = async (fn) => {
|
|
97
|
-
if (running) {
|
|
98
|
-
throw new ApiError("conflict", "A publication is already running. Wait for it to finish.");
|
|
99
|
-
}
|
|
100
|
-
running = true;
|
|
101
|
-
try {
|
|
102
|
-
return await fn();
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
running = false;
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
const pushOutcome = async (git, revision) => {
|
|
109
|
-
try {
|
|
110
|
-
await git.run(["push"]);
|
|
111
|
-
return { state: "published", revision };
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
// The commit is real and retained; only the second half failed. This is
|
|
115
|
-
// a state, not an exception — the editor shows it and offers a retry.
|
|
116
|
-
const pushError = error instanceof GitError ? error.message : String(error);
|
|
117
|
-
logger.warn("Publish committed but the push failed", { revision, pushError });
|
|
118
|
-
return { state: "committed-pending-push", revision, pushError };
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
return {
|
|
122
|
-
async status() {
|
|
123
|
-
const located = await locate();
|
|
124
|
-
if ("reason" in located) {
|
|
125
|
-
return {
|
|
126
|
-
workspace: "working-tree",
|
|
127
|
-
publish: { enabled: false, reason: located.reason },
|
|
128
|
-
baseRevision: null,
|
|
129
|
-
branch: null,
|
|
130
|
-
ahead: null,
|
|
131
|
-
changes: [],
|
|
132
|
-
validation: emptyValidation(),
|
|
133
|
-
updatedAt: null,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
const treeChanges = await listChanges(located);
|
|
137
|
-
const { changes, validation } = await analyse(located, treeChanges);
|
|
138
|
-
return {
|
|
139
|
-
workspace: "working-tree",
|
|
140
|
-
publish: deps.publishEnabled
|
|
141
|
-
? { enabled: true }
|
|
142
|
-
: { enabled: false, reason: "Disabled with MICA_LOCAL_PUBLISH=false." },
|
|
143
|
-
baseRevision: await located.git.tryRun(["rev-parse", "HEAD"]),
|
|
144
|
-
branch: await located.git.tryRun(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
145
|
-
ahead: await aheadOfUpstream(located.git),
|
|
146
|
-
changes,
|
|
147
|
-
validation,
|
|
148
|
-
updatedAt: null,
|
|
149
|
-
};
|
|
150
|
-
},
|
|
151
|
-
async publish(input) {
|
|
152
|
-
const message = input.message.trim();
|
|
153
|
-
if (!message)
|
|
154
|
-
throw badRequest("A publication needs a commit message.");
|
|
155
|
-
const workspace = await guardPublishable();
|
|
156
|
-
return exclusive(async () => {
|
|
157
|
-
const treeChanges = await listChanges(workspace);
|
|
158
|
-
if (treeChanges.length === 0)
|
|
159
|
-
throw badRequest("There is nothing to publish.");
|
|
160
|
-
/**
|
|
161
|
-
* The reviewed base. The working tree has no per-path gate the way a
|
|
162
|
-
* provider commit does — the tree is already the truth — so the gate
|
|
163
|
-
* is HEAD: if the repository moved since the editor reviewed the
|
|
164
|
-
* change list, what a commit would be based on is not what they saw.
|
|
165
|
-
*/
|
|
166
|
-
const head = await workspace.git.tryRun(["rev-parse", "HEAD"]);
|
|
167
|
-
if (input.expectedBase !== head) {
|
|
168
|
-
throw new ApiError("conflict", "The repository moved while these changes were being reviewed. Refresh the " +
|
|
169
|
-
"change list and publish again.");
|
|
170
|
-
}
|
|
171
|
-
// The model drift gate — same rule as the draft store: the staged set
|
|
172
|
-
// was validated against the model this process started with.
|
|
173
|
-
if ((await connector.revision(MANIFEST_PATH)) !== deps.schemaRevision) {
|
|
174
|
-
throw new ApiError("conflict", "The content model changed after this server started. Restart Mica to load " +
|
|
175
|
-
"the new model, then review the pending changes and publish again.");
|
|
176
|
-
}
|
|
177
|
-
const { validation } = await analyse(workspace, treeChanges);
|
|
178
|
-
if (!validation.ok) {
|
|
179
|
-
throw new ApiError("validation_failed", describeProblems(validation.problems), {
|
|
180
|
-
issues: flattenProblems(validation.problems),
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
const paths = treeChanges.map((change) => inRepo(workspace.prefix, change.path));
|
|
184
|
-
try {
|
|
185
|
-
// Stage and commit exactly the reviewed set. The pathspec limits
|
|
186
|
-
// both operations, so anything else in the developer's index or
|
|
187
|
-
// tree — staged or not — stays theirs.
|
|
188
|
-
for (const batch of chunked(paths, 500)) {
|
|
189
|
-
await workspace.git.run(["add", "-A", "--", ...batch]);
|
|
190
|
-
}
|
|
191
|
-
await workspace.git.run(["commit", "-m", message, "--", ...paths]);
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
if (error instanceof GitError) {
|
|
195
|
-
// Git's own verdict is the actionable part: no identity
|
|
196
|
-
// configured, a hook that refused, an unborn branch mid-rebase.
|
|
197
|
-
throw new ApiError("conflict", `Git refused the commit: ${error.message}`);
|
|
198
|
-
}
|
|
199
|
-
throw error;
|
|
200
|
-
}
|
|
201
|
-
const revision = await workspace.git.run(["rev-parse", "HEAD"]);
|
|
202
|
-
logger.info("Published from the working tree", { revision, files: paths.length });
|
|
203
|
-
return pushOutcome(workspace.git, revision);
|
|
204
|
-
});
|
|
205
|
-
},
|
|
206
|
-
async push() {
|
|
207
|
-
const workspace = await guardPublishable();
|
|
208
|
-
return exclusive(async () => {
|
|
209
|
-
const revision = await workspace.git.tryRun(["rev-parse", "HEAD"]);
|
|
210
|
-
if (revision === null)
|
|
211
|
-
throw badRequest("There is no commit to push yet.");
|
|
212
|
-
return pushOutcome(workspace.git, revision);
|
|
213
|
-
});
|
|
214
|
-
},
|
|
215
|
-
async discard(requested) {
|
|
216
|
-
const located = await locate();
|
|
217
|
-
if ("reason" in located)
|
|
218
|
-
throw badRequest(located.reason);
|
|
219
|
-
return exclusive(async () => {
|
|
220
|
-
// Only paths the change list actually names. Discard is the one
|
|
221
|
-
// destructive operation here, and it must not be able to reach
|
|
222
|
-
// anything the editor was not shown.
|
|
223
|
-
const current = new Map((await listChanges(located)).map((change) => [change.path, change]));
|
|
224
|
-
const discarded = [];
|
|
225
|
-
for (const path of requested) {
|
|
226
|
-
const change = current.get(path);
|
|
227
|
-
if (!change)
|
|
228
|
-
continue;
|
|
229
|
-
if (change.kind === "create") {
|
|
230
|
-
// Nothing to restore: before this change there was no file. A
|
|
231
|
-
// creation the developer had already staged is unstaged first,
|
|
232
|
-
// so the index does not keep pointing at a file that is gone.
|
|
233
|
-
if (!change.untracked) {
|
|
234
|
-
await located.git.tryRun([
|
|
235
|
-
"rm",
|
|
236
|
-
"--cached",
|
|
237
|
-
"--force",
|
|
238
|
-
"--",
|
|
239
|
-
inRepo(located.prefix, path),
|
|
240
|
-
]);
|
|
241
|
-
}
|
|
242
|
-
await rm(within(contentRoot, path), { force: true });
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
await located.git.run([
|
|
246
|
-
"restore",
|
|
247
|
-
"--source=HEAD",
|
|
248
|
-
"--staged",
|
|
249
|
-
"--worktree",
|
|
250
|
-
"--",
|
|
251
|
-
inRepo(located.prefix, path),
|
|
252
|
-
]);
|
|
253
|
-
}
|
|
254
|
-
discarded.push(path);
|
|
255
|
-
}
|
|
256
|
-
return { discarded };
|
|
257
|
-
});
|
|
258
|
-
},
|
|
259
|
-
async compare(target) {
|
|
260
|
-
const located = await locate();
|
|
261
|
-
// Without a repository (or before the first commit) nothing has ever
|
|
262
|
-
// been published, so the honest "before" is a source with no files.
|
|
263
|
-
const published = "reason" in located ? memorySource({}, "unpublished") : headSource(located);
|
|
264
|
-
return compareSources(model, published, connector, target);
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* The content root as HEAD has it — the published side of a comparison.
|
|
270
|
-
*
|
|
271
|
-
* Reading `HEAD:` rather than the tree is the point: the working tree *is*
|
|
272
|
-
* the draft workspace here, so the repository's own version of a document
|
|
273
|
-
* only exists in Git's object store.
|
|
274
|
-
*/
|
|
275
|
-
function headSource(workspace) {
|
|
276
|
-
const at = (path) => `HEAD:${path === "" ? workspace.prefix : inRepo(workspace.prefix, path)}`;
|
|
277
|
-
return {
|
|
278
|
-
origin: "HEAD",
|
|
279
|
-
async read(path) {
|
|
280
|
-
// tryRun, because "not in HEAD" and "no commits yet" both simply mean
|
|
281
|
-
// there is no published version. Stdout arrives trimmed, which the
|
|
282
|
-
// entry parsers already tolerate.
|
|
283
|
-
return workspace.git.tryRun(["show", at(path)]);
|
|
284
|
-
},
|
|
285
|
-
async list(path) {
|
|
286
|
-
const raw = await workspace.git.tryRun(["ls-tree", "-z", at(path)]);
|
|
287
|
-
if (raw === null)
|
|
288
|
-
return [];
|
|
289
|
-
const entries = [];
|
|
290
|
-
for (const field of raw.split("\0")) {
|
|
291
|
-
const tab = field.indexOf("\t");
|
|
292
|
-
if (tab === -1)
|
|
293
|
-
continue;
|
|
294
|
-
const type = field.slice(0, tab).split(" ")[1];
|
|
295
|
-
entries.push({ name: field.slice(tab + 1), type: type === "tree" ? "directory" : "file" });
|
|
296
|
-
}
|
|
297
|
-
return entries;
|
|
298
|
-
},
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
/* ------------------------------------------------------------------ */
|
|
302
|
-
function emptyValidation() {
|
|
303
|
-
return { ok: true, problems: [] };
|
|
304
|
-
}
|
|
305
|
-
function toPosix(path) {
|
|
306
|
-
return sep === "/" ? path : path.split(sep).join("/");
|
|
307
|
-
}
|
|
308
|
-
/** A content-root-relative path as Git sees it, from the repository root. */
|
|
309
|
-
function inRepo(prefix, path) {
|
|
310
|
-
return prefix === "" ? path : join(prefix, path).split(sep).join("/");
|
|
311
|
-
}
|
|
312
|
-
function chunked(items, size) {
|
|
313
|
-
const batches = [];
|
|
314
|
-
for (let at = 0; at < items.length; at += size)
|
|
315
|
-
batches.push(items.slice(at, at + size));
|
|
316
|
-
return batches;
|
|
317
|
-
}
|
|
318
|
-
/** Commits waiting to be pushed, or null when there is no upstream to say. */
|
|
319
|
-
async function aheadOfUpstream(git) {
|
|
320
|
-
const counted = await git.tryRun(["rev-list", "--count", "@{upstream}..HEAD"]);
|
|
321
|
-
if (counted === null)
|
|
322
|
-
return null;
|
|
323
|
-
const ahead = Number(counted);
|
|
324
|
-
return Number.isFinite(ahead) ? ahead : null;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Parses `git status --porcelain=v1 -z` into content-root-relative changes.
|
|
328
|
-
*
|
|
329
|
-
* The `-z` format is the one that never munges a path: entries are
|
|
330
|
-
* NUL-terminated, and a rename is two fields — the new path, then the one it
|
|
331
|
-
* came from. A rename becomes a create and a delete, which is how the rest of
|
|
332
|
-
* Mica already thinks about moves.
|
|
333
|
-
*
|
|
334
|
-
* `.mica/` is excluded. The manifest there is build output a developer
|
|
335
|
-
* commits with their own tools; the editor neither shows nor publishes it,
|
|
336
|
-
* and model changes are guarded separately, by the drift check.
|
|
337
|
-
*/
|
|
338
|
-
export function parseStatus(raw, prefix) {
|
|
339
|
-
const fields = raw.split("\0").filter((field) => field !== "");
|
|
340
|
-
const changes = [];
|
|
341
|
-
const push = (repoPath, kind, flags = {}) => {
|
|
342
|
-
const path = relativeToContent(repoPath, prefix);
|
|
343
|
-
if (path === null || path === ".mica" || path.startsWith(".mica/"))
|
|
344
|
-
return;
|
|
345
|
-
changes.push({
|
|
346
|
-
path,
|
|
347
|
-
kind,
|
|
348
|
-
untracked: flags.untracked ?? false,
|
|
349
|
-
conflicted: flags.conflicted ?? false,
|
|
350
|
-
});
|
|
351
|
-
};
|
|
352
|
-
for (let at = 0; at < fields.length; at += 1) {
|
|
353
|
-
const field = fields[at];
|
|
354
|
-
const x = field[0] ?? " ";
|
|
355
|
-
const y = field[1] ?? " ";
|
|
356
|
-
const repoPath = field.slice(3);
|
|
357
|
-
if (x === "?" && y === "?") {
|
|
358
|
-
push(repoPath, "create", { untracked: true });
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
// Both sides touched it in a merge, or one side deleted while the other
|
|
362
|
-
// edited: the file is in Git's conflict state, whatever the tree holds.
|
|
363
|
-
if (x === "U" || y === "U" || (x === "A" && y === "A") || (x === "D" && y === "D")) {
|
|
364
|
-
push(repoPath, "update", { conflicted: true });
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
if (x === "R" || x === "C") {
|
|
368
|
-
// The next field is the source path. A copy keeps its source; a rename
|
|
369
|
-
// loses it, which the status letters already said (C keeps both files).
|
|
370
|
-
const from = fields[++at];
|
|
371
|
-
push(repoPath, "create");
|
|
372
|
-
if (x === "R")
|
|
373
|
-
push(from, "delete");
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (x === "D" || y === "D") {
|
|
377
|
-
push(repoPath, "delete");
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
push(repoPath, x === "A" ? "create" : "update");
|
|
381
|
-
}
|
|
382
|
-
return changes;
|
|
383
|
-
}
|
|
384
|
-
function relativeToContent(repoPath, prefix) {
|
|
385
|
-
if (prefix === "")
|
|
386
|
-
return repoPath;
|
|
387
|
-
if (repoPath === prefix)
|
|
388
|
-
return null;
|
|
389
|
-
return repoPath.startsWith(`${prefix}/`) ? repoPath.slice(prefix.length + 1) : null;
|
|
390
|
-
}
|
|
391
|
-
//# sourceMappingURL=staging-local.js.map
|