carboncanvas-cli 0.1.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/CONTRACT.md +94 -0
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/build.js +32 -0
- package/dist/build.js.map +1 -0
- package/dist/canvasConfig.js +19 -0
- package/dist/canvasConfig.js.map +1 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/detect.js +80 -0
- package/dist/detect.js.map +1 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.js +25 -0
- package/dist/logger.js.map +1 -0
- package/dist/loginCommand.js +33 -0
- package/dist/loginCommand.js.map +1 -0
- package/dist/names.js +87 -0
- package/dist/names.js.map +1 -0
- package/dist/output.js +88 -0
- package/dist/output.js.map +1 -0
- package/dist/pack.js +65 -0
- package/dist/pack.js.map +1 -0
- package/dist/prompt.js +38 -0
- package/dist/prompt.js.map +1 -0
- package/dist/publishCommand.js +129 -0
- package/dist/publishCommand.js.map +1 -0
- package/dist/stamp.js +67 -0
- package/dist/stamp.js.map +1 -0
- package/dist/tokenStore.js +67 -0
- package/dist/tokenStore.js.map +1 -0
- package/dist/upload.js +83 -0
- package/dist/upload.js.map +1 -0
- package/dist/verify.js +25 -0
- package/dist/verify.js.map +1 -0
- package/dist/walk.js +44 -0
- package/dist/walk.js.map +1 -0
- package/package.json +42 -0
package/dist/upload.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Upload half of the publish contract. THE SHAPE HERE IS FIXED — it is the
|
|
2
|
+
// source of truth for the server (see CONTRACT.md), the way networkClient.ts is
|
|
3
|
+
// for comments. Do not change one side without the other.
|
|
4
|
+
//
|
|
5
|
+
// POST {serviceUrl}/api/publish
|
|
6
|
+
// Authorization: Bearer <token>
|
|
7
|
+
// multipart/form-data:
|
|
8
|
+
// bundle : .tar.gz of the dist dir (filename bundle.tar.gz)
|
|
9
|
+
// meta : JSON string (PublishMeta)
|
|
10
|
+
//
|
|
11
|
+
// 200 → PublishResult
|
|
12
|
+
// 401 → invalid/expired token
|
|
13
|
+
// 402 → { error, message } publish capability not entitled
|
|
14
|
+
// 400 → { error, message } validation (size cap / bad bundle)
|
|
15
|
+
// 413 → too large
|
|
16
|
+
// 503 → { error, message } publish/provider not configured server-side
|
|
17
|
+
import { promises as fs } from 'node:fs';
|
|
18
|
+
export class PublishError extends Error {
|
|
19
|
+
status;
|
|
20
|
+
// A secondary line (e.g. upgrade hint) shown under the main message.
|
|
21
|
+
hint;
|
|
22
|
+
constructor(message, status, hint) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'PublishError';
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.hint = hint;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function readErrorBody(res) {
|
|
30
|
+
try {
|
|
31
|
+
return (await res.json());
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Build the multipart form for the request. Exposed for tests.
|
|
38
|
+
export async function buildPublishForm(tarPath, meta) {
|
|
39
|
+
const buf = await fs.readFile(tarPath);
|
|
40
|
+
const form = new FormData();
|
|
41
|
+
// Uint8Array view keeps types happy across Node's Blob signature.
|
|
42
|
+
const blob = new Blob([new Uint8Array(buf)], { type: 'application/gzip' });
|
|
43
|
+
form.append('bundle', blob, 'bundle.tar.gz');
|
|
44
|
+
form.append('meta', JSON.stringify(meta));
|
|
45
|
+
return form;
|
|
46
|
+
}
|
|
47
|
+
export async function publishBundle(serviceUrl, token, tarPath, meta, fetchImpl = fetch) {
|
|
48
|
+
const base = serviceUrl.replace(/\/+$/, '');
|
|
49
|
+
const form = await buildPublishForm(tarPath, meta);
|
|
50
|
+
let res;
|
|
51
|
+
try {
|
|
52
|
+
res = await fetchImpl(`${base}/api/publish`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
55
|
+
body: form,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
60
|
+
throw new PublishError(`Could not reach the Carbon Canvas backend at ${base} (${detail}). ` +
|
|
61
|
+
'Check your network and --service-url, then retry.');
|
|
62
|
+
}
|
|
63
|
+
if (res.status === 200) {
|
|
64
|
+
return (await res.json());
|
|
65
|
+
}
|
|
66
|
+
const body = await readErrorBody(res);
|
|
67
|
+
switch (res.status) {
|
|
68
|
+
case 401:
|
|
69
|
+
throw new PublishError('Your token is invalid or expired. Run `carboncanvas login` to paste a fresh token.', 401);
|
|
70
|
+
case 402:
|
|
71
|
+
throw new PublishError(body.message ?? 'Publishing is not included in your current plan.', 402, 'Upgrade your plan on the dashboard to enable `publish`.');
|
|
72
|
+
case 400:
|
|
73
|
+
throw new PublishError(body.message ?? 'The server rejected the bundle (validation failed).', 400);
|
|
74
|
+
case 413:
|
|
75
|
+
throw new PublishError(body.message ?? 'The bundle is too large for the server to accept.', 413);
|
|
76
|
+
case 503:
|
|
77
|
+
throw new PublishError(body.message ??
|
|
78
|
+
'Publishing is not configured on this server (no CDN provider). Contact the operator.', 503);
|
|
79
|
+
default:
|
|
80
|
+
throw new PublishError(body.message ?? `Publish failed with HTTP ${res.status}.`, res.status);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Token verification against the backend.
|
|
2
|
+
//
|
|
3
|
+
// A creator API token is valid iff `GET /api/entitlements` with
|
|
4
|
+
// `Authorization: Bearer <token>` returns 200 (the endpoint is session-gated —
|
|
5
|
+
// server/src/routes/entitlements.ts — so 401 means the token is not a live
|
|
6
|
+
// session). We inject `fetchImpl` for testability; it defaults to global fetch.
|
|
7
|
+
export async function verifyToken(serviceUrl, token, fetchImpl = fetch) {
|
|
8
|
+
const base = serviceUrl.replace(/\/+$/, '');
|
|
9
|
+
const res = await fetchImpl(`${base}/api/entitlements`, {
|
|
10
|
+
method: 'GET',
|
|
11
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
12
|
+
});
|
|
13
|
+
if (res.status === 200) {
|
|
14
|
+
let entitlement;
|
|
15
|
+
try {
|
|
16
|
+
entitlement = await res.json();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
entitlement = undefined;
|
|
20
|
+
}
|
|
21
|
+
return { ok: true, status: 200, entitlement };
|
|
22
|
+
}
|
|
23
|
+
return { ok: false, status: res.status };
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,EAAE;AACF,gEAAgE;AAChE,+EAA+E;AAC/E,2EAA2E;AAC3E,gFAAgF;AAWhF,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,UAAkB,EAClB,KAAa,EACb,YAAuB,KAAK;IAE5B,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,IAAI,mBAAmB,EAAE;QACtD,MAAM,EAAE,KAAK;QACb,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;KAC9C,CAAC,CAAC;IACH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvB,IAAI,WAAoB,CAAC;QACzB,IAAI,CAAC;YACH,WAAW,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,WAAW,GAAG,SAAS,CAAC;QAC1B,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC;IAChD,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;AAC3C,CAAC"}
|
package/dist/walk.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Recursive directory walk returning relative file paths + sizes. Shared by the
|
|
2
|
+
// packer (caps), the canvasId grep, and the name-preservation check.
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
export async function walkDir(root) {
|
|
6
|
+
const out = [];
|
|
7
|
+
async function recurse(dir) {
|
|
8
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
9
|
+
for (const e of entries) {
|
|
10
|
+
const abs = path.join(dir, e.name);
|
|
11
|
+
if (e.isDirectory()) {
|
|
12
|
+
await recurse(abs);
|
|
13
|
+
}
|
|
14
|
+
else if (e.isFile()) {
|
|
15
|
+
const st = await fs.stat(abs);
|
|
16
|
+
out.push({
|
|
17
|
+
rel: path.relative(root, abs).split(path.sep).join('/'),
|
|
18
|
+
abs,
|
|
19
|
+
size: st.size,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
await recurse(root);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
// Read and concatenate the text of files matching an extension set. Used to grep
|
|
28
|
+
// the built bundle. Caps total read to avoid pathological memory use.
|
|
29
|
+
export async function readBundleText(root, extensions, maxBytes = 64 * 1024 * 1024) {
|
|
30
|
+
const files = await walkDir(root);
|
|
31
|
+
const exts = new Set(extensions.map((e) => e.toLowerCase()));
|
|
32
|
+
let total = 0;
|
|
33
|
+
const chunks = [];
|
|
34
|
+
for (const f of files) {
|
|
35
|
+
if (!exts.has(path.extname(f.rel).toLowerCase()))
|
|
36
|
+
continue;
|
|
37
|
+
if (total + f.size > maxBytes)
|
|
38
|
+
break;
|
|
39
|
+
total += f.size;
|
|
40
|
+
chunks.push(await fs.readFile(f.abs, 'utf8'));
|
|
41
|
+
}
|
|
42
|
+
return chunks.join('\n');
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=walk.js.map
|
package/dist/walk.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.js","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,qEAAqE;AAErE,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AASlC,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAY;IACxC,MAAM,GAAG,GAAiB,EAAE,CAAC;IAC7B,KAAK,UAAU,OAAO,CAAC,GAAW;QAChC,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/D,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;YACnC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;gBACpB,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;iBAAM,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;gBACtB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC9B,GAAG,CAAC,IAAI,CAAC;oBACP,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;oBACvD,GAAG;oBACH,IAAI,EAAE,EAAE,CAAC,IAAI;iBACd,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,iFAAiF;AACjF,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAY,EACZ,UAAoB,EACpB,QAAQ,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI;IAE3B,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC7D,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;YAAE,SAAS;QAC3D,IAAI,KAAK,GAAG,CAAC,CAAC,IAAI,GAAG,QAAQ;YAAE,MAAM;QACrC,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC;QAChB,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "carboncanvas-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Carbon Canvas CLI \u2014 build your prototype locally and publish the static output to a hosted, commentable review link. Bin name: `carboncanvas`.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"carboncanvas": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CONTRACT.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"start": "node dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"tar": "^7.4.3"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.10.2",
|
|
29
|
+
"typescript": "^5.7.2",
|
|
30
|
+
"vitest": "^2.1.8"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/rollsroych/quilt.git",
|
|
39
|
+
"directory": "cli"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://app.carboncanvas.design"
|
|
42
|
+
}
|