@mondaydotcomorg/z2h-cli 0.7.2 → 0.9.1
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/dist/commands/build.d.ts +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +4 -3
- package/dist/commands/clean.d.ts +1 -1
- package/dist/commands/clean.d.ts.map +1 -1
- package/dist/commands/clean.js +5 -4
- package/dist/commands/create-workspace.d.ts +1 -1
- package/dist/commands/create-workspace.d.ts.map +1 -1
- package/dist/commands/create-workspace.js +14 -14
- package/dist/commands/create.d.ts +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +27 -13
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +45 -11
- package/dist/commands/dev.d.ts +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +10 -6
- package/dist/commands/migrate.d.ts +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +1 -1
- package/dist/esm/commands/build.d.ts +1 -1
- package/dist/esm/commands/build.d.ts.map +1 -1
- package/dist/esm/commands/build.mjs +2 -1
- package/dist/esm/commands/clean.d.ts +1 -1
- package/dist/esm/commands/clean.d.ts.map +1 -1
- package/dist/esm/commands/clean.mjs +2 -1
- package/dist/esm/commands/create-workspace.d.ts +1 -1
- package/dist/esm/commands/create-workspace.d.ts.map +1 -1
- package/dist/esm/commands/create-workspace.mjs +1 -1
- package/dist/esm/commands/create.d.ts +1 -1
- package/dist/esm/commands/create.d.ts.map +1 -1
- package/dist/esm/commands/create.mjs +16 -2
- package/dist/esm/commands/deploy.d.ts +1 -1
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +42 -8
- package/dist/esm/commands/dev.d.ts +1 -1
- package/dist/esm/commands/dev.d.ts.map +1 -1
- package/dist/esm/commands/dev.mjs +5 -1
- package/dist/esm/commands/migrate.d.ts +1 -1
- package/dist/esm/commands/migrate.d.ts.map +1 -1
- package/dist/esm/commands/migrate.mjs +1 -1
- package/dist/esm/index.mjs +4 -1
- package/dist/esm/migrations/auto-migrate.mjs +1 -1
- package/dist/esm/templates/app/claude-settings.mjs +1 -1
- package/dist/esm/util/git/__tests__/repo.test.d.ts +2 -0
- package/dist/esm/util/git/__tests__/repo.test.d.ts.map +1 -0
- package/dist/esm/util/git/repo.d.ts +12 -0
- package/dist/esm/util/git/repo.d.ts.map +1 -0
- package/dist/esm/util/git/repo.mjs +105 -0
- package/dist/esm/util/manifest.d.ts +1 -0
- package/dist/esm/util/manifest.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/migrations/auto-migrate.js +11 -11
- package/dist/shadow/consumer-pkg.js +2 -2
- package/dist/shadow/gitignore.js +4 -4
- package/dist/shadow/merge-package-json.js +2 -2
- package/dist/shadow/metadata.js +4 -4
- package/dist/shadow/prepare.js +5 -5
- package/dist/shadow/scaffold.js +8 -8
- package/dist/shadow/sync.js +3 -3
- package/dist/templates/app/claude-settings.js +1 -1
- package/dist/util/auth/auth-store.js +4 -4
- package/dist/util/auth/write-auth.js +3 -3
- package/dist/util/git/__tests__/repo.test.d.ts +2 -0
- package/dist/util/git/__tests__/repo.test.d.ts.map +1 -0
- package/dist/util/git/repo.d.ts +12 -0
- package/dist/util/git/repo.d.ts.map +1 -0
- package/dist/util/git/repo.js +120 -0
- package/dist/util/manifest.d.ts +1 -0
- package/dist/util/manifest.d.ts.map +1 -1
- package/dist/util/s3/upload.js +2 -2
- package/dist/util/workspace.js +3 -3
- package/package.json +1 -1
- package/src/commands/__tests__/create-workspace.test.ts +4 -4
- package/src/commands/__tests__/create.test.ts +12 -5
- package/src/commands/build.ts +2 -1
- package/src/commands/clean.ts +2 -1
- package/src/commands/create-workspace.ts +1 -1
- package/src/commands/create.ts +15 -2
- package/src/commands/deploy.ts +43 -8
- package/src/commands/dev.ts +5 -1
- package/src/commands/migrate.ts +1 -1
- package/src/index.ts +4 -1
- package/src/migrations/auto-migrate.ts +3 -3
- package/src/templates/app/CLAUDE.md +3 -1
- package/src/templates/app/claude-settings.json +1 -1
- package/src/util/git/__tests__/repo.test.ts +208 -0
- package/src/util/git/repo.ts +112 -0
- package/src/util/manifest.ts +1 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
2
|
|
|
3
3
|
const promises = require('node:fs/promises');
|
|
4
|
-
const
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
5
|
const util_auth_authStore = require('./auth-store.js');
|
|
6
6
|
|
|
7
7
|
// 0o600 — owner read/write only. The file holds a long-lived bearer token,
|
|
8
8
|
// so other local users on the machine must not be able to read it.
|
|
9
9
|
const OWNER_READ_WRITE_FILE_MODE = 0o600;
|
|
10
10
|
async function writeZthAuth(auth) {
|
|
11
|
-
await
|
|
12
|
-
await
|
|
11
|
+
await fs.ensureDir(util_auth_authStore.Z2H_AUTH_DIR);
|
|
12
|
+
await fs.writeFile(util_auth_authStore.Z2H_AUTH_FILE, `${JSON.stringify(auth, null, 2)}\n`);
|
|
13
13
|
await promises.chmod(util_auth_authStore.Z2H_AUTH_FILE, OWNER_READ_WRITE_FILE_MODE);
|
|
14
14
|
return { filePath: util_auth_authStore.Z2H_AUTH_FILE };
|
|
15
15
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo.test.d.ts","sourceRoot":"","sources":["../../../../src/util/git/__tests__/repo.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class GitConflictError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare function defaultS3Remote(app: string): string;
|
|
5
|
+
export declare function initAndPush(dir: string, remote: string): Promise<void>;
|
|
6
|
+
export declare function cloneRepo(remote: string, dir: string): Promise<void>;
|
|
7
|
+
export declare function safeBackupIfDirty(dir: string): Promise<void>;
|
|
8
|
+
export declare function pullAndResolve(dir: string): Promise<void>;
|
|
9
|
+
export declare function remoteHead(dir: string): Promise<string>;
|
|
10
|
+
export declare function localHead(dir: string): Promise<string>;
|
|
11
|
+
export declare function commitDetailedAndPush(dir: string, version: number): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=repo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../src/util/git/repo.ts"],"names":[],"mappings":"AAKA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO5E;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS/D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASvF"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const execa = require('execa');
|
|
6
|
+
const constants = require('../../constants.js');
|
|
7
|
+
|
|
8
|
+
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
9
|
+
|
|
10
|
+
const path__default = /*#__PURE__*/_interopDefault(path);
|
|
11
|
+
const fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
12
|
+
|
|
13
|
+
class GitConflictError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'GitConflictError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
|
|
20
|
+
function defaultS3Remote(app) {
|
|
21
|
+
return `s3://${constants.BUCKET_WRITE_NAME}/${app}/git`;
|
|
22
|
+
}
|
|
23
|
+
async function initAndPush(dir, remote) {
|
|
24
|
+
await ensureGitignore(dir);
|
|
25
|
+
await execa.execa('git', ['init', '-b', 'master'], { cwd: dir });
|
|
26
|
+
await execa.execa('git', ['add', '.'], { cwd: dir });
|
|
27
|
+
await execa.execa('git', ['commit', '-m', 'Initial commit'], { cwd: dir });
|
|
28
|
+
await execa.execa('git', ['remote', 'add', 'origin', remote], { cwd: dir });
|
|
29
|
+
await execa.execa('git', ['push', '-u', 'origin', 'master'], { cwd: dir });
|
|
30
|
+
}
|
|
31
|
+
async function cloneRepo(remote, dir) {
|
|
32
|
+
await execa.execa('git', ['clone', remote, dir]);
|
|
33
|
+
}
|
|
34
|
+
async function safeBackupIfDirty(dir) {
|
|
35
|
+
let status;
|
|
36
|
+
try {
|
|
37
|
+
({ stdout: status } = await execa.execa('git', ['status', '--porcelain'], { cwd: dir }));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!status.trim()) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const sha = await localHead(dir).catch(() => 'unknown');
|
|
46
|
+
const appName = path__default.default.basename(dir);
|
|
47
|
+
const backupDir = path__default.default.join(path__default.default.dirname(dir), `${appName}.backup-${sha}`);
|
|
48
|
+
await fs__default.default.copy(dir, backupDir, { overwrite: false });
|
|
49
|
+
}
|
|
50
|
+
async function pullAndResolve(dir) {
|
|
51
|
+
try {
|
|
52
|
+
await execa.execa('git', ['pull', '--ff-only'], { cwd: dir });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// TODO(Phase 7): safeBackupIfDirty(dir) → resolveDivergence(dir) instead of throwing
|
|
56
|
+
throw new GitConflictError('This app changed while you were working — please re-open it to get the latest version.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function remoteHead(dir) {
|
|
60
|
+
const { stdout } = await execa.execa('git', ['ls-remote', 'origin', 'refs/heads/master'], { cwd: dir });
|
|
61
|
+
const sha = stdout.split('\t')[0]?.trim();
|
|
62
|
+
if (!sha) {
|
|
63
|
+
throw new Error('could not read remote head — is the remote reachable?');
|
|
64
|
+
}
|
|
65
|
+
return sha;
|
|
66
|
+
}
|
|
67
|
+
async function localHead(dir) {
|
|
68
|
+
const { stdout } = await execa.execa('git', ['rev-parse', 'HEAD'], { cwd: dir });
|
|
69
|
+
return stdout.trim();
|
|
70
|
+
}
|
|
71
|
+
async function commitDetailedAndPush(dir, version) {
|
|
72
|
+
const message = await buildCommitMessage(dir, version);
|
|
73
|
+
await execa.execa('git', ['add', '.'], { cwd: dir });
|
|
74
|
+
await execa.execa('git', ['commit', '-m', message], { cwd: dir });
|
|
75
|
+
try {
|
|
76
|
+
await execa.execa('git', ['push'], { cwd: dir });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new GitConflictError('push rejected — remote has new commits');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function buildCommitMessage(dir, version) {
|
|
83
|
+
// TODO(Phase 7): replace with LLM-generated summary using git diff + commit messages
|
|
84
|
+
const { stdout } = await execa.execa('git', ['status', '--short'], { cwd: dir });
|
|
85
|
+
const files = stdout
|
|
86
|
+
.split('\n')
|
|
87
|
+
.map(line => line.slice(3).trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
const fileList = files.length
|
|
90
|
+
? files.slice(0, 5).join(', ') + (files.length > 5 ? ` (+${files.length - 5} more)` : '')
|
|
91
|
+
: 'no changes';
|
|
92
|
+
return `v${version}: deploy — ${fileList}`;
|
|
93
|
+
}
|
|
94
|
+
async function ensureGitignore(dir) {
|
|
95
|
+
const gitignorePath = path__default.default.join(dir, '.gitignore');
|
|
96
|
+
let content = '';
|
|
97
|
+
try {
|
|
98
|
+
content = await fs__default.default.readFile(gitignorePath, 'utf8');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// file doesn't exist yet
|
|
102
|
+
}
|
|
103
|
+
const lines = content.split('\n').map(l => l.trim());
|
|
104
|
+
const missing = REQUIRED_GITIGNORE_ENTRIES.filter(entry => !lines.includes(entry));
|
|
105
|
+
if (missing.length === 0) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const separator = content && !content.endsWith('\n') ? '\n' : '';
|
|
109
|
+
await fs__default.default.outputFile(gitignorePath, content + separator + missing.join('\n') + '\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
exports.GitConflictError = GitConflictError;
|
|
113
|
+
exports.cloneRepo = cloneRepo;
|
|
114
|
+
exports.commitDetailedAndPush = commitDetailedAndPush;
|
|
115
|
+
exports.defaultS3Remote = defaultS3Remote;
|
|
116
|
+
exports.initAndPush = initAndPush;
|
|
117
|
+
exports.localHead = localHead;
|
|
118
|
+
exports.pullAndResolve = pullAndResolve;
|
|
119
|
+
exports.remoteHead = remoteHead;
|
|
120
|
+
exports.safeBackupIfDirty = safeBackupIfDirty;
|
package/dist/util/manifest.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAErD,wBAAsB,YAAY,IAAI,OAAO,CAAC,QAAQ,CAAC,CAQtD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,UAI1D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAE3F;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrE"}
|
package/dist/util/s3/upload.js
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const promises = require('node:fs/promises');
|
|
5
|
-
const
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
6
|
const mimeTypes = require('mime-types');
|
|
7
7
|
const pMap = require('p-map');
|
|
8
8
|
const clientS3 = require('@aws-sdk/client-s3');
|
|
@@ -23,7 +23,7 @@ async function uploadDir(localDir, keyPrefix) {
|
|
|
23
23
|
const files = entries.filter(e => e.isFile()).map(e => path__default.default.relative(localDir, path__default.default.join(e.parentPath, e.name)));
|
|
24
24
|
const s3 = util_s3_client.getS3Client();
|
|
25
25
|
await pMap__default.default(files, async (rel) => {
|
|
26
|
-
const body = await
|
|
26
|
+
const body = await fs.readFile(path__default.default.join(localDir, rel));
|
|
27
27
|
const contentType = mimeTypes.lookup(rel) || 'application/octet-stream';
|
|
28
28
|
const key = `${keyPrefix}/${rel.split(path__default.default.sep).join('/')}`;
|
|
29
29
|
await s3.send(new clientS3.PutObjectCommand({
|
package/dist/util/workspace.js
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const os = require('node:os');
|
|
5
|
-
const
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
6
|
|
|
7
7
|
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
8
8
|
|
|
@@ -26,11 +26,11 @@ function getWorkspaceHome() {
|
|
|
26
26
|
*/
|
|
27
27
|
async function isZ2hWorkspace(dir) {
|
|
28
28
|
const pkgPath = path__default.default.join(dir, 'package.json');
|
|
29
|
-
if (!(await
|
|
29
|
+
if (!(await fs.pathExists(pkgPath))) {
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
32
|
try {
|
|
33
|
-
const pkg = (await
|
|
33
|
+
const pkg = (await fs.readJson(pkgPath));
|
|
34
34
|
return pkg[WORKSPACE_MARKER_FIELD] === true;
|
|
35
35
|
}
|
|
36
36
|
catch {
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@ describe('create-workspace', () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it('creates package.json with the marker, gitignore, README, then runs yarn install', async () => {
|
|
34
|
-
await createWorkspaceCommand(
|
|
34
|
+
await createWorkspaceCommand();
|
|
35
35
|
|
|
36
36
|
const pkgPath = path.join(tmp, 'package.json');
|
|
37
37
|
expect(await pathExists(pkgPath)).toBe(true);
|
|
@@ -55,19 +55,19 @@ describe('create-workspace', () => {
|
|
|
55
55
|
await ensureDir(tmp);
|
|
56
56
|
await writeJson(path.join(tmp, 'somefile.json'), { hello: 'world' });
|
|
57
57
|
|
|
58
|
-
await expect(createWorkspaceCommand(
|
|
58
|
+
await expect(createWorkspaceCommand()).rejects.toThrow(/Workspace already exists/);
|
|
59
59
|
expect(runYarnInstall).not.toHaveBeenCalled();
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it('does not include z2h-cli in workspace devDeps (managed globally)', async () => {
|
|
63
|
-
await createWorkspaceCommand(
|
|
63
|
+
await createWorkspaceCommand();
|
|
64
64
|
|
|
65
65
|
const pkg = await readJson(path.join(tmp, 'package.json'));
|
|
66
66
|
expect(pkg.devDependencies['@mondaydotcomorg/z2h-cli']).toBeUndefined();
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it('ships the yarn berry release inline so any `yarn` invocation runs 4.x without corepack', async () => {
|
|
70
|
-
await createWorkspaceCommand(
|
|
70
|
+
await createWorkspaceCommand();
|
|
71
71
|
|
|
72
72
|
expect(await pathExists(path.join(tmp, '.yarn', 'releases', 'yarn-4.5.1.cjs'))).toBe(true);
|
|
73
73
|
});
|
|
@@ -4,6 +4,7 @@ import { mkdtemp, rm, writeJson, readJson, ensureDir, pathExists, writeFile } fr
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
6
|
import { runYarnInstall } from '../../util/npm/install';
|
|
7
|
+
import { buildCommand } from '../build';
|
|
7
8
|
import { createCommand } from '../create';
|
|
8
9
|
import * as manifest from '../../util/manifest';
|
|
9
10
|
|
|
@@ -14,6 +15,11 @@ vi.mock('../../util/npm/install', () => ({
|
|
|
14
15
|
runYarnInstall: vi.fn().mockResolvedValue(undefined),
|
|
15
16
|
}));
|
|
16
17
|
|
|
18
|
+
// Mock build so tsc/yarn build don't run in tests.
|
|
19
|
+
vi.mock('../build', () => ({
|
|
20
|
+
buildCommand: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
}));
|
|
22
|
+
|
|
17
23
|
vi.spyOn(manifest, 'readManifest').mockResolvedValue({});
|
|
18
24
|
|
|
19
25
|
async function makeWorkspace(tmp: string): Promise<void> {
|
|
@@ -33,6 +39,7 @@ describe('create', () => {
|
|
|
33
39
|
tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-c-'));
|
|
34
40
|
process.env.Z2H_WORKSPACE_HOME = tmp;
|
|
35
41
|
vi.mocked(runYarnInstall).mockClear();
|
|
42
|
+
vi.mocked(buildCommand).mockClear();
|
|
36
43
|
});
|
|
37
44
|
|
|
38
45
|
afterEach(async () => {
|
|
@@ -41,14 +48,14 @@ describe('create', () => {
|
|
|
41
48
|
});
|
|
42
49
|
|
|
43
50
|
it('refuses when there is no workspace marker', async () => {
|
|
44
|
-
await expect(createCommand('demo-app'
|
|
51
|
+
await expect(createCommand('demo-app')).rejects.toThrow(/create-workspace/);
|
|
45
52
|
expect(runYarnInstall).not.toHaveBeenCalled();
|
|
46
53
|
});
|
|
47
54
|
|
|
48
55
|
it('rejects invalid app names', async () => {
|
|
49
56
|
await makeWorkspace(tmp);
|
|
50
|
-
await expect(createCommand('Invalid_Name'
|
|
51
|
-
await expect(createCommand('1starts-with-digit'
|
|
57
|
+
await expect(createCommand('Invalid_Name')).rejects.toThrow(/lowercase kebab-case/);
|
|
58
|
+
await expect(createCommand('1starts-with-digit')).rejects.toThrow(/lowercase kebab-case/);
|
|
52
59
|
});
|
|
53
60
|
|
|
54
61
|
it('refuses when the target app dir already exists and is non-empty', async () => {
|
|
@@ -56,12 +63,12 @@ describe('create', () => {
|
|
|
56
63
|
const appDir = path.join(tmp, 'demo-app');
|
|
57
64
|
await ensureDir(appDir);
|
|
58
65
|
await writeFile(path.join(appDir, 'something.txt'), 'hi');
|
|
59
|
-
await expect(createCommand('demo-app'
|
|
66
|
+
await expect(createCommand('demo-app')).rejects.toThrow(/non-empty/);
|
|
60
67
|
});
|
|
61
68
|
|
|
62
69
|
it('scaffolds an app with the default React template', async () => {
|
|
63
70
|
await makeWorkspace(tmp);
|
|
64
|
-
await createCommand('demo-app'
|
|
71
|
+
await createCommand('demo-app');
|
|
65
72
|
|
|
66
73
|
const appDir = path.join(tmp, 'demo-app');
|
|
67
74
|
expect(await pathExists(path.join(appDir, 'package.json'))).toBe(true);
|
package/src/commands/build.ts
CHANGED
|
@@ -8,7 +8,8 @@ export interface BuildOptions {
|
|
|
8
8
|
publicUrl?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
12
|
+
export async function buildCommand(opts: BuildOptions = {}): Promise<void> {
|
|
12
13
|
if (opts.json) {
|
|
13
14
|
setOutputMode('json');
|
|
14
15
|
}
|
package/src/commands/clean.ts
CHANGED
|
@@ -6,7 +6,8 @@ export interface CleanOptions {
|
|
|
6
6
|
json?: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
10
|
+
export async function cleanCommand(opts: CleanOptions = {}): Promise<void> {
|
|
10
11
|
if (opts.json) {
|
|
11
12
|
setOutputMode('json');
|
|
12
13
|
}
|
|
@@ -10,7 +10,7 @@ export interface CreateWorkspaceOptions {
|
|
|
10
10
|
json?: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Promise<void> {
|
|
13
|
+
export async function createWorkspaceCommand(opts: CreateWorkspaceOptions = {}): Promise<void> {
|
|
14
14
|
if (opts.json) {
|
|
15
15
|
setOutputMode('json');
|
|
16
16
|
}
|
package/src/commands/create.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { ensureDir, pathExists, readdir, writeFile, copy } from 'fs-extra';
|
|
4
|
-
import { info, output, setOutputMode } from '../util/logger';
|
|
4
|
+
import { info, warn, output, setOutputMode } from '../util/logger';
|
|
5
5
|
import { assertWorkspace } from '../util/workspace';
|
|
6
6
|
import { getTemplatesDir, renderTemplate, APP_TEMPLATES, type AppTemplate } from '../util/templates';
|
|
7
7
|
import { readManifest } from '../util/manifest';
|
|
8
8
|
import { mfAppName } from '../util/app-name';
|
|
9
9
|
import { APP_NAME_PATTERN } from '../constants';
|
|
10
10
|
import { runYarnInstall } from '../util/npm/install';
|
|
11
|
+
import { buildCommand } from './build';
|
|
11
12
|
|
|
12
13
|
export interface CreateOptions {
|
|
13
14
|
template?: string;
|
|
@@ -42,7 +43,7 @@ async function isDirEmpty(dir: string): Promise<boolean> {
|
|
|
42
43
|
return entries.length === 0;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
export async function createCommand(appName: string, opts: CreateOptions): Promise<void> {
|
|
46
|
+
export async function createCommand(appName: string, opts: CreateOptions = {}): Promise<void> {
|
|
46
47
|
if (opts.json) {
|
|
47
48
|
setOutputMode('json');
|
|
48
49
|
}
|
|
@@ -118,6 +119,18 @@ export async function createCommand(appName: string, opts: CreateOptions): Promi
|
|
|
118
119
|
info(`installing dependencies for ${appName}...`);
|
|
119
120
|
await runYarnInstall({ cwd: workspace });
|
|
120
121
|
|
|
122
|
+
// build/dev/deploy all locate the consumer via process.cwd() — chdir before calling them.
|
|
123
|
+
const prevCwd = process.cwd();
|
|
124
|
+
try {
|
|
125
|
+
info(`pre-building ${appName}`);
|
|
126
|
+
process.chdir(appDir);
|
|
127
|
+
await buildCommand();
|
|
128
|
+
} catch {
|
|
129
|
+
warn(`pre-build failed. No worries — it will run again when needed.`);
|
|
130
|
+
} finally {
|
|
131
|
+
process.chdir(prevCwd);
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
output(`[z2h-cli] app ready at ${appDir}`, {
|
|
122
135
|
ok: true,
|
|
123
136
|
appDir,
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { pathExists, readJson } from 'fs-extra';
|
|
4
|
-
import { info, output, setOutputMode } from '../util/logger';
|
|
4
|
+
import { info, output, setOutputMode, warn } from '../util/logger';
|
|
5
5
|
import { resolveConsumerPaths } from '../shadow/paths';
|
|
6
6
|
import {
|
|
7
7
|
nextVersion,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type ManifestEntry,
|
|
13
13
|
} from '../util/manifest';
|
|
14
14
|
import { uploadDir } from '../util/s3/upload';
|
|
15
|
-
import { BUCKET_WRITE_NAME, BUCKET_READ_DNS_URL } from '../constants';
|
|
15
|
+
import { BUCKET_WRITE_NAME, BUCKET_READ_DNS_URL, brokerBaseUrl } from '../constants';
|
|
16
16
|
import { setupS3Client } from '../util/s3/client';
|
|
17
17
|
import { readZthAuth } from '../util/auth/auth-store';
|
|
18
18
|
import { cleanAppName, mfAppName } from '../util/app-name';
|
|
@@ -23,7 +23,8 @@ export interface DeployOptions {
|
|
|
23
23
|
json?: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
27
|
+
export async function deployCommand(opts: DeployOptions = {}): Promise<void> {
|
|
27
28
|
if (opts.json) {
|
|
28
29
|
setOutputMode('json');
|
|
29
30
|
}
|
|
@@ -37,7 +38,7 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
|
|
|
37
38
|
|
|
38
39
|
// Validate the user's deploy token is on disk before doing any expensive
|
|
39
40
|
// work. Throws Z2hAuthMissingError with a remediation hint if not.
|
|
40
|
-
await readZthAuth();
|
|
41
|
+
const { token } = await readZthAuth();
|
|
41
42
|
setupS3Client({ appName });
|
|
42
43
|
|
|
43
44
|
info('reading current Z2H manifest');
|
|
@@ -82,11 +83,19 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
|
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
try {
|
|
87
|
+
// Lock is acquired here — uploadDir triggers the STS credential provider on first S3 call
|
|
88
|
+
info(`uploading build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
|
|
89
|
+
await uploadDir(paths.shadowBuild, keyPrefix);
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
info(`updating manifest entry "${appName}" → v${version}`);
|
|
92
|
+
await writeManifest(upsertEntry(manifest, appName, entry));
|
|
93
|
+
|
|
94
|
+
info(`registering app "${appName}" → v${version}`);
|
|
95
|
+
await registerApp(appName, token, version, entry);
|
|
96
|
+
} finally {
|
|
97
|
+
await releaseLockBestEffort(appName, token);
|
|
98
|
+
}
|
|
90
99
|
|
|
91
100
|
// The host MF parses the app name from the URL and prepends `mf-` itself
|
|
92
101
|
// before looking it up in the manifest. So the user-facing URL must use
|
|
@@ -103,3 +112,29 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
|
|
|
103
112
|
entry,
|
|
104
113
|
});
|
|
105
114
|
}
|
|
115
|
+
|
|
116
|
+
async function registerApp(appName: string, token: string, version: number, entry: ManifestEntry): Promise<void> {
|
|
117
|
+
const res = await fetch(`${brokerBaseUrl()}/z2h-cli/apps/register`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ appName, version, entry }),
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
const text = await res.text();
|
|
124
|
+
throw new Error(`App registration failed (${res.status}): ${text.slice(0, 1024)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// best-effort — the 5-min TTL backstop handles failures
|
|
129
|
+
async function releaseLockBestEffort(appName: string, token: string): Promise<void> {
|
|
130
|
+
try {
|
|
131
|
+
await fetch(`${brokerBaseUrl()}/z2h-cli/lock/release`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ appName }),
|
|
135
|
+
});
|
|
136
|
+
info(`lock released for ${appName}`);
|
|
137
|
+
} catch {
|
|
138
|
+
warn(`failed to release lock for ${appName}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/commands/dev.ts
CHANGED
|
@@ -15,7 +15,8 @@ export interface DevOptions {
|
|
|
15
15
|
|
|
16
16
|
const TRIDENT_READY_PATTERN = /Server started at/i;
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Should be run from the consumer app directory — uses process.cwd() to locate it.
|
|
19
|
+
export async function devCommand(opts: DevOptions = {}): Promise<void> {
|
|
19
20
|
if (opts.json) {
|
|
20
21
|
setOutputMode('json');
|
|
21
22
|
}
|
|
@@ -31,6 +32,9 @@ export async function devCommand(opts: DevOptions): Promise<void> {
|
|
|
31
32
|
const server = execa('yarn', ['start'], {
|
|
32
33
|
cwd: ctx.paths.shadowDir,
|
|
33
34
|
stdio: ['inherit', 'pipe', 'inherit'],
|
|
35
|
+
// Trident requires NPM_TOKEN to be set or it refuses to start; 'FAKE_TOKEN' satisfies the check without real credentials.
|
|
36
|
+
// we always pass a fake token for same behavior for all users (no matter if they have a token or not)
|
|
37
|
+
env: { ...process.env, NPM_TOKEN: 'FAKE_TOKEN' },
|
|
34
38
|
});
|
|
35
39
|
|
|
36
40
|
const pidFile = path.join(ctx.paths.shadowDir, 'dev.pid');
|
package/src/commands/migrate.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface MigrateOptions {
|
|
|
6
6
|
json?: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export async function migrateCommand(cliVersion: string, opts: MigrateOptions): Promise<void> {
|
|
9
|
+
export async function migrateCommand(cliVersion: string, opts: MigrateOptions = {}): Promise<void> {
|
|
10
10
|
if (opts.json) {
|
|
11
11
|
setOutputMode('json');
|
|
12
12
|
}
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,10 @@ const UPDATE_SCRIPT = path.join(os.homedir(), 'Development', 'bigbrain-z2h', 'z2
|
|
|
21
21
|
|
|
22
22
|
function selfUpdate(): void {
|
|
23
23
|
try {
|
|
24
|
-
execa('bash', [UPDATE_SCRIPT], {
|
|
24
|
+
execa('bash', [UPDATE_SCRIPT], {
|
|
25
|
+
detached: true,
|
|
26
|
+
stdio: 'ignore',
|
|
27
|
+
}).unref();
|
|
25
28
|
} catch (err) {
|
|
26
29
|
errorLog(`Z2H auto-update failed to start: ${(err as Error).message}`);
|
|
27
30
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { createRequire } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { pathExists, readJson,
|
|
4
|
+
import { pathExists, readJson, writeFile, writeJson } from 'fs-extra';
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
import semver from 'semver';
|
|
7
|
-
import { info, warn
|
|
7
|
+
import { errorLog, info, warn } from '../util/logger';
|
|
8
8
|
import { getWorkspaceHome, isZ2hWorkspace } from '../util/workspace';
|
|
9
9
|
|
|
10
10
|
const STATE_FILE = '.z2h-migrations-state.json';
|
|
@@ -27,7 +27,9 @@ The orchestrator routes to `z2h:feature-development`, which handles the typechec
|
|
|
27
27
|
|
|
28
28
|
## 🌟 Data-Fetching Hierarchy
|
|
29
29
|
|
|
30
|
-
When a request needs data, follow `z2h:coding-standards-mf` §0: write a `runQuery` helper for Snowflake data (TS module using `getBigBrainAPI()` for React apps; vanilla `<script>` using `window.location.origin` for html-embed apps), write a client-side fetch for other browser-accessible sources, or tell the user to reach out at `#ask-zero-to-hero` if the data needs an API key or server-side access. Backend changes to `bigbrain-zth` are not in scope.
|
|
30
|
+
When a request needs data, follow `z2h:coding-standards-mf` §0: write a `runQuery` helper for Snowflake data (TS module using `getBigBrainAPI()` for React apps; vanilla `<script>` using `window.location.origin` for html-embed apps), write a client-side fetch for other browser-accessible sources, or tell the user to reach out at `#ask-zero-to-hero` if the data needs an API key or server-side access. Backend changes to `bigbrain-zth` are not in scope — no modifications, no suggestions, no workarounds.
|
|
31
|
+
|
|
32
|
+
**Query storage:** All SQL query strings go in `src/queries.ts`, not inline in components or hooks. Import from there.
|
|
31
33
|
|
|
32
34
|
---
|
|
33
35
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"hooks": [
|
|
11
11
|
{
|
|
12
12
|
"type": "command",
|
|
13
|
-
"command": "if [ -f .zth/dev.pid ]; then
|
|
13
|
+
"command": "if [ -f .zth/dev.pid ]; then PGID=$(ps -o pgid= -p $(cat .zth/dev.pid) 2>/dev/null | tr -d ' '); [ -n \"$PGID\" ] && kill -- -$PGID 2>/dev/null; rm -f .zth/dev.pid; fi"
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
}
|